目录
前言
吃豆人(Pac-Man)是一款经典的街机游戏,自1980年问世以来一直深受玩家喜爱。在这篇博客中,我将详细介绍如何使用Python的Pygame库从零开始构建一个完整的吃豆人游戏。我们将逐步实现游戏的核心功能,包括角色控制、碰撞检测、AI行为等,并且深入剖析每个组件的工作原理。
无论你是游戏开发新手还是想要提升编程技能的中级开发者,这个项目都将帮助你理解游戏开发的基本原理和技巧。让我们开始这段有趣的编程之旅吧!
1. Pygame游戏开发基础
1.1 Pygame简介
Pygame是一个为Python设计的游戏开发库,它基于SDL(Simple DirectMedia Layer)构建,提供了图形、声音、输入设备等功能的简单接口,非常适合初学者和中级开发者使用。
Pygame的主要特点包括:
- 跨平台:可在Windows、macOS、Linux等多种系统上运行
- 易于学习:API设计简洁直观
- 功能全面:提供图形渲染、声音播放、输入处理等游戏开发必备功能
- 活跃的社区:有大量的教程和资源可供学习
1.2 游戏开发基本概念
在开始编写游戏代码前,让我们先了解一些游戏开发的基本概念:
1. 游戏循环(Game Loop)
游戏循环是几乎所有电子游戏的核心,它通常包含以下三个主要步骤:
- 处理输入(Processing Input):检测并响应用户的键盘、鼠标等输入
- 更新游戏状态(Updating Game State):根据输入和游戏规则更新游戏对象的状态
- 渲染(Rendering):将当前游戏状态绘制到屏幕上
2. 精灵(Sprite)
在游戏开发中,精灵是指可以在屏幕上移动的图形对象。在我们的吃豆人游戏中,吃豆人和幽灵都是精灵。
3. 碰撞检测(Collision Detection)
碰撞检测用于判断游戏对象之间是否发生接触或重叠,这在游戏中非常重要。例如,我们需要检测吃豆人是否碰到了豆子或幽灵。
4. 帧率控制(Frame Rate Control)
帧率是指游戏每秒更新和渲染的次数。控制帧率对于确保游戏在不同硬件上有一致的表现非常重要。
1.3 Pygame核心模块介绍
Pygame提供了多个模块来处理游戏开发的不同方面:
pygame.display
:创建和管理游戏窗口pygame.event
:处理用户输入和其他事件pygame.draw
:提供基本图形绘制功能pygame.image
:加载和处理图像pygame.mixer
:处理音频播放pygame.font
:渲染文本pygame.time
:控制时间和帧率pygame.Rect
:处理矩形区域(对碰撞检测很有用)
在我们的吃豆人游戏中,将主要使用这些模块来实现各种功能。
2. 游戏设计与规划
2.1 游戏规则设计
首先,让我们确定我们的吃豆人游戏的基本规则:
- 玩家控制吃豆人在迷宫中移动,目标是吃掉所有的豆子
- 迷宫中有四个会追逐吃豆人的幽灵
- 如果幽灵碰到吃豆人,吃豆人会失去一条生命
- 玩家初始有3条生命,全部失去后游戏结束
- 吃掉小豆子可以获得10分
- 吃掉大豆子(能量豆)可以获得50分,并且能暂时让幽灵变成可食用状态
- 在幽灵处于可食用状态时,吃豆人可以吃掉幽灵获得200分
- 吃掉所有豆子后,玩家获胜
2.2 游戏对象规划
我们的游戏需要以下几种主要对象:
-
吃豆人(Pac-Man) :
- 属性:位置、方向、速度、生命值、分数
- 行为:移动、改变方向、吃豆子、与幽灵交互
-
幽灵(Ghost) :
- 属性:位置、颜色、方向、速度、状态(普通/可食用)
- 行为:移动、追逐吃豆人、在被吃后重生
-
迷宫(Maze) :
- 属性:网格布局(墙壁和通道)
- 用途:限制角色移动范围,提供游戏环境
-
豆子(Dots) :
- 小豆子:被吃后加10分
- 大豆子(能量豆):被吃后加50分并激活幽灵的可食用状态
-
游戏管理器:
- 控制游戏状态(运行中、暂停、游戏结束)
- 管理得分系统
- 处理游戏逻辑(如关卡切换、胜利条件检查)
2.3 技术方案选择
对于我们的吃豆人游戏,我们将采用以下技术方案:
- 游戏引擎:Pygame(提供图形渲染、输入处理等基础功能)
- 图形表示:使用简单的几何图形(圆形、矩形等)绘制游戏元素
- 碰撞检测:基于距离计算的简单碰撞检测方法
- AI算法:简化的追逐算法,幽灵会有一定概率朝吃豆人的方向移动
- 地图表示:使用二维数组表示迷宫布局,1表示墙壁,0表示通道
这种方案适合初学者理解,同时也能实现一个功能完整的吃豆人游戏。
3. 创建游戏窗口与初始化
3.1 初始化Pygame环境
首先,我们需要导入必要的模块并初始化Pygame环境:
python
import pygame
import random
import math
# 初始化 Pygame
pygame.init()
pygame.init()
函数初始化所有Pygame模块,这是使用Pygame的第一步。
3.2 设置游戏窗口
接下来,我们创建游戏窗口并设置标题:
python
# 设置游戏窗口
WIDTH, HEIGHT = 800, 600
win = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Pac-Man 游戏")
这里我们创建了一个800x600像素的游戏窗口,并将其标题设置为"Pac-Man 游戏"。pygame.display.set_mode()
函数返回一个Surface对象,我们将使用这个对象来绘制游戏元素。
3.3 定义颜色和游戏参数
为了使代码更清晰,我们定义了一些常用颜色和游戏参数:
python
# 颜色定义
BLACK = (0, 0, 0)
YELLOW = (255, 255, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
PINK = (255, 192, 203)
ORANGE = (255, 165, 0)
CYAN = (0, 255, 255)
# 游戏参数
CELL_SIZE = 30
GRID_WIDTH = WIDTH // CELL_SIZE
GRID_HEIGHT = HEIGHT // CELL_SIZE
颜色在Pygame中用RGB元组表示,例如(255, 255, 0)表示黄色。
CELL_SIZE
定义了游戏网格中每个单元格的大小(30像素),GRID_WIDTH
和GRID_HEIGHT
计算了游戏窗口可以容纳的网格数量。这种网格系统将帮助我们更容易地放置游戏对象并处理碰撞检测。
3.4 初始化游戏时钟
Pygame提供了一个Clock对象来控制游戏的帧率:
clock = pygame.time.Clock()
在游戏循环中,我们将使用clock.tick(60)
来确保游戏以约60帧每秒的速度运行。这对于保持游戏运行速度一致非常重要,无论游戏运行在什么样的硬件上。
4. 吃豆人角色设计与实现
4.1 吃豆人类设计
我们创建一个PacMan
类来封装吃豆人的属性和行为:
python
class PacMan:
def __init__(self):
self.x = GRID_WIDTH // 2
self.y = GRID_HEIGHT // 2
self.direction = "right"
self.speed = 0.1
self.mouth_open = True
self.mouth_counter = 0
self.score = 0
self.lives = 3
这个初始化方法设置了吃豆人的起始位置(在网格中心),方向(向右),速度,嘴巴状态(用于动画),分数和生命值。
4.2 实现吃豆人移动
让我们添加一个方法来处理吃豆人的移动:
python
def move(self, direction, grid):
new_x, new_y = self.x, self.y
if direction == "right":
new_x += self.speed
elif direction == "left":
new_x -= self.speed
elif direction == "up":
new_y -= self.speed
elif direction == "down":
new_y += self.speed
# 检查是否能移动(不与墙碰撞)
cell_x, cell_y = int(new_x), int(new_y)
if 0 <= cell_x < GRID_WIDTH and 0 <= cell_y < GRID_HEIGHT and grid[cell_y][cell_x] != 1:
self.x, self.y = new_x, new_y
self.direction = direction
# 更新嘴巴动画
self.mouth_counter += 1
if self.mouth_counter >= 10:
self.mouth_counter = 0
self.mouth_open = not self.mouth_open
这个方法接受一个方向参数和迷宫网格,然后尝试向该方向移动吃豆人。它首先计算吃豆人的新位置,然后检查这个位置是否有效(在网格范围内且不是墙壁)。如果有效,就更新吃豆人的位置和方向。
此外,这个方法还更新了嘴巴的动画状态,每10帧切换一次嘴巴的开合状态,这将创建吃豆人标志性的"吃"的动画效果。
4.3 绘制吃豆人
接下来,我们需要一个方法来绘制吃豆人:
python
def draw(self, win):
x = int(self.x * CELL_SIZE + CELL_SIZE // 2)
y = int(self.y * CELL_SIZE + CELL_SIZE // 2)
radius = CELL_SIZE // 2
# 绘制 Pac-Man
if self.mouth_open:
# 嘴巴张开
if self.direction == "right":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x + radius, y - radius // 2),
(x + radius, y + radius // 2)])
elif self.direction == "left":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x - radius, y - radius // 2),
(x - radius, y + radius // 2)])
elif self.direction == "up":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x - radius // 2, y - radius),
(x + radius // 2, y - radius)])
elif self.direction == "down":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x - radius // 2, y + radius),
(x + radius // 2, y + radius)])
else:
# 嘴巴闭合
pygame.draw.circle(win, YELLOW, (x, y), radius)
这个方法使用Pygame的绘图函数来绘制吃豆人。当嘴巴打开时,我们绘制一个黄色圆形和一个黑色三角形(表示嘴巴),三角形的位置根据吃豆人的方向而改变。当嘴巴闭合时,我们只绘制一个完整的黄色圆形。
5. 幽灵角色设计与AI实现
5.1 幽灵类设计
现在让我们创建一个Ghost
类来处理幽灵的行为:
python
class Ghost:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
self.direction = random.choice(["right", "left", "up", "down"])
self.speed = 0.05
self.frightened = False
每个幽灵都有一个位置、颜色、方向、速度和状态(是否处于"惊吓"状态)。我们将创建四个不同颜色的幽灵,每个幽灵都有自己的起始位置。
5.2 幽灵AI行为实现
幽灵的关键行为是在迷宫中移动并尝试追逐吃豆人。我们实现了一个简单的AI系统:
python
def move(self, grid, pacman):
directions = ["right", "left", "up", "down"]
# 简单的 AI - 有 80% 概率朝向 Pac-Man,20% 概率随机移动
if random.random() < 0.8 and not self.frightened:
# 寻找 Pac-Man 的方向
if pacman.x > self.x and "right" in directions:
self.direction = "right"
elif pacman.x < self.x and "left" in directions:
self.direction = "left"
elif pacman.y > self.y and "down" in directions:
self.direction = "down"
elif pacman.y < self.y and "up" in directions:
self.direction = "up"
else:
# 随机选择方向
self.direction = random.choice(directions)
# 移动幽灵
new_x, new_y = self.x, self.y
if self.direction == "right":
new_x += self.speed
elif self.direction == "left":
new_x -= self.speed
elif self.direction == "up":
new_y -= self.speed
elif self.direction == "down":
new_y += self.speed
# 检查是否能移动(不与墙碰撞)
cell_x, cell_y = int(new_x), int(new_y)
if 0 <= cell_x < GRID_WIDTH and 0 <= cell_y < GRID_HEIGHT and grid[cell_y][cell_x] != 1:
self.x, self.y = new_x, new_y
else:
# 如果碰到墙,选择新方向
self.direction = random.choice(directions)
这个方法实现了一个简单但有效的AI:
- 正常状态下,幽灵有80%的概率朝着吃豆人的方向移动,20%的概率随机移动
- 当幽灵处于"惊吓"状态时,它们只会随机移动
- 如果幽灵碰到墙壁,它会选择一个新的随机方向
这种AI行为创造了一种挑战性但不是不可战胜的游戏体验。
5.3 绘制幽灵
幽灵的外观是游戏中的重要视觉元素,我们需要一个方法来绘制它们:
python
def draw(self, win):
x = int(self.x * CELL_SIZE + CELL_SIZE // 2)
y = int(self.y * CELL_SIZE + CELL_SIZE // 2)
radius = CELL_SIZE // 2
# 绘制幽灵主体
color = BLUE if self.frightened else self.color
# 绘制幽灵的半圆顶部
pygame.draw.circle(win, color, (x, y - radius // 3), radius)
# 绘制幽灵的矩形底部
pygame.draw.rect(win, color, (x - radius, y - radius // 3, radius * 2, radius))
# 绘制幽灵底部的波浪形状
wave_height = radius // 3
pygame.draw.polygon(win, color, [
(x - radius, y + radius * 2 // 3), # 左上角
(x - radius * 2 // 3, y + radius * 2 // 3 - wave_height), # 第一个波谷
(x - radius // 3, y + radius * 2 // 3), # 第一个波峰
(x, y + radius * 2 // 3 - wave_height), # 第二个波谷
(x + radius // 3, y + radius * 2 // 3), # 第二个波峰
(x + radius * 2 // 3, y + radius * 2 // 3 - wave_height), # 第三个波谷
(x + radius, y + radius * 2 // 3), # 右上角
(x + radius, y + radius * 2 // 3 - radius), # 右下角
(x - radius, y + radius * 2 // 3 - radius), # 左下角
])
# 绘制眼睛 (白色部分)
eye_radius = radius // 3
left_eye_x = x - radius // 2
right_eye_x = x + radius // 2
eye_y = y - radius // 3
pygame.draw.circle(win, WHITE, (left_eye_x, eye_y), eye_radius)
pygame.draw.circle(win, WHITE, (right_eye_x, eye_y), eye_radius)
# 绘制眼球 (瞳孔)
pupil_radius = eye_radius // 2
# 根据方向移动眼球
pupil_offset_x, pupil_offset_y = 0, 0
if self.direction == "left":
pupil_offset_x = -pupil_radius // 2
elif self.direction == "right":
pupil_offset_x = pupil_radius // 2
elif self.direction == "up":
pupil_offset_y = -pupil_radius // 2
elif self.direction == "down":
pupil_offset_y = pupil_radius // 2
pygame.draw.circle(win, BLACK, (left_eye_x + pupil_offset_x, eye_y + pupil_offset_y), pupil_radius)
pygame.draw.circle(win, BLACK, (right_eye_x + pupil_offset_x, eye_y + pupil_offset_y), pupil_radius)
这个方法使用多个形状来创建经典的幽灵外观:
- 半圆形顶部
- 矩形主体
- 波浪形底部
- 两个眼睛,眼球会根据幽灵的移动方向而变化位置
当幽灵处于"惊吓"状态时,它们会变成蓝色,让玩家知道现在可以吃掉它们。
6. 迷宫生成与渲染
6.1 迷宫表示方法
在我们的游戏中,迷宫被表示为一个二维数组,其中:
- 0表示空白区域(可以移动)
- 1表示墙壁(不可移动)
我们实现了一个函数来创建一个随机迷宫:
python
def create_maze():
grid = [[0 for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
# 添加边界
for x in range(GRID_WIDTH):
grid[0][x] = 1
grid[GRID_HEIGHT - 1][x] = 1
for y in range(GRID_HEIGHT):
grid[y][0] = 1
grid[y][GRID_WIDTH - 1] = 1
# 添加随机墙壁
for _ in range(GRID_WIDTH * GRID_HEIGHT // 10):
x = random.randint(1, GRID_WIDTH - 2)
y = random.randint(1, GRID_HEIGHT - 2)
grid[y][x] = 1
# 确保 Pac-Man 的起始位置是空的
grid[GRID_HEIGHT // 2][GRID_WIDTH // 2] = 0
return grid
这个函数首先创建一个全是0的网格,然后:
- 在网格的边缘添加墙壁,形成一个封闭的区域
- 在网格内部随机添加一些墙壁,数量约为网格总单元数的10%
- 确保吃豆人的起始位置(网格中心)是空的
这种方法生成的迷宫每次游戏都不同,增加了游戏的可重玩性。
6.2 迷宫渲染
我们需要一个方法来绘制迷宫:
python
# 绘制迷宫
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if grid[y][x] == 1:
pygame.draw.rect(win, BLUE, [x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE])
这段代码遍历整个网格,当遇到值为1的单元格时,在相应位置绘制一个蓝色矩形表示墙壁。
7. 游戏物品:豆子与能量豆
7.1 豆子生成
在我们的游戏中,有两种豆子:普通豆子和能量豆(大豆子)。我们实现了一个函数来在迷宫中生成这些豆子:
python
def create_dots(grid):
dots = []
big_dots = []
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if grid[y][x] == 0 and (x != GRID_WIDTH // 2 or y != GRID_HEIGHT // 2): # 避免在Pac-Man起始位置放置豆子
# 15% 概率创建大豆子,85% 概率创建小豆子
if random.random() < 0.15:
big_dots.append((x, y))
else:
dots.append((x, y))
return dots, big_dots
这个函数遍历网格中的所有空白单元格(值为0),然后:
- 避免在吃豆人的起始位置放置豆子
- 有15%的概率在该位置放置一个能量豆
- 有85%的概率放置一个普通豆子
函数返回两个列表,分别包含普通豆子和能量豆的位置。
7.2 豆子渲染
接下来,我们需要在游戏中绘制这些豆子:
python
# 绘制小豆子
for x, y in dots:
pygame.draw.circle(win, WHITE, (x * CELL_SIZE + CELL_SIZE // 2, y * CELL_SIZE + CELL_SIZE // 2), CELL_SIZE // 10)
# 绘制大豆子
for x, y in big_dots:
pygame.draw.circle(win, WHITE, (x * CELL_SIZE + CELL_SIZE // 2, y * CELL_SIZE + CELL_SIZE // 2), CELL_SIZE // 5)
普通豆子被绘制为小白色圆点,而能量豆被绘制为较大的白色圆点。
7.3 豆子与吃豆人的交互
当吃豆人经过豆子的位置时,我们需要检测这种碰撞并作出相应反应:
python
# 检查吃豆子
pacman_cell_x, pacman_cell_y = int(pacman.x), int(pacman.y)
# 小豆子
for i, (x, y) in enumerate(dots[:]):
if x == pacman_cell_x and y == pacman_cell_y:
dots.remove((x, y))
pacman.score += 10
# 大豆子
for i, (x, y) in enumerate(big_dots[:]):
if x == pacman_cell_x and y == pacman_cell_y:
big_dots.remove((x, y))
pacman.score += 50
# 幽灵进入惊恐状态
frightened_timer = 300 # 约5秒
for ghost in ghosts:
ghost.frightened = True
这段代码检查吃豆人当前所在的网格单元是否有豆子:
- 如果有普通豆子,移除该豆子并增加10分
- 如果有能量豆,移除该豆子,增加50分,并让所有幽灵进入"惊吓"状态
8. 碰撞检测系统
8.1 角色与墙壁的碰撞检测
在我们的游戏中,角色不能穿过墙壁。我们在move
方法中实现了这种碰撞检测:
# 检查是否能移动(不与墙碰撞)
cell_x, cell_y = int(new_x), int(new_y)
if 0 <= cell_x < GRID_WIDTH and 0 <= cell_y < GRID_HEIGHT and grid[cell_y][cell_x] != 1:
self.x, self.y = new_x, new_y
self.direction = direction
else:
# 如果是幽灵撞墙,就选择新方向
if isinstance(self, Ghost):
self.direction = random.choice(["right", "left", "up", "down"])
这段代码先检查预期的新位置是否在网格范围内,然后检查该位置是否是墙壁(值为1)。如果不是墙壁,角色可以移动到新位置;如果是墙壁,则不允许移动。特别地,如果是幽灵撞到墙壁,它会选择一个新的随机方向。
8.2 吃豆人与幽灵的碰撞检测
吃豆人与幽灵之间的碰撞检测是游戏的核心机制之一。我们使用基于距离的碰撞检测方法:
python
# 检查与幽灵碰撞
for ghost in ghosts:
distance = math.sqrt((pacman.x - ghost.x) ** 2 + (pacman.y - ghost.y) ** 2)
if distance < 0.7: # 碰撞阈值
if ghost.frightened:
# Pac-Man 吃掉幽灵
ghost.x, ghost.y = random.randint(1, GRID_WIDTH - 2), random.randint(1, GRID_HEIGHT - 2)
ghost.frightened = False
pacman.score += 200
else:
# 幽灵吃掉 Pac-Man
pacman.lives -= 1
pacman.x, pacman.y = GRID_WIDTH // 2, GRID_HEIGHT // 2
if pacman.lives <= 0:
game_over = True
这段代码计算吃豆人和每个幽灵之间的欧几里得距离。如果距离小于阈值(0.7个网格单位),则认为发生了碰撞。碰撞的结果取决于幽灵的状态:
- 如果幽灵处于"惊吓"状态,吃豆人会吃掉幽灵,获得200分,幽灵会重生在一个随机位置
- 如果幽灵处于正常状态,吃豆人会失去一条生命,并重置到起始位置。如果吃豆人的生命值降到0,游戏结束
8.3 吃豆人与豆子的碰撞检测
我们已经在第7.3节中介绍了吃豆人与豆子的碰撞检测。为了完整性,这里再次展示相关代码:
python
# 检查吃豆子
pacman_cell_x, pacman_cell_y = int(pacman.x), int(pacman.y)
# 小豆子
for i, (x, y) in enumerate(dots[:]):
if x == pacman_cell_x and y == pacman_cell_y:
dots.remove((x, y))
pacman.score += 10
# 大豆子
for i, (x, y) in enumerate(big_dots[:]):
if x == pacman_cell_x and y == pacman_cell_y:
big_dots.remove((x, y))
pacman.score += 50
# 幽灵进入惊恐状态
frightened_timer = 300 # 约5秒
for ghost in ghosts:
ghost.frightened = True
这种碰撞检测基于网格位置:如果吃豆人当前所在的网格单元与豆子的位置相同,则认为吃豆人吃到了豆子。
9. 游戏状态管理
9.1 游戏状态定义
我们的游戏有几种不同的状态:
- 游戏运行中
- 游戏暂停
- 游戏结束(玩家胜利或失败)
我们使用布尔变量game_over
来跟踪游戏是否结束:
python
running = True # 游戏程序是否继续运行
game_over = False # 当前游戏是否结束
9.2 游戏状态切换
游戏状态可以通过几种方式切换:
- 游戏结束:当玩家失去所有生命或吃掉所有豆子时
python
# 检查游戏胜利
if len(dots) == 0 and len(big_dots) == 0:
game_over = True
# 或者当玩家失去所有生命时
if pacman.lives <= 0:
game_over = True
- 重新开始游戏:当游戏结束后按下回车键
python
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN and game_over:
# 重新开始游戏
pacman, grid, dots, big_dots, ghosts = init_game()
game_over = False
- 退出游戏:当玩家关闭游戏窗口时
python
if event.type == pygame.QUIT:
running = False
9.3 幽灵状态管理
幽灵有两种状态:正常状态和"惊吓"状态。我们使用frightened
属性和一个计时器来管理这种状态:
python
# 处理幽灵惊恐状态
if frightened_timer > 0:
frightened_timer -= 1
if frightened_timer == 0:
for ghost in ghosts:
ghost.frightened = False
当吃豆人吃到能量豆时,所有幽灵进入"惊吓"状态,并设置一个计时器(300帧,约5秒)。每帧都会减少计时器的值,当计时器归零时,所有幽灵回到正常状态。
10. 游戏UI与视觉效果
10.1 分数和生命值显示
我们在游戏界面顶部显示玩家的分数和剩余生命值:
python
# 绘制分数和生命值
font = pygame.font.SysFont(None, 36)
score_text = font.render(f"分数: {pacman.score}", True, WHITE)
lives_text = font.render(f"生命: {pacman.lives}", True, WHITE)
win.blit(score_text, (10, 10))
win.blit(lives_text, (WIDTH - 110, 10))
这段代码创建两个文本Surface对象,分别显示分数和生命值,然后将它们绘制在游戏窗口的顶部。
10.2 游戏结束画面
当游戏结束时,我们显示一个游戏结束画面,告诉玩家游戏结果并提供重新开始的提示:
python
# 游戏结束显示
if game_over:
font = pygame.font.SysFont(None, 72)
if pacman.lives <= 0:
game_over_text = font.render("游戏结束!", True, RED)
else:
game_over_text = font.render("恭喜你赢了!", True, YELLOW)
restart_text = font.render("按 Enter 重新开始", True, WHITE)
win.blit(game_over_text, (WIDTH // 2 - game_over_text.get_width() // 2, HEIGHT // 2 - 50))
win.blit(restart_text, (WIDTH // 2 - restart_text.get_width() // 2, HEIGHT // 2 + 50))
根据游戏结束的原因(玩家胜利或失败),显示不同的信息。游戏胜利时显示黄色的"恭喜你赢了!",失败时显示红色的"游戏结束!"。同时,提示玩家按Enter键重新开始游戏。
10.3 角色动画
为了增加游戏的视觉吸引力,我们为吃豆人和幽灵添加了简单的动画效果:
- 吃豆人的嘴巴动画:吃豆人的嘴巴会周期性地开合,创造出经典的"吃"的效果
python
# 更新嘴巴动画
self.mouth_counter += 1
if self.mouth_counter >= 10:
self.mouth_counter = 0
self.mouth_open = not self.mouth_open
- 幽灵的眼球动画:幽灵的眼球会根据移动方向改变位置
python
# 根据方向移动眼球
pupil_offset_x, pupil_offset_y = 0, 0
if self.direction == "left":
pupil_offset_x = -pupil_radius // 2
elif self.direction == "right":
pupil_offset_x = pupil_radius // 2
elif self.direction == "up":
pupil_offset_y = -pupil_radius // 2
elif self.direction == "down":
pupil_offset_y = pupil_radius // 2
这些动画效果虽小,但大大增加了游戏的视觉体验和角色的生动感。
11. 游戏主循环与事件处理
11.1 游戏主循环结构
游戏主循环是游戏程序的核心,负责处理输入、更新游戏状态和渲染画面。我们的主循环结构如下:
python
def main():
clock = pygame.time.Clock()
pacman, grid, dots, big_dots, ghosts = init_game()
frightened_timer = 0
running = True
game_over = False
while running:
clock.tick(60) # 限制帧率为60FPS
# 事件处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN and game_over:
# 重新开始游戏
pacman, grid, dots, big_dots, ghosts = init_game()
game_over = False
if not game_over:
# 处理输入
keys = pygame.key.get_pressed()
# 处理吃豆人移动
if keys[pygame.K_RIGHT]:
pacman.move("right", grid)
elif keys[pygame.K_LEFT]:
pacman.move("left", grid)
elif keys[pygame.K_UP]:
pacman.move("up", grid)
elif keys[pygame.K_DOWN]:
pacman.move("down", grid)
# 更新游戏状态
# 幽灵移动
for ghost in ghosts:
ghost.move(grid, pacman)
# 检查吃豆子
# ...
# 检查碰撞
# ...
# 检查游戏胜利
# ...
# 渲染
draw_game(win, pacman, grid, dots, big_dots, ghosts)
# 游戏结束显示
# ...
pygame.quit()
这个结构遵循了典型的游戏循环模式:
- 限制帧率
- 处理事件
- 根据输入更新游戏状态
- 渲染当前游戏状态
- 处理特殊情况(如游戏结束)
- 如此循环直到游戏退出
11.2 事件处理
Pygame使用事件队列来处理用户输入和其他事件。我们的事件处理代码如下:
python
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN and game_over:
# 重新开始游戏
pacman, grid, dots, big_dots, ghosts = init_game()
game_over = False
这段代码处理两种事件:
pygame.QUIT
事件(当玩家关闭游戏窗口时触发)pygame.KEYDOWN
事件,特别是检查游戏结束时按下回车键重新开始游戏
11.3 键盘输入处理
除了事件队列外,我们还使用pygame.key.get_pressed()
函数来检测当前按下的键,这适用于需要持续检测的输入,如方向键控制:
python
keys = pygame.key.get_pressed()
# 处理吃豆人移动
if keys[pygame.K_RIGHT]:
pacman.move("right", grid)
elif keys[pygame.K_LEFT]:
pacman.move("left", grid)
elif keys[pygame.K_UP]:
pacman.move("up", grid)
elif keys[pygame.K_DOWN]:
pacman.move("down", grid)
这段代码检查方向键(上、下、左、右)是否被按下,并据此移动吃豆人。使用elif
确保每帧只处理一个方向,优先级从右到左再到上再到下。
12. 代码优化与性能改进
12.1 碰撞检测优化
在我们的游戏中,碰撞检测是一个频繁执行的操作。为了提高性能,我们可以采取以下优化措施:
- 减少不必要的计算:只在距离较近时才进行精确的碰撞检测
python
# 优化前
distance = math.sqrt((pacman.x - ghost.x) ** 2 + (pacman.y - ghost.y) ** 2)
if distance < 0.7:
# 处理碰撞
# 优化后
dx = pacman.x - ghost.x
dy = pacman.y - ghost.y
# 避免开方运算,直接比较平方
if dx*dx + dy*dy < 0.7*0.7:
# 处理碰撞
- 使用网格位置进行初步筛选:对于豆子的碰撞检测,我们只检查吃豆人当前所在的网格单元,而不是所有豆子
python
# 优化前
for dot in dots:
if collision(pacman, dot):
# 处理碰撞
# 优化后
pacman_cell_x, pacman_cell_y = int(pacman.x), int(pacman.y)
for x, y in dots[:]:
if x == pacman_cell_x and y == pacman_cell_y:
# 处理碰撞
12.2 渲染优化
渲染是游戏中另一个性能关键点。我们可以通过以下方式优化渲染过程:
- 只渲染可见区域:如果游戏地图非常大,只渲染当前屏幕可见的部分
python
# 计算可见区域
view_left = max(0, int(pacman.x) - VIEW_RANGE)
view_right = min(GRID_WIDTH, int(pacman.x) + VIEW_RANGE + 1)
view_top = max(0, int(pacman.y) - VIEW_RANGE)
view_bottom = min(GRID_HEIGHT, int(pacman.y) + VIEW_RANGE + 1)
# 只渲染可见区域
for y in range(view_top, view_bottom):
for x in range(view_left, view_right):
# 渲染网格[y][x]
- 减少渲染调用:合并相同类型的渲染操作,减少API调用次数
python
# 优化前
for dot in dots:
draw_dot(dot)
# 优化后
# 创建一个Surface来预渲染所有豆子
dots_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
for dot in dots:
draw_dot(dots_surface, dot)
# 一次性将所有豆子绘制到屏幕上
win.blit(dots_surface, (0, 0))
12.3 内存管理
良好的内存管理对于游戏性能也很重要:
- 避免内存泄漏:确保不再需要的对象被正确清理
python
# 游戏退出时清理资源
pygame.quit()
- 重用对象:避免频繁创建和销毁临时对象
python
# 不好的做法:每次创建新字体对象
font = pygame.font.SysFont(None, 36)
score_text = font.render(f"分数: {pacman.score}", True, WHITE)
# 更好的做法:只创建一次字体对象,重复使用
# 在游戏初始化时
self.font = pygame.font.SysFont(None, 36)
# 在渲染时
score_text = self.font.render(f"分数: {pacman.score}", True, WHITE)
- 使用适当的数据结构:选择适合操作类型的数据结构
python
# 对于经常需要检查成员关系的集合,使用set而不是list
dots = set((x, y) for x in range(GRID_WIDTH) for y in range(GRID_HEIGHT) if grid[y][x] == 0)
# 检查成员关系
if (pacman_cell_x, pacman_cell_y) in dots:
dots.remove((pacman_cell_x, pacman_cell_y))
pacman.score += 10
13. 完整代码
以下是我们吃豆人游戏的完整代码:
import pygame
import random
import math
# 初始化 Pygame
pygame.init()
# 设置游戏窗口
WIDTH, HEIGHT = 800, 600
win = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Pac-Man 游戏")
# 颜色定义
BLACK = (0, 0, 0)
YELLOW = (255, 255, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
PINK = (255, 192, 203)
ORANGE = (255, 165, 0)
CYAN = (0, 255, 255)
# 游戏参数
CELL_SIZE = 30
GRID_WIDTH = WIDTH // CELL_SIZE
GRID_HEIGHT = HEIGHT // CELL_SIZE
# Pac-Man 参数
class PacMan:
def __init__(self):
self.x = GRID_WIDTH // 2
self.y = GRID_HEIGHT // 2
self.direction = "right"
self.speed = 0.1
self.mouth_open = True
self.mouth_counter = 0
self.score = 0
self.lives = 3
def move(self, direction, grid):
new_x, new_y = self.x, self.y
if direction == "right":
new_x += self.speed
elif direction == "left":
new_x -= self.speed
elif direction == "up":
new_y -= self.speed
elif direction == "down":
new_y += self.speed
# 检查是否能移动(不与墙碰撞)
cell_x, cell_y = int(new_x), int(new_y)
if 0 <= cell_x < GRID_WIDTH and 0 <= cell_y < GRID_HEIGHT and grid[cell_y][cell_x] != 1:
self.x, self.y = new_x, new_y
self.direction = direction
# 更新嘴巴动画
self.mouth_counter += 1
if self.mouth_counter >= 10:
self.mouth_counter = 0
self.mouth_open = not self.mouth_open
def draw(self, win):
x = int(self.x * CELL_SIZE + CELL_SIZE // 2)
y = int(self.y * CELL_SIZE + CELL_SIZE // 2)
radius = CELL_SIZE // 2
# 绘制 Pac-Man
if self.mouth_open:
# 嘴巴张开
if self.direction == "right":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x + radius, y - radius // 2),
(x + radius, y + radius // 2)])
elif self.direction == "left":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x - radius, y - radius // 2),
(x - radius, y + radius // 2)])
elif self.direction == "up":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x - radius // 2, y - radius),
(x + radius // 2, y - radius)])
elif self.direction == "down":
pygame.draw.circle(win, YELLOW, (x, y), radius)
pygame.draw.polygon(win, BLACK, [(x, y),
(x - radius // 2, y + radius),
(x + radius // 2, y + radius)])
else:
# 嘴巴闭合
pygame.draw.circle(win, YELLOW, (x, y), radius)
# 幽灵参数
class Ghost:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
self.direction = random.choice(["right", "left", "up", "down"])
self.speed = 0.05
self.frightened = False
def move(self, grid, pacman):
directions = ["right", "left", "up", "down"]
# 简单的 AI - 有 80% 概率朝向 Pac-Man,20% 概率随机移动
if random.random() < 0.8 and not self.frightened:
# 寻找 Pac-Man 的方向
if pacman.x > self.x and "right" in directions:
self.direction = "right"
elif pacman.x < self.x and "left" in directions:
self.direction = "left"
elif pacman.y > self.y and "down" in directions:
self.direction = "down"
elif pacman.y < self.y and "up" in directions:
self.direction = "up"
else:
# 随机选择方向
self.direction = random.choice(directions)
# 移动幽灵
new_x, new_y = self.x, self.y
if self.direction == "right":
new_x += self.speed
elif self.direction == "left":
new_x -= self.speed
elif self.direction == "up":
new_y -= self.speed
elif self.direction == "down":
new_y += self.speed
# 检查是否能移动(不与墙碰撞)
cell_x, cell_y = int(new_x), int(new_y)
if 0 <= cell_x < GRID_WIDTH and 0 <= cell_y < GRID_HEIGHT and grid[cell_y][cell_x] != 1:
self.x, self.y = new_x, new_y
else:
# 如果碰到墙,选择新方向
self.direction = random.choice(directions)
def draw(self, win):
x = int(self.x * CELL_SIZE + CELL_SIZE // 2)
y = int(self.y * CELL_SIZE + CELL_SIZE // 2)
radius = CELL_SIZE // 2
# 绘制幽灵主体
color = BLUE if self.frightened else self.color
# 绘制幽灵的半圆顶部
pygame.draw.circle(win, color, (x, y - radius // 3), radius)
# 绘制幽灵的矩形底部
pygame.draw.rect(win, color, (x - radius, y - radius // 3, radius * 2, radius))
# 绘制幽灵底部的波浪形状
wave_height = radius // 3
pygame.draw.polygon(win, color, [
(x - radius, y + radius * 2 // 3), # 左上角
(x - radius * 2 // 3, y + radius * 2 // 3 - wave_height), # 第一个波谷
(x - radius // 3, y + radius * 2 // 3), # 第一个波峰
(x, y + radius * 2 // 3 - wave_height), # 第二个波谷
(x + radius // 3, y + radius * 2 // 3), # 第二个波峰
(x + radius * 2 // 3, y + radius * 2 // 3 - wave_height), # 第三个波谷
(x + radius, y + radius * 2 // 3), # 右上角
(x + radius, y + radius * 2 // 3 - radius), # 右下角
(x - radius, y + radius * 2 // 3 - radius), # 左下角
])
# 绘制眼睛 (白色部分)
eye_radius = radius // 3
left_eye_x = x - radius // 2
right_eye_x = x + radius // 2
eye_y = y - radius // 3
pygame.draw.circle(win, WHITE, (left_eye_x, eye_y), eye_radius)
pygame.draw.circle(win, WHITE, (right_eye_x, eye_y), eye_radius)
# 绘制眼球 (瞳孔)
pupil_radius = eye_radius // 2
# 根据方向移动眼球
pupil_offset_x, pupil_offset_y = 0, 0
if self.direction == "left":
pupil_offset_x = -pupil_radius // 2
elif self.direction == "right":
pupil_offset_x = pupil_radius // 2
elif self.direction == "up":
pupil_offset_y = -pupil_radius // 2
elif self.direction == "down":
pupil_offset_y = pupil_radius // 2
pygame.draw.circle(win, BLACK, (left_eye_x + pupil_offset_x, eye_y + pupil_offset_y), pupil_radius)
pygame.draw.circle(win, BLACK, (right_eye_x + pupil_offset_x, eye_y + pupil_offset_y), pupil_radius)
# 创建迷宫
def create_maze():
grid = [[0 for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
# 添加边界
for x in range(GRID_WIDTH):
grid[0][x] = 1
grid[GRID_HEIGHT - 1][x] = 1
for y in range(GRID_HEIGHT):
grid[y][0] = 1
grid[y][GRID_WIDTH - 1] = 1
# 添加随机墙壁
for _ in range(GRID_WIDTH * GRID_HEIGHT // 10):
x = random.randint(1, GRID_WIDTH - 2)
y = random.randint(1, GRID_HEIGHT - 2)
grid[y][x] = 1
# 确保 Pac-Man 的起始位置是空的
grid[GRID_HEIGHT // 2][GRID_WIDTH // 2] = 0
return grid
# 创建豆子
def create_dots(grid):
dots = []
big_dots = []
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if grid[y][x] == 0 and (x != GRID_WIDTH // 2 or y != GRID_HEIGHT // 2): # 避免在Pac-Man起始位置放置豆子
# 15% 概率创建大豆子,85% 概率创建小豆子
if random.random() < 0.15:
big_dots.append((x, y))
else:
dots.append((x, y))
return dots, big_dots
# 游戏初始化
def init_game():
pacman = PacMan()
grid = create_maze()
dots, big_dots = create_dots(grid)
ghosts = [
Ghost(1, 1, RED),
Ghost(GRID_WIDTH - 2, 1, PINK),
Ghost(1, GRID_HEIGHT - 2, ORANGE),
Ghost(GRID_WIDTH - 2, GRID_HEIGHT - 2, CYAN)
]
return pacman, grid, dots, big_dots, ghosts
# 绘制游戏
def draw_game(win, pacman, grid, dots, big_dots, ghosts):
win.fill(BLACK)
# 绘制迷宫
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if grid[y][x] == 1:
pygame.draw.rect(win, BLUE, [x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE])
# 绘制小豆子
for x, y in dots:
pygame.draw.circle(win, WHITE, (x * CELL_SIZE + CELL_SIZE // 2, y * CELL_SIZE + CELL_SIZE // 2),
CELL_SIZE // 10)
# 绘制大豆子
for x, y in big_dots:
pygame.draw.circle(win, WHITE, (x * CELL_SIZE + CELL_SIZE // 2, y * CELL_SIZE + CELL_SIZE // 2), CELL_SIZE // 5)
# 绘制 Pac-Man
pacman.draw(win)
# 绘制幽灵
for ghost in ghosts:
ghost.draw(win)
# 绘制分数和生命值
font = pygame.font.SysFont(None, 36)
score_text = font.render(f"分数: {pacman.score}", True, WHITE)
lives_text = font.render(f"生命: {pacman.lives}", True, WHITE)
win.blit(score_text, (10, 10))
win.blit(lives_text, (WIDTH - 110, 10))
pygame.display.update()
# 主游戏循环
def main():
clock = pygame.time.Clock()
pacman, grid, dots, big_dots, ghosts = init_game()
frightened_timer = 0
running = True
game_over = False
while running:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN and game_over:
# 重新开始游戏
pacman, grid, dots, big_dots, ghosts = init_game()
game_over = False
if not game_over:
keys = pygame.key.get_pressed()
# 处理 Pac-Man 移动
if keys[pygame.K_RIGHT]:
pacman.move("right", grid)
elif keys[pygame.K_LEFT]:
pacman.move("left", grid)
elif keys[pygame.K_UP]:
pacman.move("up", grid)
elif keys[pygame.K_DOWN]:
pacman.move("down", grid)
# 处理幽灵移动
for ghost in ghosts:
ghost.move(grid, pacman)
# 检查吃豆子
pacman_cell_x, pacman_cell_y = int(pacman.x), int(pacman.y)
# 小豆子
for i, (x, y) in enumerate(dots[:]):
if x == pacman_cell_x and y == pacman_cell_y:
dots.remove((x, y))
pacman.score += 10
# 大豆子
for i, (x, y) in enumerate(big_dots[:]):
if x == pacman_cell_x and y == pacman_cell_y:
big_dots.remove((x, y))
pacman.score += 50
# 幽灵进入惊恐状态
frightened_timer = 300 # 约5秒
for ghost in ghosts:
ghost.frightened = True
# 处理幽灵惊恐状态
if frightened_timer > 0:
frightened_timer -= 1
if frightened_timer == 0:
for ghost in ghosts:
ghost.frightened = False
# 检查与幽灵碰撞
for ghost in ghosts:
distance = math.sqrt((pacman.x - ghost.x) ** 2 + (pacman.y - ghost.y) ** 2)
if distance < 0.7: # 碰撞阈值
if ghost.frightened:
# Pac-Man 吃掉幽灵
ghost.x, ghost.y = random.randint(1, GRID_WIDTH - 2), random.randint(1, GRID_HEIGHT - 2)
ghost.frightened = False
pacman.score += 200
else:
# 幽灵吃掉 Pac-Man
pacman.lives -= 1
pacman.x, pacman.y = GRID_WIDTH // 2, GRID_HEIGHT // 2
if pacman.lives <= 0:
game_over = True
# 检查游戏胜利
if len(dots) == 0 and len(big_dots) == 0:
game_over = True
# 绘制游戏
draw_game(win, pacman, grid, dots, big_dots, ghosts)
# 游戏结束显示
if game_over:
font = pygame.font.SysFont(None, 72)
if pacman.lives <= 0:
game_over_text = font.render("游戏结束!", True, RED)
else:
game_over_text = font.render("恭喜你赢了!", True, YELLOW)
restart_text = font.render("按 Enter 重新开始", True, WHITE)
win.blit(game_over_text, (WIDTH // 2 - game_over_text.get_width() // 2, HEIGHT // 2 - 50))
win.blit(restart_text, (WIDTH // 2 - restart_text.get_width() // 2, HEIGHT // 2 + 50))
pygame.display.update()
pygame.quit()
if __name__ == "__main__":
main()