Hlavní navigace

Hra Snake naprogramovaná v Pythone s pomocou Tkinter

Ján Bodnár

Na Roote práve prebieha seriál venovaný základom knižnice Tkinter. V tomto článku si ukážeme, ako naprogramovať jednoduchú hru Snake v Pythone s pomocou knižnice Tkinter.

Snake je staršia klasická videohra, ktorá sa prvý krát objavila koncom sedemdesiatich rokov. Neskôr bola preportovaná na PC. V tejto hre ovláda hráč hada, ktorý pojedá jablká. Had sa musí vyhnúť okrajom obrazovky a svojmu vlastnému telu, ktoré jedením jabĺk neustále narastá. Cieľom hry je zjesť čo najviac jabĺk.

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. Na Roote vychádza seriál venovaný základom Tkinter. Viac si môžete naštudovať na oficiálnej stránke dokumentácie k Tk, tutoriály na ZetCode, alebo na tkinter.programujte.com

Pillow

Pillow je knižnica na prácu s obrázkami v jazyku Python. Ide o „priateľský port“ pôvodnej knižnice PIL (Python Imaging Library), ktorá sa už aktívne neudržiavala.

$ sudo pip3 install pillow

Knižnica sa inštaluje hore uvedeným príkazom.

Vývoj hry

Na vývoj hry využijeme komponentu Canvas (plátno), ktorá je vysokoúrovňovou komponentou na prácu s grafikou v knižnici Tkinter. 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.

Ďalej využijeme časovač, pomocou ktorého vytvoríme herný cyklus. Vo väčšine dynamických hier máme herný cyklus, v ktorom sa zisťuje input od hráča, kontrolujú sa kolízie, vykonávajú sa nevyhnutné výpočty a preklesľuje sa obrazovka hry.

V hre sú časti hada vytvorené pomocou malých obrázkov vo veľkosti 10×10 px. Had sa ovláda pomocou kurzorových kláves. Na začiatku hry je had tvorený troma článkami. Keď sa hra ukončí, zobrazí sa hláška o ukončení hry spolu s dosiahnutým skóre. Skóre sa zvyšuje o jednotku každým zjedeným jablkom.

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
ZetCode Tkinter tutorial

This is a simple Snake game
clone.

