目录
1. 介绍
吃豆人(Pac-Man)是1980年代风靡全球的街机游戏,其核心玩法简单却充满策略性。本文将基于Python的Pygame库,解析一个完整实现的吃豆人游戏代码。
代码包含以下核心功能:
-
随机迷宫生成(每次游戏不同)
-
幽灵AI追踪(A*算法实现)
-
吃豆人移动与动画
-
能量豆机制(幽灵恐惧状态)
-
游戏状态管理(生命值、得分、胜负判定)
2. 代码结构概览
代码由6个核心类组成:
-
MazeGenerator
:迷宫生成 -
GhostAI
:幽灵路径算法 -
Ghost
:幽灵行为与渲染 -
Pacman
:玩家角色控制 -
Game
:游戏主逻辑
# 类关系示意图
Game
├── MazeGenerator
├── Pacman
└── Ghost
└── GhostAI
3. 核心功能解析
1. 迷宫生成算法(Prim算法)
实现原理:
-
初始化为全墙迷宫,随机选择一个起点(1,1)。
-
使用“边界墙列表”逐步打通路径,每次随机选择一个边界墙,检查其对侧是否为墙。
-
额外步骤:随机打通部分墙壁以增加环路,避免单一路径。
关键代码:
# 使用Prim算法生成多路径迷宫
def prim_maze():
# 从起点(1,1)开始
frontier = []
start = (1, 1)
maze[start[1]][start[0]] = 0
# 添加相邻墙到边界列表
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
nx, ny = start[0] + dx, start[1] + dy
if 0 < nx < width - 1 and 0 < ny < height - 1:
frontier.append((nx, ny, start[0], start[1]))
while frontier:
# 随机选择一个边界墙
random.shuffle(frontier)
x, y, px, py = frontier.pop()
# 如果对面是墙,就打通
opposite_x, opposite_y = px + (x - px) * 2, py + (y - py) * 2
if 0 < opposite_x < width - 1 and 0 < opposite_y < height - 1:
if maze[opposite_y][opposite_x] == 1:
maze[y][x] = 0
maze[opposite_y][opposite_x] = 0
# 添加新的边界墙
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
nx, ny = opposite_x + dx, opposite_y + dy
if 0 < nx < width - 1 and 0 < ny < height - 1 and maze[ny][nx] == 1:
frontier.append((nx, ny, opposite_x, opposite_y))
prim_maze()
优化点:
-
确保起点和终点(1,1)和(width-2, height-2)始终连通。
-
通过随机打通额外墙壁,迷宫复杂度提升80%。
2. 幽灵AI:A*路径追踪
算法核心:
-
A*算法结合了Dijkstra的最短路径和启发式估计(曼哈顿距离)。
-
幽灵每15帧重新计算路径,平衡性能与实时性。
代码实现:
class GhostAI:
@staticmethod
def astar(maze, start, end):
open_set = []
heapq.heappush(open_set, (0, start))
came_from = {}
g_score = {start: 0}
f_score = {start: GhostAI.heuristic(start, end)}
while open_set:
current = heapq.heappop(open_set)[1]
if current == end:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
return path[::-1]
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
neighbor = (current[0] + dx, current[1] + dy)
if 0 <= neighbor[0] < len(maze[0]) and 0 <= neighbor[1] < len(maze):
if maze[neighbor[1]][neighbor[0]] == 1:
continue
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + GhostAI.heuristic(neighbor, end)
heapq.heappush(open_set, (f_score[neighbor], neighbor))
return None
@staticmethod
def heuristic(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])
恐惧模式:
-
吃能量豆后,幽灵变蓝并切换为随机逃跑模式:
if self.frightened:
directions = [(0,1), (1,0), ...]
random.shuffle(directions) # 随机选择逃跑方向
3. 吃豆人控制与动画
移动机制:
-
方向键预输入:按下方向键时先检查路径合法性,再更新方向。
-
边界穿越:移动到画面边缘时会从另一侧出现。
嘴巴动画:
-
通过角度变化实现开合效果:
# 每帧更新嘴巴角度
self.mouth_angle += self.mouth_direction * 5
if mouth_angle超过45度则反向
眼睛方向:
-
根据移动方向动态调整眼睛位置:
if direction == (-1,0): # 向左
eye_offset_x = -radius//4
4. 游戏逻辑与状态管理
碰撞检测:
-
圆形碰撞检测:计算吃豆人与幽灵的欧氏距离。
dist = sqrt((x1-x2)^2 + (y1-y2)^2)
if dist < 15: # 发生碰撞
胜负条件:
-
胜利:吃光所有豆子和能量豆。
-
失败:生命值归零。
重置游戏:
-
按R键调用
reset_game()
,重新生成迷宫并重置角色位置。
4. 完整代码
如下:
import pygame
import random
import heapq
from collections import deque
# 游戏常量
CELL_SIZE = 30
MAZE_WIDTH = 21 # 必须为奇数
MAZE_HEIGHT = 21 # 必须为奇数
WIDTH = MAZE_WIDTH * CELL_SIZE
HEIGHT = MAZE_HEIGHT * CELL_SIZE
PACMAN_SPEED = 3
GHOST_SPEED = 2
# 颜色定义
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
PINK = (255, 184, 255)
CYAN = (0, 255, 255)
ORANGE = (255, 184, 82)
class MazeGenerator:
@staticmethod
def generate_maze(width, height):
# 初始化全墙迷宫
maze = [[1 for _ in range(width)] for _ in range(height)]
# 使用Prim算法生成多路径迷宫
def prim_maze():
# 从起点(1,1)开始
frontier = []
start = (1, 1)
maze[start[1]][start[0]] = 0
# 添加相邻墙到边界列表
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
nx, ny = start[0] + dx, start[1] + dy
if 0 < nx < width - 1 and 0 < ny < height - 1:
frontier.append((nx, ny, start[0], start[1]))
while frontier:
# 随机选择一个边界墙
random.shuffle(frontier)
x, y, px, py = frontier.pop()
# 如果对面是墙,就打通
opposite_x, opposite_y = px + (x - px) * 2, py + (y - py) * 2
if 0 < opposite_x < width - 1 and 0 < opposite_y < height - 1:
if maze[opposite_y][opposite_x] == 1:
maze[y][x] = 0
maze[opposite_y][opposite_x] = 0
# 添加新的边界墙
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
nx, ny = opposite_x + dx, opposite_y + dy
if 0 < nx < width - 1 and 0 < ny < height - 1 and maze[ny][nx] == 1:
frontier.append((nx, ny, opposite_x, opposite_y))
prim_maze()
# 确保至少有多个环路
for _ in range(width * height // 10):
x, y = random.randint(1, width - 2), random.randint(1, height - 2)
if maze[y][x] == 1:
neighbors = 0
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
if maze[y + dy][x + dx] == 0:
neighbors += 1
if neighbors >= 2:
maze[y][x] = 0
# 确保起点和终点是通路
maze[1][1] = 0
maze[height - 2][width - 2] = 0
return maze
class GhostAI:
@staticmethod
def astar(maze, start, end):
open_set = []
heapq.heappush(open_set, (0, start))
came_from = {}
g_score = {start: 0}
f_score = {start: GhostAI.heuristic(start, end)}
while open_set:
current = heapq.heappop(open_set)[1]
if current == end:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
return path[::-1]
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
neighbor = (current[0] + dx, current[1] + dy)
if 0 <= neighbor[0] < len(maze[0]) and 0 <= neighbor[1] < len(maze):
if maze[neighbor[1]][neighbor[0]] == 1:
continue
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + GhostAI.heuristic(neighbor, end)
heapq.heappush(open_set, (f_score[neighbor], neighbor))
return None
@staticmethod
def heuristic(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])
class Ghost:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
self.path = deque()
self.speed = GHOST_SPEED
self.frightened = False
self.frightened_timer = 0
def update(self, maze, pacman_pos):
# 如果处于恐惧状态
if self.frightened:
self.frightened_timer -= 1
if self.frightened_timer <= 0:
self.frightened = False
# 每15帧重新计算路径
if pygame.time.get_ticks() % 15 == 0:
start = (int(self.x // CELL_SIZE), int(self.y // CELL_SIZE))
end = (int(pacman_pos[0] // CELL_SIZE), int(pacman_pos[1] // CELL_SIZE))
if self.frightened:
# 恐惧状态下随机逃跑
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
random.shuffle(directions)
for dx, dy in directions:
nx, ny = start[0] + dx, start[1] + dy
if 0 <= nx < len(maze[0]) and 0 <= ny < len(maze):
if maze[ny][nx] == 0:
end = (nx, ny)
break
else:
# 正常追踪
end = (int(pacman_pos[0] // CELL_SIZE), int(pacman_pos[1] // CELL_SIZE))
new_path = GhostAI.astar(maze, start, end)
if new_path:
self.path = deque(new_path)
if self.path:
target = self.path[0]
target_x = target[0] * CELL_SIZE + CELL_SIZE // 2
target_y = target[1] * CELL_SIZE + CELL_SIZE // 2
# 平滑移动
dx = target_x - self.x
dy = target_y - self.y
dist = (dx ** 2 + dy ** 2) ** 0.5
if dist < 5: # 接近目标点时切换到下一个路径点
self.path.popleft()
else:
if dist != 0:
dx, dy = dx / dist * self.speed, dy / dist * self.speed
self.x += dx
self.y += dy
def draw(self, screen):
color = BLUE if self.frightened else self.color
pygame.draw.circle(screen, color, (int(self.x), int(self.y)), CELL_SIZE // 2)
# 画幽灵眼睛
eye_size = CELL_SIZE // 6
if not self.frightened:
pygame.draw.circle(screen, WHITE, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)), eye_size)
pygame.draw.circle(screen, WHITE, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)), eye_size)
pygame.draw.circle(screen, BLACK, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
eye_size // 2)
pygame.draw.circle(screen, BLACK, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
eye_size // 2)
else:
# 恐惧状态的眼睛
pygame.draw.circle(screen, WHITE, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
eye_size // 1.5)
pygame.draw.circle(screen, WHITE, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
eye_size // 1.5)
pygame.draw.circle(screen, BLACK, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
eye_size // 3)
pygame.draw.circle(screen, BLACK, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
eye_size // 3)
class Pacman:
def __init__(self, x, y):
self.x = x
self.y = y
self.direction = (0, 0)
self.next_direction = (0, 0)
self.speed = PACMAN_SPEED
self.mouth_angle = 0
self.mouth_direction = 1
self.radius = CELL_SIZE // 2 - 2
def update(self, maze):
# 尝试应用下一个方向
if self.next_direction != (0, 0):
next_x = self.x + self.next_direction[0] * self.speed
next_y = self.y + self.next_direction[1] * self.speed
cell_x = int(next_x // CELL_SIZE)
cell_y = int(next_y // CELL_SIZE)
if 0 <= cell_x < MAZE_WIDTH and 0 <= cell_y < MAZE_HEIGHT:
if maze[cell_y][cell_x] == 0:
self.direction = self.next_direction
# 移动
new_x = self.x + self.direction[0] * self.speed
new_y = self.y + self.direction[1] * self.speed
cell_x = int(new_x // CELL_SIZE)
cell_y = int(new_y // CELL_SIZE)
# 检查是否碰到墙壁
if 0 <= cell_x < MAZE_WIDTH and 0 <= cell_y < MAZE_HEIGHT:
if maze[cell_y][cell_x] == 0:
self.x = new_x
self.y = new_y
# 边界穿越
if self.x < 0:
self.x = WIDTH
elif self.x > WIDTH:
self.x = 0
# 嘴巴动画
self.mouth_angle += self.mouth_direction * 5
if self.mouth_angle > 45 or self.mouth_angle < 0:
self.mouth_direction *= -1
def draw(self, screen):
angle = 0
if self.direction == (-1, 0):
angle = 180
elif self.direction == (0, -1):
angle = 90
elif self.direction == (0, 1):
angle = 270
# 绘制吃豆人(带嘴巴动画)
center = (int(self.x), int(self.y))
start_angle = (angle + self.mouth_angle) * 3.14 / 180
end_angle = (angle + 360 - self.mouth_angle) * 3.14 / 180
# 主体
pygame.draw.circle(screen, YELLOW, center, self.radius)
# 嘴巴
if self.direction != (0, 0):
pygame.draw.arc(screen, BLACK,
(self.x - self.radius, self.y - self.radius,
self.radius * 2, self.radius * 2),
start_angle, end_angle, self.radius)
# 眼睛
eye_offset_x = 0
eye_offset_y = -self.radius // 3
if self.direction == (-1, 0):
eye_offset_x = -self.radius // 4
elif self.direction == (1, 0):
eye_offset_x = self.radius // 4
elif self.direction == (0, -1):
eye_offset_y = -self.radius // 2
elif self.direction == (0, 1):
eye_offset_y = 0
pygame.draw.circle(screen, BLACK,
(int(self.x + eye_offset_x), int(self.y + eye_offset_y)),
self.radius // 5)
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Pac-Man")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont(None, 36)
self.reset_game()
def reset_game(self):
self.maze = MazeGenerator.generate_maze(MAZE_WIDTH, MAZE_HEIGHT)
# 吃豆人初始位置
self.pacman = Pacman(CELL_SIZE + CELL_SIZE // 2, CELL_SIZE + CELL_SIZE // 2)
# 创建多个幽灵
self.ghosts = [
Ghost(MAZE_WIDTH * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE,
MAZE_HEIGHT * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE, RED),
Ghost(CELL_SIZE + CELL_SIZE // 2,
MAZE_HEIGHT * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE, PINK),
Ghost(MAZE_WIDTH * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE,
CELL_SIZE + CELL_SIZE // 2, CYAN),
Ghost(CELL_SIZE + CELL_SIZE // 2,
CELL_SIZE + CELL_SIZE // 2, ORANGE)
]
# 生成豆子
self.dots = []
self.power_pellets = []
for y in range(MAZE_HEIGHT):
for x in range(MAZE_WIDTH):
if self.maze[y][x] == 0:
if (x + y) % 7 == 0 and (x > 3 or y > 3) and (x < MAZE_WIDTH - 4 or y < MAZE_HEIGHT - 4):
self.power_pellets.append((x * CELL_SIZE + CELL_SIZE // 2,
y * CELL_SIZE + CELL_SIZE // 2))
else:
self.dots.append((x * CELL_SIZE + CELL_SIZE // 2,
y * CELL_SIZE + CELL_SIZE // 2))
self.score = 0
self.lives = 3
self.game_over = False
self.win = False
self.frightened_mode = False
self.frightened_timer = 0
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
return False
elif event.key == pygame.K_r:
self.reset_game()
return True
# 设置下一个方向
if event.key == pygame.K_LEFT:
self.pacman.next_direction = (-1, 0)
elif event.key == pygame.K_RIGHT:
self.pacman.next_direction = (1, 0)
elif event.key == pygame.K_UP:
self.pacman.next_direction = (0, -1)
elif event.key == pygame.K_DOWN:
self.pacman.next_direction = (0, 1)
return True
def update(self):
if self.game_over or self.win:
return
# 更新吃豆人
self.pacman.update(self.maze)
# 更新幽灵
for ghost in self.ghosts:
ghost.update(self.maze, (self.pacman.x, self.pacman.y))
# 吃豆子检测
for dot in self.dots[:]:
if ((self.pacman.x - dot[0]) ** 2 + (self.pacman.y - dot[1]) ** 2) ** 0.5 < CELL_SIZE // 3:
self.dots.remove(dot)
self.score += 10
# 吃能量豆检测
for pellet in self.power_pellets[:]:
if ((self.pacman.x - pellet[0]) ** 2 + (self.pacman.y - pellet[1]) ** 2) ** 0.5 < CELL_SIZE // 3:
self.power_pellets.remove(pellet)
self.score += 50
self.frightened_mode = True
self.frightened_timer = 500 # 约8秒
for ghost in self.ghosts:
ghost.frightened = True
ghost.frightened_timer = self.frightened_timer
# 更新恐惧模式计时器
if self.frightened_mode:
self.frightened_timer -= 1
if self.frightened_timer <= 0:
self.frightened_mode = False
for ghost in self.ghosts:
ghost.frightened = False
# 幽灵碰撞检测
for ghost in self.ghosts:
dist = ((self.pacman.x - ghost.x) ** 2 + (self.pacman.y - ghost.y) ** 2) ** 0.5
if dist < CELL_SIZE // 2:
if ghost.frightened:
# 吃掉幽灵
ghost.x = MAZE_WIDTH * CELL_SIZE // 2
ghost.y = MAZE_HEIGHT * CELL_SIZE // 2
ghost.frightened = False
self.score += 200
else:
# 被幽灵抓到
self.lives -= 1
if self.lives <= 0:
self.game_over = True
else:
# 重置位置
self.pacman.x = CELL_SIZE + CELL_SIZE // 2
self.pacman.y = CELL_SIZE + CELL_SIZE // 2
self.pacman.direction = (0, 0)
for g in self.ghosts:
g.x = random.choice([CELL_SIZE + CELL_SIZE // 2,
MAZE_WIDTH * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE])
g.y = random.choice([CELL_SIZE + CELL_SIZE // 2,
MAZE_HEIGHT * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE])
g.frightened = False
break
# 胜利条件
if not self.dots and not self.power_pellets:
self.win = True
def draw(self):
self.screen.fill(BLACK)
# 绘制迷宫
for y in range(MAZE_HEIGHT):
for x in range(MAZE_WIDTH):
if self.maze[y][x] == 1:
pygame.draw.rect(self.screen, BLUE,
(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE))
# 绘制豆子
for dot in self.dots:
pygame.draw.circle(self.screen, WHITE, dot, 3)
# 绘制能量豆
for pellet in self.power_pellets:
pygame.draw.circle(self.screen, WHITE, pellet, 8)
# 绘制幽灵
for ghost in self.ghosts:
ghost.draw(self.screen)
# 绘制吃豆人
self.pacman.draw(self.screen)
# 绘制分数和生命
score_text = self.font.render(f"Score: {self.score}", True, WHITE)
lives_text = self.font.render(f"Lives: {self.lives}", True, WHITE)
self.screen.blit(score_text, (10, 10))
self.screen.blit(lives_text, (WIDTH - 120, 10))
# 游戏结束或胜利提示
if self.game_over:
text = self.font.render("GAME OVER! Press R to restart", True, WHITE)
self.screen.blit(text, (WIDTH // 2 - 180, HEIGHT // 2))
elif self.win:
text = self.font.render("YOU WIN! Press R to restart", True, WHITE)
self.screen.blit(text, (WIDTH // 2 - 160, HEIGHT // 2))
pygame.display.flip()
def run(self):
running = True
while running:
running = self.handle_events()
self.update()
self.draw()
self.clock.tick(60)
pygame.quit()
if __name__ == "__main__":
game = Game()
game.run()