摘要
本章将深入探讨2D平台跳跃游戏的物理架构核心 ,重点剖析离散碰撞检测与响应(Discrete Collision Detection and Response) 、视口变换系统(Viewport Transformation)以及 分层有限状态机(Hierarchical Finite State Machine)的工程实现。与前述章节的街机式射击游戏不同,平台跳跃类型对物理一致性的要求显著提升------玩家期望通过可预测的重力、惯性和碰撞反馈建立起对虚拟世界的"物理直觉"。
我们将通过实现一个具备完整物理参数实时调节功能的平台跳跃原型,深入探讨**AABB碰撞解析(AABB Collision Resolution)中的 分离轴定理(Separating Axis Theorem)简化应用、摄像机 Clamp 算法的边界约束数学,以及多跳机制(Multi-Jump Mechanism)**的状态守卫设计。通过本项目,你将掌握构建现代2D平台游戏(如《Celeste》、《Hollow Knight》等独立游戏标杆)所需的核心物理架构与摄像机系统知识。
23.1 游戏架构与核心系统理论
23.1.1 2D平台物理模拟的理论基础
离散物理仿真与时间步进
在游戏物理引擎中,我们面临连续物理现实 与离散计算步骤的根本矛盾。本章实现采用**固定时间步长(Fixed Time Step)**的欧拉积分(Euler Integration)方法,对玩家运动学状态进行数值积分:
v t + 1 = v t + a ⋅ Δ t v_{t+1} = v_t + a \cdot \Delta t vt+1=vt+a⋅Δt
p t + 1 = p t + v t + 1 ⋅ Δ t p_{t+1} = p_t + v_{t+1} \cdot \Delta t pt+1=pt+vt+1⋅Δt
其中重力加速度 a a a 由滑动条动态控制(0.1至2.0范围),这种**参数外部化(Parameter Externalization)**设计允许在运行时调整物理手感,而无需重新编译。这种设计在游戏原型阶段(Prototyping Phase)尤为重要------设计师可通过实时调节寻找"跳跃手感(Jump Feel)"的最佳数值。
值得注意的是,我们在垂直速度应用中采用了int()强制转换:
python
self.rect.y += int(self.velocity_y)
这实际上引入了量化误差(Quantization Error) ,将浮点物理空间映射到整数像素空间。虽然在本项目尺度下影响有限,但在高速运动场景(如《Sonic》系列)中,这种离散化可能导致隧道效应(Tunneling) ------物体穿透薄平台。工业级解决方案需引入**连续碰撞检测(CCD, Continuous Collision Detection)或子步进(Sub-stepping)**技术。
分离轴定理与碰撞响应
平台碰撞检测采用**轴对齐包围盒(AABB, Axis-Aligned Bounding Box)相交测试,这是分离轴定理(SAT, Separating Axis Theorem)**在2D正交坐标系下的特例。定理指出:若两个凸多边形不相交,则必然存在至少一个轴(在此简化为X轴与Y轴),使得两物体在该轴上的投影不重叠。
碰撞响应分为阶段式解析(Phase-separated Resolution):
- 水平阶段 :先执行水平位移,检测并解决水平穿透(通过
rect.right = platform.rect.left等硬约束) - 垂直阶段:再执行垂直位移,检测并解决垂直穿透
这种分离轴处理(Per-axis Resolution)防止了斜向碰撞时的"滑动粘滞"问题。当玩家垂直速度velocity_y > 0(下落状态)且碰撞发生时,我们判定为着陆(Landing),重置跳跃计数器与接地状态标志。
多跳机制的状态守卫
**双跳(Double Jump)实现引入了 状态守卫(State Guard)**模式。跳跃触发条件不仅检查按键事件,更维护jump_count计数器与DOUBLE_JUMP_LIMIT上限:
python
if self.on_ground or self.jump_count < DOUBLE_JUMP_LIMIT:
if self.velocity_y > -1.0: # 速度阈值守卫
self.velocity_y = jump_strength
self.jump_count += 1
其中velocity_y > -1.0的判定是防连发守卫(Anti-spam Guard) ,防止玩家按住空格键时物理引擎每帧都判定为新的跳跃请求。这种**边缘触发(Edge-triggered)与电平触发(Level-triggered)**的混合逻辑,是平台游戏输入处理的标准实践。
23.1.2 摄像机系统与视口变换
世界-屏幕坐标变换
摄像机系统实现了**观察变换(View Transform)的2D简化版本。游戏世界坐标系(World Space)尺寸为2400×900像素,而屏幕空间(Screen Space)仅为800×600,这种超界世界(Oversized World)**设计 necessitates 视口管理。
摄像机核心算法为居中跟随(Centered Following)结合硬边界约束(Hard Boundary Constraints):
c a m x = − p l a y e r x + s c r e e n _ w i d t h 2 cam_x = -player_x + \frac{screen\_width}{2} camx=−playerx+2screen_width
c a m y = − p l a y e r y + s c r e e n _ h e i g h t 2 cam_y = -player_y + \frac{screen\_height}{2} camy=−playery+2screen_height
随后执行Clamp操作:
c a m x = min ( 0 , max ( c a m x , − ( w o r l d _ w i d t h − s c r e e n _ w i d t h ) ) ) cam_x = \min(0, \max(cam_x, -(world\_width - screen\_width))) camx=min(0,max(camx,−(world_width−screen_width)))
这确保摄像机不会移出世界边界,避免露出"世界之外的虚空(The Void Beyond the World)"。在矩阵代数视角下,这等价于一个**平移变换矩阵(Translation Matrix)与裁剪矩阵(Clipping Matrix)**的级联。
视差滚动与图层管理
虽然本实现未引入视差滚动(Parallax Scrolling) ,但Camera.apply(entity)方法的设计预留了扩展接口。通过为不同实体分配不同的视差因子(Parallax Factor):
python
# 理论扩展(非本章代码)
def apply_parallax(self, entity, factor):
return entity.rect.move(self.camera.topleft[0] * factor,
self.camera.topleft[1] * factor)
可实现前景/背景的深度差异感,这是2D平台游戏营造空间层次感的关键技术。
23.1.3 分层状态机与UI架构
游戏状态的分层管理
本实现采用**分层有限状态机(Hierarchical FSM)**管理应用生命周期,顶层状态包括:
- Menu(菜单态):初始状态,等待玩家启动
- Playing(游戏态):物理模拟与输入处理激活
- GameOver(失败态):玩家死亡或跌落,显示重启提示
- Win(胜利态):到达终点,显示通关提示
状态转换通过self.state字符串变量控制,虽为简化实现,但展示了**状态隔离(State Isolation)原则------update()与draw()方法在入口处检查状态,仅执行当前状态合法的操作。这种状态守卫(State Guard)**模式防止了非法状态转换(如在死亡后继续移动)。
滑动条控件的人机交互设计
滑动条(Slider)控件的实现引入了 直接操作(Direct Manipulation)的交互范式。与按钮等离散控件不同,滑动条支持连续值调节(Continuous Value Adjustment),适用于物理参数这种需精细微调的数值。
其实现包含三个交互状态:
- 空闲态(Idle):仅渲染轨道与滑块位置
- 悬停态(Hover):检测鼠标与滑块几何距离(圆形碰撞检测)
- 拖拽态(Dragging):锁定鼠标X轴偏移,实时重算归一化值(0.0-1.0)并映射到物理区间
这种三态交互模型(Three-state Interaction Model)是GUI控件设计的经典模式,确保了操作的 即时反馈(Immediate Feedback)与撤销友好性(Undo-friendly,松开即停止调节)。
23.2 完整代码
以下代码完整实现了上述理论架构,包括基于SAT的分离轴碰撞解析、视口变换摄像机、分层状态机以及实时物理参数调节系统。
(注:以下代码与原始版本完全一致,未做任何修改)
python
import pygame
import sys
import random
# 初始化 pygame 库,这是使用 pygame 功能的前提
pygame.init()
# =========================
# 常量定义
# =========================
# 屏幕宽度和高度,定义了游戏窗口的尺寸
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# 帧率,控制游戏每秒更新的次数,影响游戏流畅度
FPS = 60
# 游戏世界(地图)的宽度和高度,可以比屏幕大,用于实现视差滚动效果
WORLD_WIDTH = 2400
WORLD_HEIGHT = 900
# 默认的物理参数
DEFAULT_GRAVITY = 0.8 # 默认重力加速度,值越大物体下落越快
DEFAULT_JUMP_STRENGTH = -15 # 默认跳跃初速度,负值表示向上,值越小跳得越高
MOVE_SPEED = 5 # 玩家水平移动速度
DOUBLE_JUMP_LIMIT = 2 # 双跳允许的最大次数(这里设置为2,表示可以跳一次,再跳一次)
# =========================
# 颜色定义
# =========================
# 使用 RGB 元组表示颜色,方便在绘制时使用
SKY_BLUE = (135, 206, 235) # 天蓝色
GREEN = (34, 139, 34) # 绿色
BROWN = (139, 69, 19) # 棕色
YELLOW = (255, 215, 0) # 黄色
RED = (255, 0, 0) # 红色
WHITE = (255, 255, 255) # 白色
BLACK = (0, 0, 0) # 黑色
PURPLE = (160, 32, 240) # 紫色
ORANGE = (255, 140, 0) # 橙色
GRAY = (120, 120, 120) # 灰色
DARK_GRAY = (60, 60, 60) # 深灰色
BLUE = (70, 130, 180) # 蓝色
class Slider:
"""
滑动条类
用于在游戏中手动调整重力和跳跃力等物理参数。
"""
def __init__(self, x, y, width, min_value, max_value, value, label):
"""
初始化滑动条
:param x: 滑动条的起始 x 坐标
:param y: 滑动条的起始 y 坐标
:param width: 滑动条的长度
:param min_value: 滑动条允许的最小值
:param max_value: 滑动条允许的最大值
:param value: 滑动条的初始值
:param label: 显示在滑动条旁边的标签文本
"""
# 滑动条的矩形区域,用于绘制和碰撞检测
self.rect = pygame.Rect(x, y, width, 20)
self.min_value = min_value
self.max_value = max_value
self.value = value
self.label = label
self.dragging = False # 标记当前是否正在拖动滑块
self.knob_radius = 10 # 滑块(圆点)的半径
def handle_event(self, event):
"""
处理用户输入事件,例如鼠标点击和拖动。
:param event: Pygame 事件对象
"""
# 鼠标按下事件
if event.type == pygame.MOUSEBUTTONDOWN:
mx, my = event.pos # 获取鼠标点击的坐标
# 计算滑块中心的 x 坐标
knob_x = self.get_knob_x()
knob_y = self.rect.centery
# 判断鼠标点击位置是否在滑块圆圈内
if (mx - knob_x) ** 2 + (my - knob_y) ** 2 <= self.knob_radius ** 2:
self.dragging = True # 如果在,则标记为正在拖动
# 鼠标松开事件
if event.type == pygame.MOUSEBUTTONUP:
self.dragging = False # 结束拖动
# 鼠标移动事件,并且当前正在拖动
if event.type == pygame.MOUSEMOTION and self.dragging:
mx, my = event.pos # 获取鼠标当前位置
# 计算鼠标位置相对于滑动条的比例 (0.0 到 1.0)
ratio = (mx - self.rect.x) / self.rect.width
# 将比例限制在 0 到 1 之间,防止超出范围
ratio = max(0, min(1, ratio))
# 根据比例计算当前值
self.value = self.min_value + ratio * (self.max_value - self.min_value)
def get_knob_x(self):
"""
根据当前值计算滑块的 x 坐标。
:return: 滑块的 x 坐标
"""
# 计算当前值在整个范围内的比例
ratio = (self.value - self.min_value) / (self.max_value - self.min_value)
# 根据比例计算滑块在滑动条上的 x 坐标
return int(self.rect.x + ratio * self.rect.width)
def draw(self, surface, font):
"""
在指定的 surface 上绘制滑动条。
:param surface: 绘制的目标 surface (通常是游戏屏幕)
:param font: 用于渲染文本的字体对象
"""
# 画滑动条的背景轨道(深灰色)
pygame.draw.rect(surface, DARK_GRAY, self.rect, border_radius=10)
# 计算滑块的 x 坐标
knob_x = self.get_knob_x()
# 定义已填充部分的矩形区域(从滑动条起点到滑块位置)
fill_rect = pygame.Rect(self.rect.x, self.rect.y, knob_x - self.rect.x, self.rect.height)
# 画已填充部分(蓝色)
pygame.draw.rect(surface, BLUE, fill_rect, border_radius=10)
# 画滑块(白色圆圈)
pygame.draw.circle(surface, WHITE, (knob_x, self.rect.centery), self.knob_radius)
# 渲染并显示滑动条的标签和当前值
text = font.render(f"{self.label}: {self.value:.2f}", True, WHITE)
surface.blit(text, (self.rect.x, self.rect.y - 28))
class Player(pygame.sprite.Sprite):
"""
玩家类
继承自 pygame.sprite.Sprite,代表游戏中的玩家角色。
支持左右移动、跳跃、双跳、爬梯子以及重力效果。
"""
def __init__(self, x, y):
"""
初始化玩家
:param x: 玩家的初始 x 坐标
:param y: 玩家的初始 y 坐标
"""
super().__init__() # 调用父类 Sprite 的初始化方法
# 创建玩家的图像(一个红色矩形)
self.image = pygame.Surface((30, 50))
self.image.fill(RED)
# 获取玩家的矩形区域,用于定位和碰撞检测
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
# 玩家的速度分量
self.velocity_x = 0
self.velocity_y = 0
# 玩家状态标记
self.on_ground = False # 是否在地面上
self.jump_count = 0 # 当前的跳跃次数(用于双跳)
self.on_ladder = False # 是否在梯子上
def update(self, platforms, ladders, gravity, jump_strength):
"""
更新玩家的状态和位置。
:param platforms: 所有平台的 sprite 组
:param ladders: 所有梯子的 sprite 组
:param gravity: 当前的重力值
:param jump_strength: 当前的跳跃初速度值
"""
# 获取当前按键状态
keys = pygame.key.get_pressed()
# =========================
# 水平移动逻辑
# =========================
self.velocity_x = 0 # 默认水平速度为 0
if keys[pygame.K_LEFT]:
self.velocity_x = -MOVE_SPEED # 按左箭头向左移动
if keys[pygame.K_RIGHT]:
self.velocity_x = MOVE_SPEED # 按右箭头向右移动
# =========================
# 梯子交互逻辑
# =========================
# 判断玩家是否与任何一个梯子发生碰撞
self.on_ladder = pygame.sprite.spritecollideany(self, ladders) is not None
# 如果在梯子上
if self.on_ladder:
# 梯子上可以上下移动,速度由按键决定
if keys[pygame.K_UP]:
self.velocity_y = -MOVE_SPEED
elif keys[pygame.K_DOWN]:
self.velocity_y = MOVE_SPEED
else:
self.velocity_y = 0 # 没有按上下键,垂直速度为 0
else:
# 如果不在梯子上,则应用重力
self.velocity_y += gravity
# =========================
# 跳跃逻辑
# =========================
if keys[pygame.K_SPACE]:
# 满足以下任一条件可以跳跃:
# 1. 在地面上 (on_ground is True)
# 2. 跳跃次数小于双跳限制 (jump_count < DOUBLE_JUMP_LIMIT)
if self.on_ground or self.jump_count < DOUBLE_JUMP_LIMIT:
# 为了避免按住空格键连续触发跳跃,这里增加一个判断:
# 只有当玩家垂直速度接近 0 或向上时(即刚落地或在空中但未快速下落),才允许跳跃。
# 这样可以防止玩家在快速下落时通过按住空格无限跳跃。
if self.velocity_y > -1.0:
self.velocity_y = jump_strength # 设置向上的初速度
self.on_ground = False # 跳跃后就不在地面上了
self.jump_count += 1 # 跳跃次数加一
# =========================
# 应用移动和碰撞检测
# =========================
# 水平移动
self.rect.x += self.velocity_x
# 检测水平方向的碰撞,并根据碰撞结果调整位置
self.check_collision(platforms, "horizontal")
# 垂直移动
# 注意:这里将 velocity_y 强制转换为整数,因为 rect 的坐标必须是整数
self.rect.y += int(self.velocity_y)
self.on_ground = False # 每次垂直移动前,先假设不在地面上
# 检测垂直方向的碰撞,并根据碰撞结果调整位置
self.check_collision(platforms, "vertical")
def check_collision(self, platforms, direction):
"""
检查玩家与平台的碰撞,并处理碰撞后的位置调整。
:param platforms: 所有平台的 sprite 组
:param direction: 碰撞检测的方向 ("horizontal" 或 "vertical")
"""
# 使用 spritecollide 函数检测玩家与平台组的碰撞
# 第三个参数 False 表示碰撞后不移除平台精灵
hits = pygame.sprite.spritecollide(self, platforms, False)
# 遍历所有发生碰撞的平台
for platform in hits:
if direction == "horizontal":
# 水平方向碰撞处理
if self.velocity_x > 0:
# 如果玩家向右移动时碰撞,则将玩家的右边缘放在平台左边缘的左边
self.rect.right = platform.rect.left
elif self.velocity_x < 0:
# 如果玩家向左移动时碰撞,则将玩家的左边缘放在平台右边缘的右边
self.rect.left = platform.rect.right
elif direction == "vertical":
# 垂直方向碰撞处理
if self.velocity_y > 0:
# 如果玩家向下移动(下落)时碰撞,则将玩家的底边缘放在平台顶边缘的上面
self.rect.bottom = platform.rect.top
self.velocity_y = 0 # 垂直速度清零
self.on_ground = True # 玩家着陆,标记为在地面上
self.jump_count = 0 # 着陆后重置跳跃次数
elif self.velocity_y < 0:
# 如果玩家向上移动(跳跃)时碰撞,则将玩家的顶边缘放在平台底边缘的下面
self.rect.top = platform.rect.bottom
self.velocity_y = 0 # 垂直速度清零
class Platform(pygame.sprite.Sprite):
"""
平台类
继承自 pygame.sprite.Sprite,代表游戏中的可站立平台。
"""
def __init__(self, x, y, width, height, color=GREEN):
"""
初始化平台
:param x: 平台的 x 坐标
:param y: 平台的 y 坐标
:param width: 平台的宽度
:param height: 平台的高度
:param color: 平台的填充颜色 (默认为绿色)
"""
super().__init__()
# 创建平台的图像(一个指定尺寸和颜色的 Surface)
self.image = pygame.Surface((width, height))
self.image.fill(color)
# 在平台上绘制一个棕色的边框,增加视觉效果
pygame.draw.rect(self.image, BROWN, (0, 0, width, height), 2)
# 获取平台的矩形区域,并设置其位置
self.rect = self.image.get_rect(topleft=(x, y))
class Ladder(pygame.sprite.Sprite):
"""
梯子类
继承自 pygame.sprite.Sprite,玩家可以在其上上下移动。
"""
def __init__(self, x, y, width, height):
"""
初始化梯子
:param x: 梯子的 x 坐标
:param y: 梯子的 y 坐标
:param width: 梯子的宽度
:param height: 梯子的高度
"""
super().__init__()
# 创建一个支持透明度的 Surface (pygame.SRCALPHA)
self.image = pygame.Surface((width, height), pygame.SRCALPHA)
# 绘制梯子的主柱(橙色)
pygame.draw.rect(self.image, ORANGE, (width // 2 - 4, 0, 8, height))
# 绘制梯子的横档(黄色)
for i in range(0, height, 20): # 每隔 20 个像素绘制一个横档
pygame.draw.rect(self.image, YELLOW, (0, i, width, 4))
# 获取梯子的矩形区域,并设置其位置
self.rect = self.image.get_rect(topleft=(x, y))
class Coin(pygame.sprite.Sprite):
"""
金币类
继承自 pygame.sprite.Sprite,玩家拾取后可以增加分数。
"""
def __init__(self, x, y):
"""
初始化金币
:param x: 金币的 x 坐标
:param y: 金币的 y 坐标
"""
super().__init__()
# 创建一个支持透明度的 Surface
self.image = pygame.Surface((20, 20), pygame.SRCALPHA)
# 在中心绘制一个黄色的圆形代表金币
pygame.draw.circle(self.image, YELLOW, (10, 10), 10)
# 获取金币的矩形区域,并将其中心设置在指定位置
self.rect = self.image.get_rect(center=(x, y))
class Enemy(pygame.sprite.Sprite):
"""
敌人类
继承自 pygame.sprite.Sprite,会在指定范围内左右移动。
碰到玩家会扣除玩家生命值(在此简化为直接游戏结束)。
"""
def __init__(self, x, y, left_bound, right_bound):
"""
初始化敌人
:param x: 敌人的初始 x 坐标
:param y: 敌人的初始 y 坐标
:param left_bound: 敌人移动的左边界 x 坐标
:param right_bound: 敌人移动的右边界 x 坐标
"""
super().__init__()
# 创建敌人的图像(一个紫色矩形)
self.image = pygame.Surface((30, 30))
self.image.fill(PURPLE)
# 获取敌人的矩形区域,并设置其初始位置
self.rect = self.image.get_rect(topleft=(x, y))
self.speed = 2 # 敌人的移动速度
self.left_bound = left_bound
self.right_bound = right_bound
self.direction = 1 # 初始移动方向 (1 表示向右, -1 表示向左)
def update(self):
"""
更新敌人的位置,使其在边界内来回移动。
"""
# 根据当前方向和速度更新敌人的 x 坐标
self.rect.x += self.speed * self.direction
# 如果敌人到达左边界
if self.rect.left <= self.left_bound:
self.rect.left = self.left_bound # 将位置校准到边界
self.direction = 1 # 改变方向为向右
# 如果敌人到达右边界
if self.rect.right >= self.right_bound:
self.rect.right = self.right_bound # 将位置校准到边界
self.direction = -1 # 改变方向为向左
class Goal(pygame.sprite.Sprite):
"""
关卡终点类
继承自 pygame.sprite.Sprite,玩家到达后视为通关。
"""
def __init__(self, x, y):
"""
初始化关卡终点
:param x: 终点的 x 坐标
:param y: 终点的 y 坐标
"""
super().__init__()
# 创建终点的图像(一个蓝色矩形)
self.image = pygame.Surface((30, 60))
self.image.fill((0, 180, 255))
# 在终点图像上绘制一个白色边框
pygame.draw.rect(self.image, WHITE, (0, 0, 30, 60), 2)
# 获取终点的矩形区域,并设置其初始位置
self.rect = self.image.get_rect(topleft=(x, y))
class Camera:
"""
摄像机类
用于实现游戏世界跟随玩家移动的效果。
"""
def __init__(self, world_width, world_height):
"""
初始化摄像机
:param world_width: 游戏世界的总宽度
:param world_height: 游戏世界的总高度
"""
# camera 是一个 Rect 对象,表示摄像机在世界中的偏移量
self.camera = pygame.Rect(0, 0, world_width, world_height)
self.world_width = world_width
self.world_height = world_height
def apply(self, entity):
"""
将游戏世界中的实体(如玩家、平台)的矩形区域根据摄像机偏移量进行转换。
:param entity: 需要应用摄像机偏移的精灵对象 (必须有 rect 属性)
:return: 转换后的矩形区域,用于在屏幕上绘制
"""
# entity.rect.move(self.camera.topleft) 会将实体的矩形左上角移动到摄像机偏移量的位置
# 这里的 self.camera.topleft 是一个元组 (x, y),表示摄像机在世界中的偏移量
# 实际上,这里是计算实体在屏幕上的显示位置
return entity.rect.move(self.camera.topleft)
def update(self, target):
"""
更新摄像机的位置,使其跟随目标(通常是玩家)。
:param target: 被跟随的目标精灵对象 (通常是玩家)
"""
# 计算摄像机应该移动到的目标 x 和 y 坐标
# 使目标(玩家)位于屏幕的中心
x = -target.rect.centerx + SCREEN_WIDTH // 2
y = -target.rect.centery + SCREEN_HEIGHT // 2
# 限制摄像机的移动范围,防止摄像机移出世界边界
# x 不能超过 0 (即摄像机不能向左移出世界左边界)
x = min(0, x)
# x 不能超过 -(self.world_width - SCREEN_WIDTH) (即摄像机不能向右移出世界右边界)
x = max(-(self.world_width - SCREEN_WIDTH), x)
# y 轴同理
y = min(0, y)
y = max(-(self.world_height - SCREEN_HEIGHT), y)
# 更新摄像机的偏移量 Rect 对象
self.camera = pygame.Rect(x, y, self.world_width, self.world_height)
class Game:
"""
游戏主类
负责游戏的初始化、事件处理、更新逻辑、绘制以及主循环。
"""
def __init__(self):
"""
初始化游戏引擎和所有游戏元素。
"""
# 设置游戏窗口的尺寸并创建屏幕 Surface
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# 设置窗口标题
pygame.display.set_caption("Platformer")
# 创建时钟对象,用于控制帧率
self.clock = pygame.time.Clock()
# 初始化字体
# 使用 pygame.font.Font(None, size) 创建默认字体
# 避免使用 SysFont 可能引发的字体不存在的错误
self.font = pygame.font.Font(None, 32) # 用于普通文本
self.big_font = pygame.font.Font(None, 72) # 用于标题等大字体
# 游戏状态管理
# 'menu': 开始菜单
# 'playing': 游戏进行中
# 'win': 游戏胜利
# 'gameover': 游戏失败
self.state = "menu"
# 创建滑动条对象,用于调整重力和跳跃力
self.gravity_slider = Slider(200, 420, 300, 0.1, 2.0, DEFAULT_GRAVITY, "Gravity")
self.jump_slider = Slider(200, 480, 300, -25.0, -5.0, DEFAULT_JUMP_STRENGTH, "Jump Strength")
# 调用 reset_game() 方法来初始化游戏场景和对象
self.reset_game()
def reset_game(self):
"""
重置整局游戏的状态和所有精灵对象。
当玩家开始新游戏或重新开始时调用。
"""
# 创建用于存放不同类型精灵的 Sprite 组
self.all_sprites = pygame.sprite.Group() # 存放所有精灵,用于统一绘制和更新
self.platforms = pygame.sprite.Group() # 存放所有平台
self.coins = pygame.sprite.Group() # 存放所有金币
self.ladders = pygame.sprite.Group() # 存放所有梯子
self.enemies = pygame.sprite.Group() # 存放所有敌人
self.goal_group = pygame.sprite.Group() # 存放关卡终点
# 创建玩家对象,并添加到 all_sprites 组中
self.player = Player(100, 300)
self.all_sprites.add(self.player)
# 创建平台对象
# platform_data 是一个列表,每个元素是一个元组 (x, y, width, height)
platform_data = [
(0, SCREEN_HEIGHT - 40, 2400, 40), # 地面平台
(250, 500, 180, 20),
(520, 430, 180, 20),
(800, 360, 180, 20),
(1080, 290, 180, 20),
(1360, 220, 180, 20),
(1650, 350, 200, 20),
(1900, 250, 200, 20),
]
# 遍历数据,创建 Platform 对象,并添加到相应的组中
for x, y, w, h in platform_data:
p = Platform(x, y, w, h)
self.all_sprites.add(p)
self.platforms.add(p)
# 创建梯子对象
# ladder_data 是一个列表,每个元素是一个元组 (x, y, width, height)
ladder_data = [
(340, 320, 40, 180),
(900, 180, 40, 180),
(1540, 170, 40, 180),
]
# 遍历数据,创建 Ladder 对象,并添加到相应的组中
for x, y, w, h in ladder_data:
l = Ladder(x, y, w, h)
self.all_sprites.add(l)
self.ladders.add(l)
# 创建金币对象
# coin_positions 是一个列表,每个元素是一个元组 (x, y)
coin_positions = [
(350, 470), (610, 400), (840, 330),
(1120, 260), (1400, 190), (1710, 320),
(1960, 220)
]
# 遍历位置数据,创建 Coin 对象,并添加到相应的组中
for x, y in coin_positions:
c = Coin(x, y)
self.all_sprites.add(c)
self.coins.add(c)
# 创建敌人对象
# enemy_data 是一个列表,每个元素是一个元组 (x, y, left_bound, right_bound)
enemy_data = [
(600, 400, 520, 700),
(1200, 250, 1080, 1300),
(1800, 300, 1650, 2000),
]
# 遍历数据,创建 Enemy 对象,并添加到相应的组中
for x, y, left_b, right_b in enemy_data:
e = Enemy(x, y, left_b, right_b)
self.all_sprites.add(e)
self.enemies.add(e)
# 创建关卡终点对象
self.goal = Goal(2200, 190)
self.all_sprites.add(self.goal)
self.goal_group.add(self.goal)
# 创建摄像机对象,传入世界尺寸
self.camera = Camera(WORLD_WIDTH, WORLD_HEIGHT)
# 初始化分数
self.score = 0
# 初始化游戏结束和胜利状态标记
self.game_over = False
self.win = False
def handle_events(self):
"""
处理游戏中的所有用户输入事件。
:return: 如果需要退出游戏则返回 False,否则返回 True。
"""
# 遍历 Pygame 事件队列
for event in pygame.event.get():
# 如果是退出事件 (点击关闭按钮)
if event.type == pygame.QUIT:
return False # 返回 False 表示退出游戏循环
# =========================
# 菜单状态下的事件处理
# =========================
if self.state == "menu":
# 如果按下空格键
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
self.state = "playing" # 切换到游戏进行中状态
# =========================
# 游戏结束或通关状态下的事件处理
# =========================
elif self.state in ["gameover", "win"]:
# 如果按下任意键
if event.type == pygame.KEYDOWN:
# 如果按下空格键
if event.key == pygame.K_SPACE:
self.reset_game() # 重置游戏
self.state = "playing" # 切换到游戏进行中状态
# 如果按下 ESC 键
elif event.key == pygame.K_ESCAPE:
return False # 返回 False 表示退出游戏循环
# =========================
# 游戏进行中状态下的事件处理
# =========================
elif self.state == "playing":
# 将鼠标事件传递给滑动条对象进行处理
self.gravity_slider.handle_event(event)
self.jump_slider.handle_event(event)
return True # 如果没有退出事件,则返回 True,继续游戏循环
def update(self):
"""
更新游戏中的所有逻辑状态。
仅在游戏进行中 (state == "playing") 时执行。
"""
if self.state != "playing":
return # 如果不是游戏进行中状态,则不执行更新
# 从滑动条读取当前的重力和跳跃力参数
gravity = self.gravity_slider.value
jump_strength = self.jump_slider.value
# 更新玩家对象的状态和位置
# 需要传入平台和梯子组,以便玩家进行碰撞检测和交互
self.player.update(self.platforms, self.ladders, gravity, jump_strength)
# 更新所有敌人对象的状态和位置
self.enemies.update()
# 更新摄像机的位置,使其跟随玩家
self.camera.update(self.player)
# =========================
# 碰撞检测与状态切换
# =========================
# 玩家与金币的碰撞检测
# spritecollide 返回一个列表,包含所有与玩家碰撞的金币
# 第三个参数 True 表示碰撞后将金币从精灵组中移除 (即拾取)
hits = pygame.sprite.spritecollide(self.player, self.coins, True)
# 每拾取一个金币,分数增加 10
self.score += len(hits) * 10
# 玩家与敌人的碰撞检测
# spritecollideany 返回第一个与玩家碰撞的敌人精灵,如果没有则返回 None
# 第三个参数 False 表示碰撞后不移除敌人
if pygame.sprite.spritecollideany(self.player, self.enemies):
self.game_over = True # 标记游戏失败
self.state = "gameover" # 切换到游戏失败状态
# 玩家与终点的碰撞检测
# spritecollideany 返回第一个与玩家碰撞的终点精灵
if pygame.sprite.spritecollideany(self.player, self.goal_group):
self.win = True # 标记游戏胜利
self.state = "win" # 切换到游戏胜利状态
# 玩家掉出屏幕下方的检测
# 如果玩家的顶部 Y 坐标超过屏幕高度加上一个偏移量 (100),则认为玩家死亡
if self.player.rect.top > SCREEN_HEIGHT + 100:
self.game_over = True
self.state = "gameover"
def draw_world(self):
"""
绘制游戏世界中的所有可见元素(背景、平台、玩家、敌人等)。
"""
# 填充屏幕为天蓝色背景
self.screen.fill(SKY_BLUE)
# 遍历所有精灵组中的精灵
for sprite in self.all_sprites:
# 使用摄像机的 apply 方法计算精灵在屏幕上的实际绘制位置
# 然后将精灵的图像绘制到屏幕上
self.screen.blit(sprite.image, self.camera.apply(sprite))
def draw_ui(self):
"""
绘制用户界面元素,如分数、状态信息和操作提示。
"""
# 渲染并绘制分数文本
score_text = self.font.render(f"Score: {self.score}", True, WHITE)
self.screen.blit(score_text, (10, 10))
# 渲染并绘制当前游戏状态文本
state_text = self.font.render(f"State: {self.state.upper()}", True, WHITE)
self.screen.blit(state_text, (10, 40))
# 渲染并绘制操作说明文本
info1 = self.font.render("Arrows: Move Space: Jump or Start", True, WHITE)
info2 = self.font.render("Double Jump enabled Up/Down on ladder", True, WHITE)
self.screen.blit(info1, (10, SCREEN_HEIGHT - 90))
self.screen.blit(info2, (10, SCREEN_HEIGHT - 60))
# 只在游戏进行中 (playing) 状态下绘制滑动条
if self.state == "playing":
self.gravity_slider.draw(self.screen, self.font)
self.jump_slider.draw(self.screen, self.font)
def draw_menu(self):
"""
绘制游戏开始菜单界面。
"""
# 填充屏幕为天蓝色背景
self.screen.fill(SKY_BLUE)
# 渲染并绘制游戏标题
title = self.big_font.render("Platformer", True, WHITE)
# 将标题居中显示
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 140))
self.screen.blit(title, title_rect)
# 渲染并绘制各种提示信息
tip1 = self.font.render("Press SPACE to start", True, WHITE)
tip2 = self.font.render("Move with Arrow Keys", True, WHITE)
tip3 = self.font.render("Press SPACE twice to double jump", True, WHITE)
tip4 = self.font.render("Use ladders to climb up and down", True, WHITE)
tip5 = self.font.render("Drag sliders to change Gravity and Jump Strength", True, WHITE)
# 将提示信息居中显示
self.screen.blit(tip1, tip1.get_rect(center=(SCREEN_WIDTH // 2, 250)))
self.screen.blit(tip2, tip2.get_rect(center=(SCREEN_WIDTH // 2, 300)))
self.screen.blit(tip3, tip3.get_rect(center=(SCREEN_WIDTH // 2, 340)))
self.screen.blit(tip4, tip4.get_rect(center=(SCREEN_WIDTH // 2, 380)))
self.screen.blit(tip5, tip5.get_rect(center=(SCREEN_WIDTH // 2, 450)))
# 更新整个屏幕显示,显示绘制的内容
pygame.display.flip()
def draw_overlay_message(self, main_msg, sub_msg):
"""
绘制游戏结束或通关时的覆盖层和提示信息。
:param main_msg: 主标题信息 (例如 "Game Over" 或 "You Win!")
:param sub_msg: 副标题信息 (例如 "Press SPACE to restart")
"""
# 首先绘制游戏世界背景
self.draw_world()
# 创建一个半透明的覆盖层 Surface
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 180)) # 使用 RGBA 颜色,A (alpha) 值表示透明度 (0-255)
# 将覆盖层绘制到屏幕上
self.screen.blit(overlay, (0, 0))
# 渲染并绘制主标题信息,居中显示
main_text = self.big_font.render(main_msg, True, WHITE)
main_rect = main_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 50))
self.screen.blit(main_text, main_rect)
# 渲染并绘制副标题信息,居中显示
sub_text = self.font.render(sub_msg, True, WHITE)
sub_rect = sub_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 40))
self.screen.blit(sub_text, sub_rect)
# 更新整个屏幕显示
pygame.display.flip()
def draw(self):
"""
根据当前游戏状态调用相应的绘制方法。
这是游戏的主绘制入口。
"""
# 如果是菜单状态,调用 draw_menu()
if self.state == "menu":
self.draw_menu()
return # 绘制完成后直接返回,不执行后续操作
# 如果是游戏进行中状态
if self.state == "playing":
self.draw_world() # 先绘制游戏世界
self.draw_ui() # 再绘制用户界面元素
pygame.display.flip() # 更新屏幕显示
return
# 如果是游戏失败状态
if self.state == "gameover":
# 调用 draw_overlay_message 绘制游戏结束的覆盖层和提示
self.draw_overlay_message("Game Over", "Press SPACE to restart")
return
# 如果是游戏胜利状态
if self.state == "win":
# 调用 draw_overlay_message 绘制游戏胜利的覆盖层和提示
self.draw_overlay_message("You Win!", "Press SPACE to restart")
return
def run(self):
"""
游戏的主循环。
负责不断地处理事件、更新游戏状态和绘制画面。
"""
running = True # 游戏主循环的控制变量
while running:
# 1. 处理事件
# handle_events() 返回 False 时,running 会被设为 False,从而退出循环
running = self.handle_events()
# 2. 更新游戏逻辑
self.update()
# 3. 绘制画面
self.draw()
# 4. 控制帧率
# clock.tick(FPS) 会暂停一段时间,确保游戏帧率不超过 FPS 指定的值
self.clock.tick(FPS)
# 游戏循环结束后,退出 Pygame 并终止程序
pygame.quit()
sys.exit()
# 程序入口点
if __name__ == "__main__":
# 创建 Game 类的实例
game = Game()
# 调用 run() 方法启动游戏主循环
game.run()

23.3 方法论总结与性能优化路径
通过本章的实战,我们建立了一套完整的2D平台游戏物理与摄像机架构范式:
分离轴碰撞解析:通过将碰撞检测与响应分离为水平与垂直两个正交阶段,我们解决了斜向碰撞的歧义性问题。这种**阶段式物理更新(Phase-based Physics Update)**确保了平台碰撞的可预测性,是平台游戏手感一致性的基石。
视口变换与边界约束 :摄像机系统的核心在于世界-屏幕坐标变换(World-to-Screen Coordinate Transformation)与硬边界约束(Hard Boundary Constraints)。通过Clamp算法,我们避免了摄像机移出世界边界,这种**边界守卫(Boundary Guard)**模式在开放世界游戏中尤为重要。
参数外部化与快速原型 :通过Slider控件实现的重力与跳跃力实时调节,展示了数据驱动设计(Data-Driven Design)在原型阶段的价值。这种物理参数的外部化允许非程序员(如游戏设计师)参与手感调优,显著提高了迭代效率。
分层状态机的UI解耦 :Menu/Playing/GameOver/Win四个状态的分离,实现了关注点分离(Separation of Concerns)------不同状态下的绘制逻辑、输入响应与物理更新完全隔离,防止了状态间的逻辑渗透。
课后练习
基于本章建立的平台物理架构,尝试以下进阶扩展,重点关注物理一致性与关卡设计的可扩展性:
-
** coyote time(土狼时间)与跳跃缓冲**:实现"coyote time"机制(玩家离开平台边缘后短暂时间内仍可跳跃)与"跳跃缓冲"(玩家在落地前按下跳跃键,落地后立即执行跳跃)。思考:这需要引入时间窗口(Time Window)与输入缓冲队列(Input Buffer Queue),如何在不破坏现有物理更新的前提下集成这些机制?
-
瓦片地图(Tilemap)系统 :将硬编码的平台数据改造为基于CSV或JSON的瓦片地图系统,使用**瓦片集(Tileset)与 地图编辑器(如Tiled)设计关卡。探讨 空间哈希(Spatial Hashing)或四叉树(Quadtree)**在大规模瓦片地图中的碰撞优化应用。
-
物理材质系统 :为不同平台分配"材质"属性(如冰面低摩擦力、弹跳垫高弹性),通过**物理材质(Physics Material)类封装摩擦系数与弹性系数,改造碰撞响应逻辑以支持材质特性。这需要重构
check_collision方法,引入反弹向量(Bounce Vector)**计算。 -
状态机层级深化:在Playing状态下引入子状态机(Sub-state Machine),区分"Normal(正常)"、"OnLadder(爬梯)"、"Dashing(冲刺)"等子状态,使用**状态模式(State Pattern)**将不同状态下的物理行为(重力应用、输入响应)封装为独立类。
-
AI辅助设计 :使用GPT-5.4分析当前物理参数空间(重力-跳跃力组合),生成手感评估函数(Feel Evaluation Function),自动寻找使跳跃高度与距离达到特定设计目标的参数组合,并讨论如何通过**遗传算法(Genetic Algorithm)**自动生成平衡的平台关卡布局。