Jak vytvořit Pacmana v Pythonu - 2. část
10.01.2023
Jak vytvořit Pacmana v Pythonu - 2. část

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:

  1. vlna - SCATTER 7 sekund, pak CHASE 20 sekund
  2. vlna - SCATTER 7 sekund, pak CHASE 20 sekund
  3. vlna - SCATTER 5 sekund, pak CHASE 20 sekund
  4. 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