Vítejte ve druhé části seriálu o tom, jak vytvořit hru Pac-Man v Pythonu. V
minulém díle jsme si představili základní principy hry Pac-Man a navrhli si její datovou strukturu. V tomto díle se budeme zabývat implementací několika důležitých prvků hry, jako je například kolize s duchy, počítání score, umělá inteligence duchů, power-upy, koncové stavy hry, animace a textury.
Nejprve si ale připomeneme, jakým způsobem bude hra fungovat. Hráč ovládá Pacmana a snaží se získat co nejvíce bodů za snězení všech cookies na mapě. Pacman je ale pronásledován duchy, kteří se po mapě pohybují podle určitých pravidel. Hra končí, pokud Pacmana chytí jeden z duchů, nebo pokud Pacman sní všechna cookies.
V tomto finálním díle se zaměříme na implementaci kolizí s duchy, které jsou pro hru důležité, jelikož jejich kontakt s Pacmanem může vést k jeho zničení. Dále se podíváme na to, jak můžeme počítat skóre hráče a jak implementovat umělou inteligenci duchů, aby se po mapě pohybovali realističtěji.
Power-upy jsou speciální prvky, které mohou hráči pomoci zvýšit jeho šance na vítězství. V našem případě to mohou být například různé druhy cookies, které mohou Pacmanovi dodat speciální schopnosti nebo ho ochránit před duchy.
Koncové stavy hry jsou důležité, abychom mohli oznámit hráči, zda vyhrál nebo prohrál. V našem případě budeme mít dva možné koncové stavy: vítězství, pokud Pacman sní všechna cookies, a prohru, pokud ho chytí jeden z duchů.
Animace a textury postav duchů a Pacmana zase zajistí, aby hra vypadala živěji a realističtěji. V našem případě budeme animovat pohyb Pacmana a duchů po mapě.
Pokud budete následovat tento článek až do konce, tak výsledná hra bude vypadat takhle:
Kolize s duchy a počítání životů
Počítání životů je spojeno se systémem kolizí hráče s duchy. V minulém díle jsme však pořádně neodlišili duchy od ostatních herních objektů, proto pro ducha založíme samostatnou funkcionalitu pro jeho ukládání, získávání a kontrolu kolize s hráčem. Logika je totožná s kolizí s cookie z prvního dílu. Také připravíme proměnné pro powerupy (schopnost podle originálního jména “kokoro”), životy, skóre a časovače událostí. Pro počítání životů jsem přidal jednoduchou funkci
kill_pacman - pokud se ho dotkne duch, odeberu mu jeden život a umístím ho znovu na startovní pozici. Pokud mu dojdou životy, tak zničím jeho objekt.
#class GameRenderer:
def __init__(self, in_width: int, in_height: int):
self._won = False
self._powerups = []
self._ghosts = []
self._hero: Hero = None
self._lives = 3
self._score = 0
self._score_cookie_pickup = 10
self._score_ghost_eaten = 400
self._score_powerup_pickup = 50
self._kokoro_active = False
self._current_mode = GhostBehaviour.SCATTER
self._mode_switch_event = pygame.USEREVENT + 1 # custom event
self._kokoro_end_event = pygame.USEREVENT + 2
self._pakupaku_event = pygame.USEREVENT + 3
self._modes = [
(7, 20),
(7, 20),
(5, 20),
(5, 999999) # infinite chase
]
self._current_phase = 0
def add_ghost(self, obj: GameObject):
self._game_objects.append(obj)
self._ghosts.append(obj)
def get_ghosts(self):
return self._ghosts
def end_game(self):
if self._hero in self._game_objects:
self._game_objects.remove(self._hero)
self._hero = None
def kill_pacman(self):
self._lives -= 1
self._hero.set_position(32, 32)
self._hero.set_direction(Direction.NONE)
if self._lives == 0: self.end_game()
#class Hero
def tick():
self.handle_cookie_pickup()
self.handle_ghosts()
def handle_ghosts(self):
collision_rect = pygame.Rect(self.x, self.y, self._size, self._size)
ghosts = self._renderer.get_ghosts()
game_objects = self._renderer.get_game_objects()
for ghost in ghosts:
collides = collision_rect.colliderect(ghost.get_shape())
if collides and ghost in game_objects:
if self._renderer.is_kokoro_active():
game_objects.remove(ghost)
self._renderer.add_score(ScoreType.GHOST)
else:
if not self._renderer.get_won():
self._renderer.kill_pacman()
Power-upy a počítání skóre
Skóre je v Pacmanovi udělování za snězení cookie, power-upu nebo ducha. Hodnoty se pro každý ze zmíněných objektů liší. Specifikujeme si je proto do enumu
ScoreType.
class ScoreType(Enum):
COOKIE = 10
POWERUP = 50
GHOST = 400
V Pacmanovi existují speciální cookies, které mu udělí schopnost požírání duchů. V prvním díle jsme schopnost popsali původním japonským slovem “kokoro”, proto ji tak referencuji i v kódu. Vytvoříme novou třídu Powerup - v podstatě totožné s Cookie, jen o něco větší. Do ASCII bludiště použijeme pro power-up písmeno “O” a ve funkci main lehce modifikujeme kód pro zpracování volných míst v bludišti. Také upravíme funkci pro kolizi s cookies handle_cookie_pickup, a zahrneme i novou třídu Powerup. Když Pacman sebere power-up, aktivujeme tuto schopnost na 15 sekund. Po sebrání cookie i powerupu ještě zavoláme funkci add_score s odpovídajícím typem skóre. Přidáme taky set_won pro konec hry, kdy hráč (Pacman) sebral všechny cookies.
class Powerup(GameObject):
def __init__(self, in_surface, x, y):
super().__init__(in_surface, x, y, 8, (255, 255, 255), True)
# class GameRenderer
def add_powerup(self, obj: GameObject):
self._game_objects.append(obj)
self._powerups.append(obj)
def start_kokoro_timeout(self):
pygame.time.set_timer(self._kokoro_end_event, 15000) # 15s
def activate_kokoro(self):
self._kokoro_active = True
self.set_current_mode(GhostBehaviour.SCATTER)
self.start_kokoro_timeout()
def _handle_events(self):
for event in pygame.event.get():
if event.type == self._kokoro_end_event:
self._kokoro_active = False
# class Hero
def handle_cookie_pickup(self):
collision_rect = pygame.Rect(self.x, self.y, self._size, self._size)
cookies = self._renderer.get_cookies()
powerups = self._renderer.get_powerups()
game_objects = self._renderer.get_game_objects()
cookie_to_remove = None
for cookie in cookies:
collides = collision_rect.colliderect(cookie.get_shape())
if collides and cookie in game_objects:
game_objects.remove(cookie)
self._renderer.add_score(ScoreType.COOKIE)
cookie_to_remove = cookie
if cookie_to_remove is not None:
cookies.remove(cookie_to_remove)
if len(self._renderer.get_cookies()) == 0:
self._renderer.set_won()
for powerup in powerups:
collides = collision_rect.colliderect(powerup.get_shape())
if collides and powerup in game_objects:
if not self._renderer.is_kokoro_active():
game_objects.remove(powerup)
self._renderer.add_score(ScoreType.POWERUP)
self._renderer.activate_kokoro()
# main
for powerup_space in pacman_game.powerup_spaces:
translated = translate_maze_to_screen(powerup_space)
powerup = Powerup(game_renderer, translated[0] + unified_size / 2, translated[1] + unified_size / 2)
game_renderer.add_powerup(powerup)
Umělá inteligence duchů
V originální hře se duchové chovají podle následujícího vzorce:
- vlna - SCATTER 7 sekund, pak CHASE 20 sekund
- vlna - SCATTER 7 sekund, pak CHASE 20 sekund
- vlna - SCATTER 5 sekund, pak CHASE 20 sekund
- vlna - SCATTER 5 sekund, pak permanentně CHASE
SCATTER je režim, ve kterém se duchové chovají náhodně a vybírají si náhodná místa v bludišti (ve skutečnosti mají ještě další specifičtější vzorce pro pohyb okolo překážek, ale do tyto složitější vzorce zde řešit nebudeme). CHASE je režim, ve kterém si duchové dají za cíl pozici hráče - využijeme pro to kód z prvního dílu a jen upravíme cílovou lokaci z náhodné na danou pozici hráče
get_hero_position.
Tyto časovače máme v kódu od začátku článku, teď jen musíme přidat přepínání mezi režimy CHASE a SCATTER a kód pro zjištění trasy na pozici hráče.
# class GameRenderer
def tick(self, in_fps: int):
self.handle_mode_switch()
def handle_mode_switch(self):
current_phase_timings = self._modes[self._current_phase]
print(f"Current phase: {str(self._current_phase)}, current_phase_timings: {str(current_phase_timings)}")
scatter_timing = current_phase_timings[0]
chase_timing = current_phase_timings[1]
if self._current_mode == GhostBehaviour.CHASE:
self._current_phase += 1
self.set_current_mode(GhostBehaviour.SCATTER)
else:
self.set_current_mode(GhostBehaviour.CHASE)
used_timing = scatter_timing if self._current_mode == GhostBehaviour.SCATTER else chase_timing
pygame.time.set_timer(self._mode_switch_event, used_timing * 1000)
def get_hero_position(self):
return self._hero.get_position() if self._hero != None else (0, 0)
def set_current_mode(self, in_mode: GhostBehaviour):
self._current_mode = in_mode
def get_current_mode(self):
return self._current_mode
def _handle_events(self):
for event in pygame.event.get():
if event.type == self._mode_switch_event:
self.handle_mode_switch()
# class Ghost
def request_path_to_player(self, in_ghost):
player_position = translate_screen_to_maze(in_ghost._renderer.get_hero_position())
current_maze_coord = translate_screen_to_maze(in_ghost.get_position())
path = self.game_controller.p.get_path(current_maze_coord[1], current_maze_coord[0], player_position[1],player_position[0])
new_path = [translate_maze_to_screen(item) for item in path]
in_ghost.set_new_path(new_path)
def calculate_direction_to_next_target(self) -> Direction:
if self.next_target is None:
if self._renderer.get_current_mode() == GhostBehaviour.CHASE and not self._renderer.is_kokoro_active():
self.request_path_to_player(self)
else:
self.game_controller.request_new_random_path(self)
return Direction.NONE
diff_x = self.next_target[0] - self.x
diff_y = self.next_target[1] - self.y
if diff_x == 0:
return Direction.DOWN if diff_y > 0 else Direction.UP
if diff_y == 0:
return Direction.LEFT if diff_x < 0 else Direction.RIGHT
if self._renderer.get_current_mode() == GhostBehaviour.CHASE and not self._renderer.is_kokoro_active():
self.request_path_to_player(self)
else:
self.game_controller.request_new_random_path(self)
return Direction.NONE
Animace a textury
Duchy a Pacmana upravíme tak, aby se místo dosavadních základních geometrických tvarů zobrazovali jako textury, respektive “sprites”. Do
MovableObject třídy přidáme proměnnou image a upravíme funkci draw následovně:
# class MovableObject
def __init__(self, in_surface, x, y, in_size: int, in_color=(255, 0, 0), is_circle: bool = False):
self.image = pygame.image.load('images/ghost.png')
def draw(self):
self.image = pygame.transform.scale(self.image, (32, 32))
self._surface.blit(self.image, self.get_shape())
Referencované obrázky jsou dostupné na mém
githubu.
Každý duch bude mít jiný obrázek (specifikujeme ve funkci main) a když hráč aktivuje powerup, přepnou se duchové na texturu
fright.
# class Ghost
def __init__(self, in_surface, x, y, in_size: int, in_game_controller, sprite_path="images/ghost_fright.png"):
super().__init__(in_surface, x, y, in_size)
self.game_controller = in_game_controller
self.sprite_normal = pygame.image.load(sprite_path)
self.sprite_fright = pygame.image.load("images/ghost_fright.png")
def draw(self):
self.image = self.sprite_fright if self._renderer.is_kokoro_active() else self.sprite_normal
super(Ghost, self).draw()
Animace otevírání a zavírání pusy Pacmana uděláme přes časovanou událost _pakupaku_event, která se volá každých 200ms (implementujeme v kapitole níže). Ta zneguje binární hodnotu mouth_open a podle toho se vykreslí jeden nebo druhý sprite Pacmana.
# class Hero
def __init__(self, in_surface, x, y, in_size: int):
super().__init__(in_surface, x, y, in_size, (255, 255, 0), False)
self.last_non_colliding_position = (0, 0)
self.open = pygame.image.load("images/paku.png")
self.closed = pygame.image.load("images/man.png")
self.image = self.open
self.mouth_open = True
def draw(self):
half_size = self._size / 2
self.image = self.open if self.mouth_open else self.closed
self.image = pygame.transform.rotate(self.image, self.current_direction.value)
super(Hero, self).draw()
# class GameRenderer
def _handle_events(self):
if event.type == self._pakupaku_event:
if self._hero is None: break
self._hero.mouth_open = not self._hero.mouth_open
Koncové stavy hry a textové UI
Zobrazení stavu hry je na konec jednoduché. Funkcí
display_text vykreslíme text o dané velikosti na danou pozici. V hlavní tick funkci zobrazíme na základě výsledku
get_won text pro výhru “YOU WON”. Pokud neexistuje objekt hráče - což mohlo nastat jen při “smrti”, když objekt zničíme - tak zobrazíme text “YOU DIED”.
# class GameRenderer
def display_text(self, text, in_position=(32, 0), in_size=30):
font = pygame.font.SysFont('Arial', in_size)
text_surface = font.render(text, False, (255, 255, 255))
self._screen.blit(text_surface, in_position)
def set_won(self):
self._won = True
def get_won(self):
return self._won
def tick(self, in_fps: int):
black = (0, 0, 0)
self.handle_mode_switch()
pygame.time.set_timer(self._pakupaku_event, 200) # open close mouth
while not self._done:
for game_object in self._game_objects:
game_object.tick()
game_object.draw()
self.display_text(
f"[Score: {self._score}] [Lives: {self._lives}]")
if self._hero is None: self.display_text("YOU DIED", (self._width / 2 - 256, self._height / 2 - 256), 100)
if self.get_won(): self.display_text("YOU WON", (self._width / 2 - 256, self._height / 2 - 256), 100)
pygame.display.flip()
self._clock.tick(in_fps)
self._screen.fill(black)
self._handle_events()
Závěrem
Ve finální části seriálu o vývoji hry Pac-Man v Pythonu jsme se zaměřili na implementaci několika důležitých prvků hry.
Nejprve jsme si ukázali, jak můžeme implementovat kolize s duchy a počítat skóre hráče. Poté jsme se zaměřili na umělou inteligenci duchů, která je důležitá pro realistický pohyb těchto objektů po mapě. Dále jsme se věnovali implementaci power-upů, které mohou hráčům pomoci zvýšit šance na vítězství.
V závěru jsme se podívali na koncové stavy hry a implementovali jsme animace a textury objektů hry. Díky těmto prvkům se nám podařilo vytvořit kompletní prototyp legendární hry Pac-Man. V případě nejasností je můj kód k dispozici na
githubu.
Doufám, že vám tento seriál pomohl pochopit, jak vytvořit hru Pac-Man v Pythonu, a že jste se při implementaci dozvěděli něco nového. Pokud máte zájem o další informace o vývoji her v Pythonu, neváhejte se podívat na další
články na našem webu.
Jan Jileček