Breakout
Breakout je arkádová hra vytvorená v sedemdesiatych rokoch spoločnosťou Atari. V nej hráč ovláda malú plošinu, ktorá odráža pohybujúcu sa loptičku. Cieľom hry je zničiť loptičkou všetky tehličky nachádzajúce sa v hornej časti obrazovky. Lopta sa nesmie dostať pod úroveň plošiny; v opačnom prípade stráca hráč život.
Neskoršia verzia hry s novými prvkami je známa pod názvom Arkanoid.
Tkinter
Tkinter je grafická knižnica pre tvorbu desktopových aplikácií. Je súčasťou štandardnej distribúcie jazyka Python. Ide o multiplatformovú knižnicu, ktorá beží na Linuxe, systéme Windows a Mac OS. Z technického hľadiska je Tkinter obálkou nad grafickou knižnicou Tk, ktorá bola vytvorená pre jazyk Tcl. Viac informácií o Tkinter si môžete naštudovať na oficiálnej stránke dokumentácie k Tk, tutoriály na ZetCode, alebo na tkinter.programujte.com.
Canvas
Komponenta Canvas
je grafickým plátnom, ktoré slúži v Tkinteri na prácu s grafikou. V prípade tejto komponenty pracujeme s grafikou na vyššej úrovni; t.j. nereagujeme na udalosti prekreslenia, ale tvoríme grafické objekty priamo volaním metód plátna. Pomocou komponenty Canvas
môžeme vytvárať jednoduché hry, kresliť grafy, alebo vytvárať vlastné komponenty.
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ This script creates a simple Breakout game clone. Author: Jan Bodnar Last modified: January 2016 Website: www.zetcode.com """ from tkinter import Tk, Canvas, BOTH, ALL from tkinter.ttk import Frame import colorsys BAR_WIDTH = 60 BAR_HEIGHT = 1 BAR_INIT_X = 170 BAR_Y = 250 BOTTOM_EDGE = 270 RIGHT_EDGE = 400 BALL_SIZE = 3 NEAR_BAR_Y = BAR_Y - BALL_SIZE - BAR_HEIGHT BALL_INIT_X = 200 BALL_INIT_Y = 150 INIT_DELAY = 800 DELAY = 30 class Example(Frame): def __init__(self, parent): Frame.__init__(self, parent) self.parent = parent self.initVariables() self.initBoard() self.after(INIT_DELAY, self.onTimer) def initVariables(self): self.bricks = [] self.ballvx = self.ballvy = 3 self.ball_x = BALL_INIT_X self.ball_y = BALL_INIT_Y self.inGame = True self.bar_x = BAR_INIT_X self.bar_y = BAR_Y self.lives = 3 def initBoard(self): self.parent.title("Breakout") self.pack(fill=BOTH, expand=True) self.parent.config(cursor="none") self.canvas = Canvas(self, width=400, height=300, background="#000000") self.canvas.bind("<Motion>", self.onMotion) self.bar = self.canvas.create_line(self.bar_x, self.bar_y, self.bar_x+BAR_WIDTH, self.bar_y, fill="#ffffff") self.lives_item = self.canvas.create_text(15, 270, text=self.lives, fill="white") k = 0.0 for j in range(10): for i in range(10): c = colorsys.hsv_to_rgb(k, 0.5, 0.4) d = hex(int(c[0]*256)<<16 | int(c[1]*256)<<8 | int(c[2]*256)) d = "#"+d[2:len(d)] k += 0.01 brick = self.canvas.create_rectangle(40*i, (j*10)+20, 40+(40*i), 30+(j*10), fill=d) self.bricks.append(brick) self.ball = self.canvas.create_oval(self.ball_x-BALL_SIZE, self.ball_y-BALL_SIZE, self.ball_x+BALL_SIZE, self.ball_y+BALL_SIZE, fill="#cccccc") self.canvas.pack(fill=BOTH, expand=True) def onMotion(self, e): if (e.x + BAR_WIDTH <= RIGHT_EDGE): self.bar_x = e.x def onTimer(self): if self.inGame: self.doCycle() self.checkCollisions() self.after(DELAY, self.onTimer) else: self.gameOver() def doCycle(self): self.ball_x += self.ballvx self.ball_y += self.ballvy self.canvas.coords(self.ball, self.ball_x - BALL_SIZE, self.ball_y - BALL_SIZE, self.ball_x + BALL_SIZE, self.ball_y + BALL_SIZE) self.canvas.coords(self.bar, self.bar_x, self.bar_y, self.bar_x+BAR_WIDTH, self.bar_y) if (len(self.bricks) == 0): self.msg = "Game won" self.inGame = False def checkCollisions(self): if (self.ball_x >= RIGHT_EDGE or self.ball_x <= 0): self.ballvx *= -1 if (self.ball_y <= 0): self.ballvy *= -1 for brick in self.bricks: hit = 0 co = self.canvas.coords(brick) if (self.ball_x > co[0] and self.ball_x < co[2] and self.ball_y + self.ballvy > co[1] and self.ball_y + self.ballvy < co[3]): hit = 1 self.ballvy *= -1 if (self.ball_x + self.ballvx > co[0] and self.ball_x + self.ballvx < co[2] and self.ball_y > co[1] and self.ball_y < co[3]): hit = 1 self.ballvx *= -1 if (hit == 1): self.bricks.remove(brick) self.canvas.delete(brick) if ((self.ball_y == NEAR_BAR_Y and self.ball_y < self.bar_y) and (self.ball_x > self.bar_x and self.ball_x < self.bar_x + BAR_WIDTH)): self.ballvy *= -1 if (self.ball_y == NEAR_BAR_Y and self.ball_x < self.bar_x + BAR_WIDTH/2 and self.ball_x > self.bar_x and self.ballvx > 0): self.ballvx *= -1 if (self.ball_y == NEAR_BAR_Y and self.ball_x > self.bar_x + BAR_WIDTH/2 and self.ball_x < self.bar_x + BAR_WIDTH and self.ballvx < 0): self.ballvx *= -1 if (self.ball_y > BOTTOM_EDGE): self.lives -= 1 self.canvas.delete(self.lives_item) self.lives_item = self.canvas.create_text(15, 270, text=self.lives, fill="white") if self.lives == 0: self.inGame = False self.msg = "Game lost" else: self.ball_x = BALL_INIT_X self.ball_y = BALL_INIT_Y def gameOver(self): self.canvas.delete(ALL) self.canvas.create_text(self.winfo_width()/2, self.winfo_height()/2, text=self.msg, fill="white") def main(): root = Tk() ex = Example(root) root.geometry("+300+300") root.mainloop() if __name__ == '__main__': main()
V tejto hre máme jeden objekt plošiny, jednu loptičku a sto tehličiek. Na vytvorenie herného cyklu sme využili časovač. Hráč ovláda plošinu pomocou myši. Ak dopadne loptička do bližšej polovice plošiny, loptička sa odrazí do protismeru; ináč sa odrazí v smere pohybu.
BAR_WIDTH = 60 BAR_HEIGHT = 1 BAR_INIT_X = 170 BAR_Y = 250 BOTTOM_EDGE = 270 RIGHT_EDGE = 400 BALL_SIZE = 3 NEAR_BAR_Y = BAR_Y - BALL_SIZE - BAR_HEIGHT BALL_INIT_X = 200 BALL_INIT_Y = 150 INIT_DELAY = 800 DELAY = 30
Na začiatku si definujeme zopár konštánt. BAR_WIDTH
predstavuje dĺžku plošiny a BAR_HEIGHT
jej hrúbku. BAR_INIT_X
je počiatočná x-sová súradnica horného ľavého bodu plošiny a BAR_Y
je y-lonová súradnica. (Y-lonová súradnica sa počas hry nemení.) BOTTOM_EDGE
a RIGHT_EDGE
určujú hranice plochy hry. BALL_SIZE
je veľkosť loptičky. NEAR_BAR_Y
je miestom, pri ktorom dochádza ku kontaktu loptičky a plošiny. BALL_INIT_X
a BALL_INIT_Y
sú východzie súradnice loptičky. Nakoniec, INIT_DELAY
a DELAY
sú východzie a normálne zdržanie v časovači.
self.initVariables() self.initBoard() self.after(INIT_DELAY, self.onTimer)
V konštruktore inicializujeme herné premenné, hernú plochu a štartujeme časovač. Časovač sa spustí pomocou metódy after()
.
def initVariables(self): self.bricks = [] self.ballvx = self.ballvy = 3 self.ball_x = BALL_INIT_X self.ball_y = BALL_INIT_Y self.inGame = True self.bar_x = BAR_INIT_X self.bar_y = BAR_Y self.lives = 3
V metóde initVariables()
inicializujeme dôležité herné premenné. Zoznam bricks
obsahuje objekty tehličiek. Premenné ballvx
a ballvy
sú horizontálne a vertikálne rýchlosti loptičky. Premenné ball_y
a ball_x
sú súradnice bodu v hornom ľavom rohu loptičky. Premenná inGame
určuje, či je hra ukončená alebo či stále prebieha. Premenné bar_x
a bar_y
sú súradnicami horného, ľavého bodu plošiny. Nakoniec premenná lives
kontroluje počet životov hráča.
def initBoard(self): self.parent.title("Arkanoid") self.pack(fill=BOTH, expand=True) self.parent.config(cursor="none") ...
Metódou initBoard()
vytvárame hernú plochu. Pomocou metódy config()
vypneme kurzor myši; hra sa ovláda pomocou myši a jej kurzor by nám prekážal.
self.canvas = Canvas(self, width=400, height=300, background="#000000") self.canvas.bind("<Motion>", self.onMotion)
Tu vytvárame komponentu Canvas
. Jej pozadie vyplníme čiernou farbou. Pomocou metódy bind()
napojíme udalosti vysielané myšou na nami vytvorenú metódu onMotion()
.
self.bar = self.canvas.create_line(self.bar_x, self.bar_y, self.bar_x+BAR_WIDTH, self.bar_y, fill="#ffffff")
Naša plošina je jednoduchú biela čiara. Objekt čiary na plátne vytvoríme pomocou metódy create_line()
. Parametrami metódy sú súradnice bodov čiary; je možné špecifikovať viac ako dva body. Parameter fill
udáva farbu čiary.
self.lives_item = self.canvas.create_text(15, 270, text=self.lives, fill="white")
Textový objekt sa vytvorí v dolnom ľavom rohu okna. Tento objekt zobrazuje počet našich životov. Vytvoríme ho pomocou metódy create_text()
.
k = 0.0 for j in range(10): for i in range(10): c = colorsys.hsv_to_rgb(k, 0.5, 0.4) d = hex(int(c[0]*256)<<16 | int(c[1]*256)<<8 | int(c[2]*256)) d = "#"+d[2:len(d)] k += 0.01 brick = self.canvas.create_rectangle(40*i, (j*10)+20, 40+(40*i), 30+(j*10), fill=d) self.bricks.append(brick)
V cykle for sa vytvorí sto objektov tehličiek v rôznych farbách. Tehličky sa uložia do premennej bricks
. Tehlička je obdĺžnikový objekt vytvorený pomocou metódy create_rectangle()
. Rôzne druhy farieb sú vytvorené modelom HSV (Hue, Saturation, Value).
self.ball = self.canvas.create_oval(self.ball_x-BALL_SIZE, self.ball_y-BALL_SIZE, self.ball_x+BALL_SIZE, self.ball_y+BALL_SIZE, fill="#cccccc")
Loptička je jednoduchý kruh vytvorený pomocou metódy create_oval()
. Pre vytvorenie oválneho tvaru je treba poskytnúť obdĺžnik, ktorý daný ovál ohraničuje. Obdĺžnik je daný dvoma bodmi: horným ľavým a dolným pravým bodom.
def onMotion(self, e): if (e.x + BAR_WIDTH <= RIGHT_EDGE): self.bar_x = e.x
Metóda onMotion()
sa volá ako reakcia na udalosti generované myšou. Vo vnútri metódy nastavíme x-sovú súradnicu plošiny. Tá nesmie prekročiť hodnotu, kde by už plošina prešla cez pravý okraj hracej plochy. Čerstvú x-sovú súradnicu získame z atribútu x
udalostného objektu, ktorú nám Tkinter posiela ako parameter.
def onTimer(self): if self.inGame: self.doCycle() self.checkCollisions() self.after(DELAY, self.onTimer) else: self.gameOver()
Každých DELAY
milisekúnd je volaná metóda onTimer()
. Ona kontroluje herný cyklus, testuje kolízie, alebo ukončuje hru.
def doCycle(self): self.ball_x += self.ballvx self.ball_y += self.ballvy self.canvas.coords(self.ball, self.ball_x - BALL_SIZE, self.ball_y - BALL_SIZE, self.ball_x + BALL_SIZE, self.ball_y + BALL_SIZE) self.canvas.coords(self.bar, self.bar_x, self.bar_y, self.bar_x+BAR_WIDTH, self.bar_y) if (len(self.bricks) == 0): self.msg = "Game won" self.inGame = False
Vo vnútri metódy doCycle()
aktualizujeme súradnice loptičky, premiestnime loptičku a plošinu pomocou metódy coords()
. Ak už nezostali žiadne tehličky, nastavíme premennú inGame
na False
.
def checkCollisions(self): if (self.ball_x >= RIGHT_EDGE or self.ball_x <= 0): self.ballvx *= -1 if (self.ball_y <= 0): self.ballvy *= -1 ...
V metóde checkCollisions()
kontrolujeme či nedošlo ku kolíziám. Loptička zmení smer, ak narazí na horný, ľavý alebo pravý okraj hernej plochy.
for brick in self.bricks: hit = 0 co = self.canvas.coords(brick) ...
V tomto for cykle kontrolujeme kolízie medzi tehličkami a loptičkou. Metóda coords()
môže byť využitá na posun objektu plátna alebo na zistenie jeho súradníc. V tomto prípade zisťujeme súradnice objektu.
if (self.ball_x > co[0] and self.ball_x < co[2] and self.ball_y + self.ballvy > co[1] and self.ball_y + self.ballvy < co[3]): hit = 1 self.ballvy *= -1
Ak loptička narazí na horný alebo dolný okraj tehličky, nastaví sa premenná hit
a zmení sa vertikálny smer loptičky.
if (self.ball_x + self.ballvx > co[0] and self.ball_x + self.ballvx < co[2] and self.ball_y > co[1] and self.ball_y < co[3]): hit = 1 self.ballvx *= -1
Podobne ak loptička narazí na pravý alebo ľavý okraj tehličky, nastaví sa premenná hit
a zmení sa horizontálny smer loptičky.
if (hit == 1): self.bricks.remove(brick) self.canvas.delete(brick)
Zasiahnutá tehlička je odstránená.
if ((self.ball_y == NEAR_BAR_Y and self.ball_y < self.bar_y) and (self.ball_x > self.bar_x and self.ball_x < self.bar_x + BAR_WIDTH)): self.ballvy *= -1
Ak loptička zasiahne plošinu, zmení sa jej vertikálny pohyb — odrazí sa od plošiny.
if (self.ball_y == NEAR_BAR_Y and self.ball_x < self.bar_x + BAR_WIDTH/2 and self.ball_x > self.bar_x and self.ballvx > 0): self.ballvx *= -1 if (self.ball_y == NEAR_BAR_Y and self.ball_x > self.bar_x + BAR_WIDTH/2 and self.ball_x < self.bar_x + BAR_WIDTH and self.ballvx < 0): self.ballvx *= -1
Zmeny v horizontálnom pohybe loptičky závisia od toho, na ktorú polovicu plošiny padne loptička.
if (self.ball_y > BOTTOM_EDGE): self.lives -= 1 self.canvas.delete(self.lives_item) self.lives_item = self.canvas.create_text(15, 270, text=self.lives, fill="white") if self.lives == 0: self.inGame = False self.msg = "Game lost" else: self.ball_x = BALL_INIT_X self.ball_y = BALL_INIT_Y
Ak loptička prejde cez spodok určený konštantou BOTTOM_EDGE
, strácame v hre život. Ak už nemáme viac herných životov, hra sa ukončí. Inak sa loptička znova zjaví vo svojej počiatočnej pozícii a hra pokračuje.
def gameOver(self): self.canvas.delete(ALL) self.canvas.create_text(self.winfo_width()/2, self.winfo_height()/2, text=self.msg, fill="white")
V metóde gameOver()
vymažeme všetky objekty plátna a vytvoríme záverečný text; buď „Game over“ alebo „Game lost“. Text sa vykreslí v strede okna. Veľkosť komponenty zistíme metódami winfo_width()
a winfo_height()
.
def main(): root = Tk() ex = Example(root) root.geometry("+300+300") root.mainloop()
V tomto kóde sa vytvára hlavné okno našej aplikácie a spúšťa sa slučka udalostí.
Zdroje
- Referenčná príručka k Tkinter
- Časť kódu pochádza z tohto postu
V tomto článku sme si ukázali, ako vytvoriť jednoduchú hru v Tkinteri. Na jej vytvorenie sme využili grafické plátno, ktoré nám poskytuje komponenta Canvas
. Odporúčame čitateľovi, aby si po prelúskaní kódu rozšíril hru o nové prvky alebo spravil jednoduché zmeny.