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.