FINANCE.czFINANCE.cz

Hra Breakout napísaná v Tkinteri

17. 2. 2016
Doba čtení: 11 minut

Sdílet

Jedným z najlepších spôsobov, ako začať s programovaním, je naprogramovať si jednoduchú počítačovú hru. V tomto článku si ukážeme, ako vytvoriť jednoduchý klon hry Breakout pomocou knižnice Tkinter a jazyka Python.

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#!/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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.

1
2
3
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().

1
2
3
4
5
6
7
8
9
10
11
12
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.

1
2
3
4
5
6
7
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.

1
2
3
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().

1
2
3
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.

1
2
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().

1
2
3
4
5
6
7
8
9
10
11
12
13
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).

1
2
3
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.

1
2
3
4
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.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
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.

1
2
3
4
5
6
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.

1
2
3
4
5
6
7
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.

1
2
3
4
if (hit == 1):
 
    self.bricks.remove(brick)
    self.canvas.delete(brick)

Zasiahnutá tehlička je odstránená.

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.

1
2
3
4
5
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().

bitcoin školení listopad 24

1
2
3
4
5
6
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

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.

Byl pro vás článek přínosný?

Autor článku

Od roku 2006 sa venujem písaniu o počítačových technológiách, predovšetkých programovacím jazykom, grafickému užívateľskému rozhraniu a databázam.