Hra Breakout napísaná v Tkinteri

Ján Bodnár 17. 2. 2016

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.

#!/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

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.

Našli jste v článku chybu?
Měšec.cz: Co s reklamací, když e-shop krachuje?

Co s reklamací, když e-shop krachuje?

120na80.cz: Tipy pro odvodnění organismu

Tipy pro odvodnění organismu

Podnikatel.cz: Selhala pokladna k EET. Kdo zaplatí pokutu?

Selhala pokladna k EET. Kdo zaplatí pokutu?

Měšec.cz: TEST: Vyzkoušeli jsme pražské taxikáře

TEST: Vyzkoušeli jsme pražské taxikáře

120na80.cz: Začátek hlavní sezóny pelyňku je tu

Začátek hlavní sezóny pelyňku je tu

Lupa.cz: Největší torrentový web KickassTorrents padl

Největší torrentový web KickassTorrents padl

Podnikatel.cz: Italské těstoviny nebyly k mání, tak je začal vyrábět

Italské těstoviny nebyly k mání, tak je začal vyrábět

DigiZone.cz: Sázka na e-sporty stanici Prima vychází

Sázka na e-sporty stanici Prima vychází

Měšec.cz: Do ostravské MHD bez jízdenky. Stačí vaše karta

Do ostravské MHD bez jízdenky. Stačí vaše karta

Podnikatel.cz: 3 velké průšvihy obchodních řetězců

3 velké průšvihy obchodních řetězců

Vitalia.cz: Taky je nosíte? Barefoot není pro každého

Taky je nosíte? Barefoot není pro každého

Měšec.cz: Test: Výběry z bankomatů v cizině a kurzy

Test: Výběry z bankomatů v cizině a kurzy

Vitalia.cz: Signál roztroušené sklerózy: brnění končetin

Signál roztroušené sklerózy: brnění končetin

Podnikatel.cz: Tahle praktika stála šmejdy přes milion

Tahle praktika stála šmejdy přes milion

Podnikatel.cz: Polská vejce na českém pultu Albertu

Polská vejce na českém pultu Albertu

Vitalia.cz: Tohle je Břicháč Tom, co zhubnul 27 kg

Tohle je Břicháč Tom, co zhubnul 27 kg

DigiZone.cz: Samsung uvolnil nástroj pro Tizen

Samsung uvolnil nástroj pro Tizen

Vitalia.cz: Sobotní masakr žrádla, chlastu a zábavy

Sobotní masakr žrádla, chlastu a zábavy

DigiZone.cz: Markíza HD a Dajto? U Digi TV asi minulost

Markíza HD a Dajto? U Digi TV asi minulost

Měšec.cz: Platíme NFC mobilem. Konečně to funguje!

Platíme NFC mobilem. Konečně to funguje!