Author: Jan Bodnar
Website: zetcode.com
Last edited: July 2017
"""

import sys
import random
from PIL import Image, ImageTk
from tkinter import Tk, Frame, Canvas, ALL, NW

class Cons:

    BOARD_WIDTH = 300
    BOARD_HEIGHT = 300
    DELAY = 100
    DOT_SIZE = 10
    MAX_RAND_POS = 27

class Board(Canvas):

    def __init__(self):
        super().__init__(width=Cons.BOARD_WIDTH, height=Cons.BOARD_HEIGHT,
            background="black", highlightthickness=0)

        self.initGame()
        self.pack()


    def initGame(self):
        '''initializes game'''

        self.inGame = True
        self.dots = 3
        self.score = 0

        # variables used to move snake object
        self.moveX = Cons.DOT_SIZE
        self.moveY = 0

        # starting apple coordinates
        self.appleX = 100
        self.appleY = 190

        self.loadImages()

        self.createObjects()
        self.locateApple()
        self.bind_all("<Key>", self.onKeyPressed)
        self.after(Cons.DELAY, self.onTimer)


    def loadImages(self):
        '''loads images from the disk'''

        try:
            self.idot = Image.open("dot.png")
            self.dot = ImageTk.PhotoImage(self.idot)
            self.ihead = Image.open("head.png")
            self.head = ImageTk.PhotoImage(self.ihead)
            self.iapple = Image.open("apple.png")
            self.apple = ImageTk.PhotoImage(self.iapple)

        except IOError as e:

            print(e)
            sys.exit(1)


    def createObjects(self):
        '''creates objects on Canvas'''

        self.create_text(30, 10, text="Score: {0}".format(self.score),
                         tag="score", fill="white")
        self.create_image(self.appleX, self.appleY, image=self.apple,
            anchor=NW, tag="apple")
        self.create_image(50, 50, image=self.head, anchor=NW,  tag="head")
        self.create_image(30, 50, image=self.dot, anchor=NW, tag="dot")
        self.create_image(40, 50, image=self.dot, anchor=NW, tag="dot")


    def checkAppleCollision(self):
        '''checks if the head of snake collides with apple'''

        apple = self.find_withtag("apple")
        head = self.find_withtag("head")

        x1, y1, x2, y2 = self.bbox(head)
        overlap = self.find_overlapping(x1, y1, x2, y2)

        for ovr in overlap:

            if apple[0] == ovr:

                self.score += 1
                x, y = self.coords(apple)
                self.create_image(x, y, image=self.dot, anchor=NW, tag="dot")
                self.locateApple()


    def moveSnake(self):
        '''moves the Snake object'''

        dots = self.find_withtag("dot")
        head = self.find_withtag("head")

        items = dots + head

        z = 0
        while z < len(items)-1:

            c1 = self.coords(items[z])
            c2 = self.coords(items[z+1])
            self.move(items[z], c2[0]-c1[0], c2[1]-c1[1])
            z += 1

        self.move(head, self.moveX, self.moveY)


    def checkCollisions(self):
        '''checks for collisions'''

        dots = self.find_withtag("dot")
        head = self.find_withtag("head")

        x1, y1, x2, y2 = self.bbox(head)
        overlap = self.find_overlapping(x1, y1, x2, y2)

        for dot in dots:
            for over in overlap:
                if over == dot:
                  self.inGame = False

        if x1 < 0:
            self.inGame = False

        if x1 > Cons.BOARD_WIDTH - Cons.DOT_SIZE:
            self.inGame = False

        if y1 < 0:
            self.inGame = False

        if y1 > Cons.BOARD_HEIGHT - Cons.DOT_SIZE:
            self.inGame = False


    def locateApple(self):
        '''places the apple object on Canvas'''

        apple = self.find_withtag("apple")
        self.delete(apple[0])

        r = random.randint(0, Cons.MAX_RAND_POS)
        self.appleX = r * Cons.DOT_SIZE
        r = random.randint(0, Cons.MAX_RAND_POS)
        self.appleY = r * Cons.DOT_SIZE

        self.create_image(self.appleX, self.appleY, anchor=NW,
            image=self.apple, tag="apple")


    def onKeyPressed(self, e):
        '''controls direction variables with cursor keys'''

        key = e.keysym

        LEFT_CURSOR_KEY = "Left"
        if key == LEFT_CURSOR_KEY and self.moveX <= 0:

            self.moveX = -Cons.DOT_SIZE
            self.moveY = 0

        RIGHT_CURSOR_KEY = "Right"
        if key == RIGHT_CURSOR_KEY and self.moveX >= 0:

            self.moveX = Cons.DOT_SIZE
            self.moveY = 0

        RIGHT_CURSOR_KEY = "Up"
        if key == RIGHT_CURSOR_KEY and self.moveY <= 0:

            self.moveX = 0
            self.moveY = -Cons.DOT_SIZE

        DOWN_CURSOR_KEY = "Down"
        if key == DOWN_CURSOR_KEY and self.moveY >= 0:

            self.moveX = 0
            self.moveY = Cons.DOT_SIZE


    def onTimer(self):
        '''creates a game cycle each timer event '''

        self.drawScore()
        self.checkCollisions()

        if self.inGame:
            self.checkAppleCollision()
            self.moveSnake()
            self.after(Cons.DELAY, self.onTimer)
        else:
            self.gameOver()


    def drawScore(self):
        '''draws score'''

        score = self.find_withtag("score")
        self.itemconfigure(score, text="Score: {0}".format(self.score))


    def gameOver(self):
        '''deletes all objects and draws game over message'''

        self.delete(ALL)
        self.create_text(self.winfo_width() /2, self.winfo_height()/2,
            text="Game Over with score {0}".format(self.score), fill="white")


class Snake(Frame):

    def __init__(self):
        super().__init__()

        self.master.title('Snake')
        self.board = Board()
        self.pack()


def main():

    root = Tk()
    nib = Snake()
    root.mainloop()


if __name__ == '__main__':
    main()

Na začiaku si zadefinujeme použité konštanty.

class Cons:

    BOARD_WIDTH = 300
    BOARD_HEIGHT = 300
    DELAY = 100
    DOT_SIZE = 10
    MAX_RAND_POS = 27

Konštanty BOARD_WIDTH a BOARD_HEIGHT určujú veľkosť hracej plochy. Konštanta DELAY určuje rýchlosť hry. DOT_SIZE stanovuje veľkosť jablka a častí hada. MAX_RAND_POS sa využíva na výpočet náhodnej polohy jablka.

Metóda initGame() inicializuje premenné, nahrá obrázky a štartuje timer.

self.createObjects()
self.locateApple()

Metóda createObjects() vytvára objekty na plátne a metóda locateApple() umiestni náhodným spôsobom jablko na plátno.

self.bind_all("<Key>", self.onKeyPressed)

Hra sa ovláda kurzorovými klávesami. Pomocou metódy bind_all() naviažeme udalosti klávesnice na metódu onKeyPressed().

try:
    self.idot = Image.open("dot.png")
    self.dot = ImageTk.PhotoImage(self.idot)
    self.ihead = Image.open("head.png")
    self.head = ImageTk.PhotoImage(self.ihead)
    self.iapple = Image.open("apple.png")
    self.apple = ImageTk.PhotoImage(self.iapple)

except IOError as e:

    print(e)
    sys.exit(1)

Tento kód nahrá potrebné obrázky. V hre ich máme tri druhy: hlavu, telo hada a jablko. Na prácu s obrázkami využívame knižnicu Pillow.

def createObjects(self):
    '''creates objects on Canvas'''

    self.create_text(30, 10, text="Score: {0}".format(self.score),
                        tag="score", fill="white")
    self.create_image(self.appleX, self.appleY, image=self.apple,
        anchor=NW, tag="apple")
    self.create_image(50, 50, image=self.head, anchor=NW,  tag="head")
    self.create_image(30, 50, image=self.dot, anchor=NW, tag="dot")
    self.create_image(40, 50, image=self.dot, anchor=NW, tag="dot")

V metóde createObjects() vytvoríme objekty hry. Objektom pridelíme počiatočné súradnice. Motóda create_text() vytvorí text a metóda create_image() obrázok. Aby hra fungovala normálne, musíme ukotviť objekty do ľavého horného rohu bodu na súradnicovej osi, ktorý zadáme v prvých dvoch parametroch príslušných metód. To dosiahneme parametrom anchor. Pomocou parametra tag identifikujeme objetky na plátne. Tak napríklad na jablko sa budeme odvolávať pomocou reťazca „apple“. Jeden tag môže byť použitý aj pre vlaceré elementy plátna.

Pomocou metódy checkAppleCollision() zisťujeme, či had narazil na jablko. Ak áno, zvýšime skóre, zväčšíme telo hada, zmažeme objekt jablka a vytvoríme nové jablko pomocou metódy locateApple().

apple = self.find_withtag("apple")
head = self.find_withtag("head")

Metóda find_withtag() nám nájde objekt pomocou jeho tagu. Hľadáme dva objekty: jablko a hlavu hada.

x1, y1, x2, y2 = self.bbox(head)
overlap = self.find_overlapping(x1, y1, x2, y2)

Metóda bbox() nám vráti súradnice ľavého horného a dolného pravého bodu nášho objektu. Metóda find_overlapping() nájde objekty, ktoré s danými súradnicami kolidujú.

for ovr in overlap:

    if apple[0] == ovr:
        x, y = self.coords(apple)
        self.create_image(x, y, image=self.dot, anchor=NW, tag="dot")
        self.locateApple()

Ak jablko koliduje s hlavou hada, vytvorí sa nový článok hada. Metódou coords() zisťujeme súradnice objektu. Zavolá sa metóda locateApple(), ktorá zmaže predchádzajúce jablko a vytvorí nové, ktoré je náhodným spôsobom umiestnené na plátne.

V metóde moveSnake() máme základný algoritmus hry. Pochopenie tohto algoritmu nám uľahčí, ak sa zadívame na pohyb hada. Hráč kontroluje hlavu hada, ktorého smer pohybu sa ovláda kurzorovými klávesami. Ak sa hlava hada posunie o jedno miesto, všetky ostatné články sa posunú na miesto prechádzajúceho článku. Teda druhý článok sa posunie na miesto prvého, tretí na štvrtého a tak ďalej.

z = 0
while z < len(items)-1:
    c1 = self.coords(items[z])
    c2 = self.coords(items[z+1])
    self.move(items[z], c2[0]-c1[0], c2[1]-c1[1])
    z += 1

Tento while cyklus posúva články hada vyššie uvedeným spôsobom. Metóda move() posúva objekt na plátne.

self.move(head, self.moveX, self.moveY)

Tento riadok kódu posúva hlavu hada. Premenné self.moveX a self.moveY sa nastavia v momente, keď dochádza k stlačeniu kurzorových kláves.

V metóde checkCollisions() sa zisťuje, či had prekročí hranicu hracej plochy alebo narazí do vlastného tela.

x1, y1, x2, y2 = self.bbox(head)
overlap = self.find_overlapping(x1, y1, x2, y2)

for dot in dots:
    for over in overlap:
        if over == dot:
          self.inGame = False

Hra sa ukončí, ak had narazí na niektorý svoj článok.

if y1 > Cons.BOARD_HEIGHT - Cons.DOT_SIZE:
    self.inGame = False

Hra sa ukončí, ak had prekročí dolnú hranicu hracej plochy.

Metóda locateApple() zmaže existujúce jablko, vytvorí nové, a zobrazí ho na náhodnom mieste na ploche.

apple = self.find_withtag("apple")0
self.delete(apple[0])

Tieto dva riadky nájdu objekt jablka a vymažú ho.

r = random.randint(0, Cons.MAX_RAND_POS)

Na generovane náhodných čísiel použijeme modul random. Získame náhodné číslo v rozmedzí 0 až MAX_RAND_POS  – 1.Po

self.appleX = r * Cons.DOT_SIZE
...
self.appleY = r * Cons.DOT_SIZE

Tu vypočítame súradnice nového objektu jablka.

V metóde onKeyPressed() reagujeme na stlačené klávesy počas hry.

LEFT_CURSOR_KEY = "Left"
if key == LEFT_CURSOR_KEY and self.moveX <= 0:

    self.moveX = -Cons.DOT_SIZE
    self.moveY = 0

Ak bola stlačená ľavá kurzorová klávesa, nastavia sa príslušným spôsobom premenné self.moveX a self.moveY. Tieto premenné využívame v metóde moveSnake(). Všimnime si tiež podmienku, ktorá nám zabráni protipohybu; teda ak sa had hýbe doprava, nemôžeme hneď prejsť doľava.

def onTimer(self):
    '''creates a game cycle each timer event '''

    self.drawScore()
    self.checkCollisions()

    if self.inGame:
        self.checkAppleCollision()
        self.moveSnake()
        self.after(Cons.DELAY, self.onTimer)
    else:
        self.gameOver()

Každých DELAY milisekúnd sa nám zavolá metóda onTimer(). V nej sa volajú metódy, ktoré tvoria herný cyklus: zobrazí sa skóre, kontrolujú sa kolízie a animuje sa had. Časovač sa vytvorí metódou after(). Keďže sa jedná o časovač na jeden výstrel, je potrebné ho v každom cykle znova aktivovať.

def drawScore(self):
    '''draws score'''

    score = self.find_withtag("score")
    self.itemconfigure(score, text="Score: {0}".format(self.score))

Metóda drawScore() nám zobrazí skóre.

def gameOver(self):
    '''deletes all objects and draws game over message'''

    self.delete(ALL)
    self.create_text(self.winfo_width() /2, self.winfo_height()/2,
        text="Game Over with score {0}".format(self.score), fill="white")

Keď sa hra ukončí, vymažú sa všetky objekty na plátne. Potom sa zobrazí hláška o ukončení hry spolu s dosiahnutým skóre.

Zdroje

V tomto článku sme si ukázali, ako vytvoriť jednoduchú hru Snake v Tkinteri. Článok je prekladom autorovho anglického originálu. Zdrojový kód spolu s obrázkami nájdete na Github repozitári. Na Roote som zverejnil v minulosti článok o hre Breakout.

Našli jste v článku chybu?