Bootstrap

使用Pygame制作“吃豆人”游戏

本篇博客展示如何使用 Python + Pygame 编写一个简易版的“吃豆人(Pac-Man)” 风格游戏。这里我们暂且命名为 Py-Man。玩家需要控制主角在一个网格地图里移动、吃掉散布在各处的豆子,并躲避在地图中巡逻的幽灵。此示例可帮助你理解网格地图角色移动敌人随机移动碰撞与得分等常见2D游戏开发技巧。


1. 前言

吃豆人(Pac-Man) 自 1980 年诞生以来,一直是街机游戏史上的里程碑作品。它的核心玩法是:

  • 玩家控制吃豆人(Pac-Man),在迷宫状的地图里收集豆子;
  • 幽灵(Ghost)会在地图中巡逻,玩家要设法规避幽灵追击;
  • 当豆子吃光或玩家被幽灵抓到,游戏结束。

本篇中,我们编写一个缩小且精简的“吃豆人”原型示例,重点演示:

  • 地图使用网格表示:不同数字代表墙壁、豆子、幽灵、可行走区域等;
  • 玩家(Pac-Man)的移动:基于方向键上下左右移动,每次移动一格;
  • 幽灵的随机行动:每帧可能随机选择一个方向前进(如遇墙则停留或换方向);
  • 碰撞和得分:吃完全部豆子即胜利,若与幽灵坐标重叠则游戏失败。

2. 开发环境

  1. Python 3.x
  2. Pygame 库:若未安装可通过
    pip install pygame
    
  3. 支持图形界面的操作系统:Windows、macOS 或绝大多数 Linux 均可。

在确保 import pygame 没有报错后,即可开始项目的开发。


3. 游戏设计思路

  1. 网格地图(MAP)

    • 使用一个二维列表来表示地图;
    • 为简化,我们定义:
      • 1 表示墙壁,不可通过;
      • 2 表示幽灵初始位置;
      • 3 表示豆子,需要被吃掉;
      • 4 表示玩家初始位置;
      • 0 表示空地,可以行走。
    • 当然,实际 Pac-Man 会有更复杂的地图布局,这里只做示例。
  2. 玩家

    • 存储玩家的网格坐标 (row, col)
    • 通过键盘上下左右控制每次移动一格;
    • 若目标位置是墙壁则保持不动,否则进入该格;
    • 如果在有豆子的格子上,就吃掉该豆子并加分。
  3. 幽灵

    • 地图可含多个幽灵;
    • 每帧随机选择一个方向移动,如果该方向是墙壁或出界,则不移动或重新选择;
    • 当玩家和幽灵坐标重叠时,游戏失败。
  4. 得分与胜利

    • 每当玩家吃掉一颗豆子(3),得分增加 1,并将该格子改为 0;
    • 如果全部豆子都被吃光,游戏胜利。
  5. 游戏循环

    • 处理键盘事件与幽灵的随机移动;
    • 更新玩家与幽灵位置;
    • 检测玩家是否吃到豆子、是否与幽灵碰撞;
    • 如果游戏结束或胜利,则跳转到结束场景。

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()

代码说明

  1. 地图 GAME_MAP

    • 用一个二维列表存储地图信息,示例尺寸 10×10:
      • 1(蓝色方块)表示墙壁;
      • 3(黄色小圆)表示豆子;
      • 24 仅用于初始位置记录,随后会被设为 0
    • 你可以自行扩展地图,或使用文件读取的方式加载更大规模的关卡。
  2. 玩家与幽灵

    • 玩家每帧检测上下左右键,若不是墙就移动。并且若所在格子是豆子就得分、把该格子变成空地;
    • 幽灵update() 里随机选择一个方向移动,如果遭遇墙壁则保持原地或尝试别的方向。
    • 玩家坐标幽灵坐标一致,则判定失败。
  3. 得分与胜利

    • 统计地图中豆子的总数 total_beans
    • 如果玩家吃掉的豆子数与 total_beans 相等,则判定胜利;
    • 碰到幽灵则立即失败。
  4. 绘制

    • 墙壁用蓝色矩形,豆子用黄色小圆,空地用灰色背景
    • 玩家用绿色圆,幽灵用红色小方块
    • 你可以替换为更精美的贴图或动画帧,让游戏看起来更加逼真。

5. 运行效果

image.png


6. 总结

通过本篇示例,你已掌握了一个简化“吃豆人” 原型所需的关键实现,你可以在此基础上,结合你对 Pac-Man 的灵感或其他创意,一步步将这个简易示例打磨成更完备、更具乐趣的 2D 游戏。

;