本篇博客展示如何使用 Python + Pygame 编写一个简易版的“吃豆人(Pac-Man)” 风格游戏。这里我们暂且命名为 Py-Man。玩家需要控制主角在一个网格地图里移动、吃掉散布在各处的豆子,并躲避在地图中巡逻的幽灵。此示例可帮助你理解网格地图、角色移动、敌人随机移动、碰撞与得分等常见2D游戏开发技巧。
1. 前言
吃豆人(Pac-Man) 自 1980 年诞生以来,一直是街机游戏史上的里程碑作品。它的核心玩法是:
- 玩家控制吃豆人(Pac-Man),在迷宫状的地图里收集豆子;
- 幽灵(Ghost)会在地图中巡逻,玩家要设法规避幽灵追击;
- 当豆子吃光或玩家被幽灵抓到,游戏结束。
本篇中,我们编写一个缩小且精简的“吃豆人”原型示例,重点演示:
- 地图使用网格表示:不同数字代表墙壁、豆子、幽灵、可行走区域等;
- 玩家(Pac-Man)的移动:基于方向键上下左右移动,每次移动一格;
- 幽灵的随机行动:每帧可能随机选择一个方向前进(如遇墙则停留或换方向);
- 碰撞和得分:吃完全部豆子即胜利,若与幽灵坐标重叠则游戏失败。
2. 开发环境
- Python 3.x
- Pygame 库:若未安装可通过
pip install pygame
- 支持图形界面的操作系统:Windows、macOS 或绝大多数 Linux 均可。
在确保 import pygame
没有报错后,即可开始项目的开发。
3. 游戏设计思路
-
网格地图(MAP)
- 使用一个二维列表来表示地图;
- 为简化,我们定义:
1
表示墙壁,不可通过;2
表示幽灵初始位置;3
表示豆子,需要被吃掉;4
表示玩家初始位置;0
表示空地,可以行走。
- 当然,实际 Pac-Man 会有更复杂的地图布局,这里只做示例。
-
玩家
- 存储玩家的网格坐标
(row, col)
; - 通过键盘上下左右控制每次移动一格;
- 若目标位置是墙壁则保持不动,否则进入该格;
- 如果在有豆子的格子上,就吃掉该豆子并加分。
- 存储玩家的网格坐标
-
幽灵
- 地图可含多个幽灵;
- 每帧随机选择一个方向移动,如果该方向是墙壁或出界,则不移动或重新选择;
- 当玩家和幽灵坐标重叠时,游戏失败。
-
得分与胜利
- 每当玩家吃掉一颗豆子(3),得分增加 1,并将该格子改为 0;
- 如果全部豆子都被吃光,游戏胜利。
-
游戏循环
- 处理键盘事件与幽灵的随机移动;
- 更新玩家与幽灵位置;
- 检测玩家是否吃到豆子、是否与幽灵碰撞;
- 如果游戏结束或胜利,则跳转到结束场景。
4. 完整示例代码
将以下代码保存为 py_man.py
并执行。你可根据需要自定义地图大小、关卡布局、移动速度、幽灵 AI 等来拓展本示例。
import pygame
import sys
import random
# 初始化 Pygame
pygame.init()
# -----------------------
# 全局配置
# -----------------------
TILE_SIZE = 40 # 每个格子的像素大小
FPS = 8 # 帧率(适当降低 以便看清幽灵移动)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 200, 0)
RED = (255, 0, 0)
YELLOW= (255, 255, 0)
GRAY = (100, 100, 100)
# 地图定义:1-墙,2-幽灵,3-豆子,4-玩家位置,0-空地
GAME_MAP = [
[1,1,1,1,1,1,1,1,1,1],
[1,4,3,3,0,3,3,3,3,1],
[1,3,1,1,3,1,1,1,0,1],
[1,3,1,2,3,1,2,3,3,1],
[1,3,3,3,3,3,3,1,3,1],
[1,0,1,3,1,3,1,3,3,1],
[1,3,1,3,1,3,1,3,3,1],
[1,3,3,3,3,3,3,3,3,1],
[1,3,3,1,3,0,3,3,3,1],
[1,1,1,1,1,1,1,1,1,1],
]
ROWS = len(GAME_MAP)
COLS = len(GAME_MAP[0])
SCREEN_WIDTH = COLS * TILE_SIZE
SCREEN_HEIGHT = ROWS * TILE_SIZE
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Py-Man 简易吃豆人")
clock = pygame.time.Clock()
font = pygame.font.SysFont("arial", 32)
# -----------------------
# 玩家类
# -----------------------
class Player:
def __init__(self, row, col):
self.row = row
self.col = col
self.score = 0
def move(self, dr, dc):
new_row = self.row + dr
new_col = self.col + dc
if 0 <= new_row < ROWS and 0 <= new_col < COLS:
# 如果不是墙(1),则可移动
if GAME_MAP[new_row][new_col] != 1:
self.row = new_row
self.col = new_col
@property
def x(self):
return self.col * TILE_SIZE
@property
def y(self):
return self.row * TILE_SIZE
# -----------------------
# 幽灵类
# -----------------------
class Ghost:
def __init__(self, row, col):
self.row = row
self.col = col
def update(self):
# 随机尝试一个方向移动(上下左右)
directions = [(-1,0), (1,0), (0,-1), (0,1)]
dr, dc = random.choice(directions)
new_row = self.row + dr
new_col = self.col + dc
# 如果新位置不是墙,且在地图内,则移动
if 0 <= new_row < ROWS and 0 <= new_col < COLS:
if GAME_MAP[new_row][new_col] != 1:
self.row = new_row
self.col = new_col
@property
def x(self):
return self.col * TILE_SIZE
@property
def y(self):
return self.row * TILE_SIZE
# -----------------------
# 工具函数:初始化游戏对象
# -----------------------
def init_game():
player = None
ghosts = []
for r in range(ROWS):
for c in range(COLS):
cell = GAME_MAP[r][c]
if cell == 4: # 玩家
player = Player(r, c)
# 恢复成空地
GAME_MAP[r][c] = 0
elif cell == 2: # 幽灵
ghosts.append(Ghost(r, c))
# 恢复成空地
GAME_MAP[r][c] = 0
return player, ghosts
# -----------------------
# 主游戏逻辑
# -----------------------
def main():
player, ghosts = init_game()
running = True
# 统计剩余豆子数
total_beans = sum(row.count(3) for row in GAME_MAP)
while running:
clock.tick(FPS)
# 1) 事件处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 2) 键盘输入(上下左右)
keys = pygame.key.get_pressed()
if keys[pygame.K_UP]:
player.move(-1, 0)
elif keys[pygame.K_DOWN]:
player.move(1, 0)
elif keys[pygame.K_LEFT]:
player.move(0, -1)
elif keys[pygame.K_RIGHT]:
player.move(0, 1)
# 玩家若踩到豆子(3),吃掉并得分
if GAME_MAP[player.row][player.col] == 3:
GAME_MAP[player.row][player.col] = 0
player.score += 1
# 若吃完所有豆子 -> 胜利
if player.score == total_beans:
running = False
game_over(won=True, score=player.score)
continue
# 幽灵随机移动
for g in ghosts:
g.update()
# 检测玩家是否与幽灵相碰
for g in ghosts:
if g.row == player.row and g.col == player.col:
# 玩家失败
running = False
game_over(won=False, score=player.score)
break
# 3) 绘制场景
screen.fill(BLACK)
# 绘制地图
for r in range(ROWS):
for c in range(COLS):
tile = GAME_MAP[r][c]
x = c * TILE_SIZE
y = r * TILE_SIZE
if tile == 1:
pygame.draw.rect(screen, BLUE, (x, y, TILE_SIZE, TILE_SIZE)) # 墙
elif tile == 3:
pygame.draw.circle(screen, YELLOW, (x + TILE_SIZE//2, y + TILE_SIZE//2), TILE_SIZE//6) # 豆子
else:
# 通路 or 空地
pygame.draw.rect(screen, GRAY, (x, y, TILE_SIZE, TILE_SIZE))
# 绘制玩家 (绿色圆)
pygame.draw.circle(screen, GREEN, (player.x + TILE_SIZE//2, player.y + TILE_SIZE//2), TILE_SIZE//2 - 4)
# 绘制幽灵 (红色方块)
for g in ghosts:
pygame.draw.rect(screen, RED, (g.x+5, g.y+5, TILE_SIZE-10, TILE_SIZE-10))
# 分数
score_surf = font.render(f"Score: {player.score}", True, WHITE)
screen.blit(score_surf, (10, 10))
pygame.display.flip()
pygame.quit()
sys.exit()
def game_over(won, score):
screen.fill(BLACK)
if won:
msg = f"恭喜,你吃掉所有豆子!得分: {score}"
else:
msg = f"被幽灵抓住了!得分: {score}"
label = font.render(msg, True, WHITE)
rect = label.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2))
screen.blit(label, rect)
pygame.display.flip()
pygame.time.wait(3000) # 等待3秒再退出
if __name__ == "__main__":
main()
代码说明
-
地图
GAME_MAP
- 用一个二维列表存储地图信息,示例尺寸 10×10:
1
(蓝色方块)表示墙壁;3
(黄色小圆)表示豆子;2
与4
仅用于初始位置记录,随后会被设为0
。
- 你可以自行扩展地图,或使用文件读取的方式加载更大规模的关卡。
- 用一个二维列表存储地图信息,示例尺寸 10×10:
-
玩家与幽灵
- 玩家每帧检测上下左右键,若不是墙就移动。并且若所在格子是豆子就得分、把该格子变成空地;
- 幽灵在
update()
里随机选择一个方向移动,如果遭遇墙壁则保持原地或尝试别的方向。 - 当玩家坐标与幽灵坐标一致,则判定失败。
-
得分与胜利
- 统计地图中豆子的总数
total_beans
; - 如果玩家吃掉的豆子数与
total_beans
相等,则判定胜利; - 碰到幽灵则立即失败。
- 统计地图中豆子的总数
-
绘制
- 墙壁用蓝色矩形,豆子用黄色小圆,空地用灰色背景;
- 玩家用绿色圆,幽灵用红色小方块。
- 你可以替换为更精美的贴图或动画帧,让游戏看起来更加逼真。
5. 运行效果
6. 总结
通过本篇示例,你已掌握了一个简化“吃豆人” 原型所需的关键实现,你可以在此基础上,结合你对 Pac-Man 的灵感或其他创意,一步步将这个简易示例打磨成更完备、更具乐趣的 2D 游戏。