Lekce 5 - Pygame - Pong - Logika stavů hry a dokončení
V minulé lekci, Pygame - Pong - Prostředí a herní objekty, jsme si do hry Pong přidali hráče, míč a nastavili herní cyklus.
V dnešní lekci kurzu Tvorba her v Pythonu, přidáme do naší hry Pong logiku jednotlivých herních stavů. Celou hru bychom tak měli úspěšně dokončit 🙂
Stavy hry
Nyní nastává ta nejtěžší část, musíme zapřemýšlet do budoucnosti a rozhodnout se, jaké stavy naše hra bude mít. Můžeme dojít k následujícím třem stavům:
- výběr, jestli má být druhý hráč ovládán hráčem nebo počítačem
- začátek kola, kdy je hra pozastavena, aby měli hráči možnost se nachystat
- samotné kolo, ve kterém se oba hráči pohybují a létá mezi nimi míč
Jak tyto stavy reprezentovat? Nejsnazší asi bude každý stav napsat jako metodu, přičemž budeme mít proměnnou, ve které bude uložen odkaz na aktuální stav, který se bude volat. Nazvěme si tedy stavy například následovně:
logic_game_start()
logic_round_start()
logic_game_body()
Dále si vytvoříme nový atribut logic
(jako logika
aktuálního stavu), který pak na začátku funkce main()
nastavíme na první stav:
from typing import Optional, Callable # ... def __init__(self): logic: Optional[Callable] = None # ... def main(self): self.logic = self.logic_game_start while self.running: # UDALOSTI # ... self.logic() # KRESLENI # ... # ... def logic_game_start(self): pass def logic_round_start(self): pass def logic_game_body(self): pass
Když se ještě zamyslíme, můžeme dojít k závěru, že některé
objekty (konkretně oba hráče, míč a skóre) budeme chtít mít vykreslené
úplně pokaždé, takže je nemusíme členit do jednotlivých logik a můžeme
je přidat přímo do cyklu v main()
. Všechny naše objekty budou
bílé a skóre bude nahoře uprostřed:
def main(self): # ... while self.running: # ... # KRESLENI pygame.draw.rect(screen, (255, 255, 255), self.player1) pygame.draw.rect(screen, (255, 255, 255), self.player2) pygame.draw.rect(screen, (255, 255, 255), self.ball) text = font_score.render(f"{self.player1_score} : {self.player2_score}", True, (255, 255, 255)) self.screen.blit(text, ((self.screen.get_width() - self.text.get_width()) / 2, 0))
Tím bychom dokončili náš hlavní herní cyklus, takže nám zbývá už pouze vytvořit jednotlivé stavy a máme hotovo!
Začátek hry
Začněme s prvním stavem, tedy se začátkem hry. V tomto stavu po
uživateli chceme, aby se rozhodl, jestli chce hrát proti druhému člověku,
nebo počítači. Dejme tomu, že ho vyzveme, ať stiskne H
(human), pokud bude chtít hrát proti člověku, nebo C
(computer), pokud bude chtít hrát proti počítači. Naši otázku
můžeme umístit doprostřed obrazovky a podle odpovědi uživatele nastavíme
proměnnou player2_ai
:
def logic_game_start(self): # vykresleni otazky na obrazovku text = self.font_info.render("Press C to play with computer or H to play against human", True, (255, 255, 255)) self.screen.blit(text, ((self.screen.get_width() - text.get_width()) / 2, (self.screen.get_height() * 0.75 - text.get_height()) / 2)) # kontrola odpovedi pressed = pygame.key.get_pressed() if pressed[pygame.K_h] or pressed[pygame.K_c]: self.player2_ai = bool(pressed[pygame.K_c]) # uzivatel si vybral, presun do dalsiho stavu self.logic = self.logic_round_start
Celkem přímočaré, že?
Začátek kola
Před začátkem každého kola je dobrý nápad umístit hráče a míč do původní pozice a vyresetovat míči směr a rychlost. Pak nám již stačí pouze hráče vyzvat, aby stiskl Mezerník, čímž signalizuje, že jsou všichni připraveni a hra může začít:
def logic_round_start(self): # vyresetuje vse na hodnoty a pozice pred zacatkem kola self.player1 = self.player1_orig.copy() self.player2 = self.player2_orig.copy() self.ball = self.ball_orig.copy() self.ball_speed = self.ball_speed_orig self.round_tick = 0 # vybere novy prvotni smer mice # (smer musi byt dostatecne ostry, jinak se mic odrazi prilis dlouho od zdi) self.set_ball_direction(choice( (randrange(-75, -15), randrange(15, 75), randrange(195, 255)) )) pressed = pygame.key.get_pressed() if pressed[pygame.K_SPACE]: # vsichni pripraveni, muzeme spustit kolo self.logic = self.logic_game_body # vykresli hlasku pro uzivatele text = self.font_info.render("Press SPACE to start", True, (255, 255, 255)) self.screen.blit(text, ((self.screen.get_width() - text.get_width()) / 2, (self.screen.get_height() * 0.75 - text.get_height()) / 2))
Samotné kolo
Jak jste správně vytušili, toto je nejtěžší část celého programu. Právě v ní se totiž odehrává drtivá většina všech aktivit:
def logic_game_body(self):
Pohyby hráčů
Začneme tím, že se vypořádáme s pohyby hráčů. Řekněme, že hráč číslo 1 bude pro pohyb nahoru používat W a pro pohyb dolů S. Pohybovat se bude samozřejmě moci pouze v případě, že nenaráží do vrchní nebo spodní části obrazovky:
pressed = pygame.key.get_pressed() if pressed[pygame.K_s] and not self.player1.colliderect(self.bottom): self.player1.move_ip(0, self.player_speed) elif pressed[pygame.K_w] and not self.player1.colliderect(self.top): self.player1.move_ip(0, -self.player_speed)
Hráč 2, pokud není ovládán počítačem, bude používat pro pohyb nahoru a dolů šipky:
if not self.player2_ai: if pressed[pygame.K_DOWN] and not self.player2.colliderect(self.bottom): self.player2.move_ip(0, self.player_speed) elif pressed[pygame.K_UP] and not self.player2.colliderect(self.top): self.player2.move_ip(0, -self.player_speed)
AI počítače
Pro případ, že je druhý hráč ovládán počítačem, vytvoříme jednoduchou dvouřádkovou AI. Ta se bude pokoušet být na stejné výškové hladině, jako je míč. Pohyb vypočítáme jednoduše:
Pokud je míč níže než hráč, musí jít hráč níže (tj. posun
hráče se musí pohybovat v kladných číslech). Pokud je ale míč výše,
musí se hráč posunout o záporné číslo. Hráč se v jednom kroku herního
cyklu nikdy nemůže posunout dále, než je jeho rychlost. Zároveň, jelikož
nechce míč přeletět, posune se maximálně tolik, aby se přesně zarovnal s
míčem. Vzdálenost pohybu tak vypočítáme pomocí
min(player_speed, abs(player2.centery - ball.centery))
.
Abychom určili, jestli se má hráč pohybovat v kladných, nebo záporných
číslech, potřebujeme funkci, která nám zjistí znaménko rozdílů
souřadnic míče a hráče. Za tímto účelem si vytvoříme funkci
sign
:
# jelikož sign není omezena na použití s naší hrou, vytvoříme ji raději jako funkci, ne metodu třídy def sign(num: int) -> int: return 1 if num > 0 else -1 if num < 0 else 0
Nyní víme, o kolik se má hráč ovládaný pomocí AI pohnout. Nesmíme ale zapomenout, že ani on nesmí jít za hranice obrazovky. Ve výsledku kód naší AI může tedy vypadat nějak takto:
else: move_y = sign(self.ball.centery - self.player2.centery) * min(self.player_speed, abs(self.player2.centery - self.ball.centery)) if (move_y > 0 and not self.player2.colliderect(self.bottom)) or (move_y < 0 and not self.player2.colliderect(self.top)): self.player2.move_ip(0, move_y)
Pohyb míče
Pohyby hráčů již máme tedy vyřešené. Teď si ještě vyřešíme
pohyb míče, který přímo vyplývá z jednotkové
kružnice. Použijeme funkce sin()
a cos()
pro
získání nové pozice míče podle směru, kterým zrovna letí a jeho
rychlosti:
ball_direction_radians = radians(self.ball_direction) self.ball.move_ip(self.ball_speed * cos(ball_direction_radians), ball_speed * sin(ball_direction_radians))
Odrážení
Když si nyní spustíme naši hru, tak se nám nebude příliš dařit, protože, i přes všechnu naši snahu, míč vždy opustí hrací plochu. Proto musíme ještě definovat, co má míč dělat při nárazu do hráčů nebo některého okraje.
Odražení od horní a dolní hrany
Pro vrchní nebo spodní hranu je řešení velice snadné - stačí nám obrátit směr míče:
if self.ball.collidelist([self.top, self.bottom]) > -1: self.set_ball_direction(-self.ball_direction)
Pokud ale míč narazí z levé nebo pravé strany do hráče, musíme již jeho úhel odrazu vypočítat manuálně:
elif self.ball.collidelist([self.player1, self.player2]) > -1: self.set_ball_direction(180 - self.ball_direction)
Odražení od levé a pravé hrany
Poslední situace, kterou nám ještě zbývá vyřešit, je, když míč narazí do pravé nebo levé strany. V takovém případě to znamená, že jeden z hráčů nebyl schopný míč dostatečně rychle odrazit. Druhému hráči tedy přičteme bod a vrátíme se na stav začátku kola:
if self.ball.colliderect(self.left): self.player2_score += 1 self.logic = self.logic_round_start elif self.ball.colliderect(self.right): self.player1_score += 1 self.logic = self.logic_round_start
Zrychlování
A posledním vylepšením, které jsme si slíbili, ale ještě jsme jej
nezakomponovali, je samozrychlující se míč. Řekněme, že každé 2 sekundy
kola se míč sám zrychlí o 10% až do své maximální rychlosti (lidé mají
své limity, kdy míč ještě dokáží sledovat )
Ke zjištění toho, zda-li uplynuly právě 2 sekundy, nám poslouží náš
časovač round_tick
, jehož hodnota se každou iteraci v kole
zvýší o 1
. A každé 2 sekundy to jsou právě, když
round_tick
je násobkem dvojnásobku FPS
:
if self.round_tick > 0 and self.round_tick % (2 * self.FPS) == 0: self.ball_speed = min(1.1 * self.ball_speed, self.ball_speed_max) self.round_tick += 1
Máme hotovo!
A je to! Naše první hra je na světě! Zvládli jste to!

Teď vám už nic nebrání si ji rozšířit, jak jen uznáte za vhodné. Nebo se pustit do něčeho vlastního, představivosti se meze nekladou 🙂
V další lekci { NEXT}, se podíváme na nový velký projekt - Skákačku v Pygame. Vytvoříme složkovou strukturu a herní smyčku.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 273x (8.03 kB)
Aplikace je včetně zdrojových kódů v jazyce Python