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.
