游戏主题:2D特技赛车平衡游戏
代码行数:680行完整可运行游戏
学习目标:掌握pymunk约束与关节系统,实现复杂机械结构的物理模拟
最终效果:一个具有真实悬挂系统、可操控的2D特技赛车,可在各种地形上行驶、飞跃和表演特技
一、开篇引入:机械之美与关节艺术
从弹球到机械:物理游戏的进阶之路
在上一篇《弹球大作战》中,我们探索了刚体、形状和碰撞的基础世界。现在,我们将进入一个更加精彩的领域:约束与关节。如果说刚体是物理世界的"单词",那么关节就是将这些单词连接成复杂"句子"的语法规则。
想象一下:
-
《桥梁建筑师》中那些精巧的桁架结构
-
《人类一败涂地》中软绵绵角色的滑稽动作
-
《Besiege》中复杂的战争机器
-
《Spintires》中真实的车辆物理
这些游戏的核心魔法,都源自约束与关节系统的精妙运用。通过关节,我们可以创造从简单的铰链门到复杂的多足机器人的一切。
为什么学习约束与关节?
1. 模拟现实世界的连接方式
现实世界中很少有完全自由的物体。大多数物体都以某种方式连接:
-
铰链连接的门窗
-
弹簧悬挂的车辆
-
齿轮传动的机械
-
绳索牵引的装置
2. 创造有趣的游戏机制
关节不仅仅是物理模拟,更是游戏设计的工具:
-
物理谜题:玩家需要理解约束来解谜
-
载具操控:真实的车辆、飞机、机器人控制
-
角色动画:更自然的角色动作和布娃娃系统
-
可破坏环境:关节断裂产生的动态破坏效果
3. 性能优化的利器
相比大量自由运动的刚体,通过关节连接的刚体系统:
-
计算更高效
-
行为更可控
-
更容易调试和调整
本篇学习目标:打造你的物理特技车
我们将通过创建一个2D特技赛车游戏,系统学习pymunk的约束系统。这辆车将具备:
-
真实的悬挂系统:弹簧阻尼悬挂,适应不平地形
-
精确的转向控制:前轮转向,后轮驱动
-
物理特技能力:飞跃、翻滚、平衡
-
复杂的碰撞响应:连续碰撞检测,防止高速穿透
-
可调参数系统:实时调整车辆性能
技术栈升级
除了pymunk基础,我们还将学习:
-
高级碰撞检测:射线检测、形状查询
-
自定义约束:弹簧、马达、旋转限制
-
物理调试可视化:约束力的可视化
-
性能优化:休眠系统、碰撞过滤
-
输入处理:模拟摇杆输入、力反馈
预备知识检查
在开始前,请确保你已经掌握:
-
pymunk基础(刚体、形状、空间)
-
Pygame绘图和事件处理
-
向量基础运算(加法、点积、叉积)
-
基本的三角函数
如果你对某些概念还不熟悉,不用担心,我会在需要时进行简要复习。现在,系好安全带,我们准备发车!
二、理论讲解:pymunk约束系统深度剖析
2.1 约束的基本原理
什么是约束?
约束是限制刚体运动自由度的规则。在3D空间中,一个自由刚体有6个自由度(3个平移,3个旋转)。约束通过数学方程限制这些自由度,使物体以特定方式运动。
约束方程的一般形式:
C(p1, p2, ..., pn) = 0
其中C是约束函数,p是物体的位置和方向参数。
约束的求解:拉格朗日乘子法
物理引擎通过拉格朗日乘子法求解约束:
M·a = F + J^T·λ
C(q) = 0
其中:
-
M:质量矩阵
-
a:加速度向量
-
F:外力向量
-
J:约束的雅可比矩阵
-
λ:拉格朗日乘子(约束力)
pymunk封装了这些复杂计算,我们只需关注高层抽象。
2.2 pymunk约束类型详解
pymunk提供了多种约束类型,我们将从简单到复杂逐一解析。
2.2.1 简单约束:PinJoint(销关节)
物理意义:将两个点固定在一起,就像用图钉钉住两张纸。
python
# 创建PinJoint
joint = pymunk.PinJoint(body_a, body_b, anchor_a, anchor_b)
参数解析:
-
body_a,body_b:连接的两个刚体 -
anchor_a:相对于body_a的锚点(局部坐标) -
anchor_b:相对于body_b的锚点(局部坐标)
数学原理:
约束方程:p_a + r_a = p_b + r_b
其中p是质心位置,r是锚点相对于质心的位置。
应用场景:
-
简单的链条连接
-
摆锤悬挂
-
固定点约束
2.2.2 滑动约束:SlideJoint(滑动关节)
物理意义:限制两个锚点之间的距离在最小值和最大值之间。
python
# 创建SlideJoint
joint = pymunk.SlideJoint(body_a, body_b, anchor_a, anchor_b, min, max)
参数解析:
-
min:最小距离 -
max:最大距离
数学原理:
约束条件:min ≤ ||p_a + r_a - (p_b + r_b)|| ≤ max
应用场景:
-
伸缩杆
-
绳索长度限制
-
车辆悬挂的行程限制
2.2.3 旋转约束:PivotJoint(旋转关节)
物理意义:将两个刚体在指定点连接,允许绕该点自由旋转。
python
# 创建PivotJoint
joint = pymunk.PivotJoint(body_a, body_b, anchor_a, anchor_b)
# 或者使用世界坐标锚点
joint = pymunk.PivotJoint(body_a, body_b, world_anchor)
应用场景:
-
门铰链
-
摆臂
-
车轮连接
2.2.4 旋转限制:RotaryLimitJoint(旋转限制关节)
物理意义:限制两个刚体间的相对旋转角度。
python
# 创建RotaryLimitJoint
joint = pymunk.RotaryLimitJoint(body_a, body_b, min_angle, max_angle)
参数解析:
-
min_angle:最小相对角度(弧度) -
max_angle:最大相对角度(弧度)
数学原理:
min ≤ θ_b - θ_a ≤ max
其中θ是刚体的绝对角度。
应用场景:
-
有限旋转的门
-
机械臂的活动范围限制
-
车辆转向角度限制
2.2.5 旋转弹簧:DampedRotarySpring(阻尼旋转弹簧)
物理意义:模拟扭转弹簧,产生与相对角度成正比的扭矩。
python
# 创建DampedRotarySpring
joint = pymunk.DampedRotarySpring(body_a, body_b, rest_angle, stiffness, damping)
参数解析:
-
rest_angle:平衡位置的角度 -
stiffness:刚度系数(扭矩/弧度) -
damping:阻尼系数
数学原理:
扭矩 = -stiffness * (θ - rest_angle) - damping * ω
其中ω是相对角速度。
应用场景:
-
车辆的稳定杆
-
可回中的转向系统
-
物理布娃娃的关节
2.2.6 线性弹簧:DampedSpring(阻尼弹簧)
物理意义:模拟线性弹簧阻尼器,这是我们车辆悬挂的核心。
python
# 创建DampedSpring
joint = pymunk.DampedSpring(body_a, body_b, anchor_a, anchor_b,
rest_length, stiffness, damping)
参数解析:
-
rest_length:弹簧的自然长度 -
stiffness:刚度系数(力/单位长度) -
damping:阻尼系数
数学原理:
胡克定律 + 阻尼:F = -k·(l - l₀) - c·v
其中l是当前长度,v是长度变化速度。
应用场景:
-
车辆悬挂
-
弹簧减震器
-
弹性连接
2.2.7 齿轮约束:GearJoint(齿轮关节)
物理意义:保持两个刚体以固定角速度比旋转。
python
# 创建GearJoint
joint = pymunk.GearJoint(body_a, body_b, phase, ratio)
参数解析:
-
phase:相位偏移 -
ratio:角速度比(body_b / body_a)
数学原理:
约束方程:θ_b - ratio * θ_a = phase
应用场景:
-
齿轮传动系统
-
同步旋转机构
-
差速器模拟
2.2.8 马达约束:SimpleMotor(简单马达)
物理意义:以恒定相对角速度驱动两个刚体。
python
# 创建SimpleMotor
joint = pymunk.SimpleMotor(body_a, body_b, rate)
参数解析:
rate:目标相对角速度(弧度/秒)
应用场景:
-
旋转马达
-
传送带
-
持续旋转的机构
2.3 约束的进阶概念
约束力与冲量
约束通过施加力(冲量)来满足约束条件。我们可以查询这些力:
python
# 获取约束力
force = joint.impulse / dt # 冲量除以时间得到平均力
# 获取约束扭矩(对于旋转约束)
torque = joint.impulse # 对于旋转约束,impulse是扭矩冲量
约束的破坏阈值
可以设置约束的最大力,超过时约束会"断裂":
python
joint.max_force = 1000 # 最大力限制
joint.max_bias = 100 # 最大偏置速度限制(用于防止过冲)
# 检查约束是否失效
if joint.impulse > joint.max_force * dt:
print("约束断裂!")
space.remove(joint)
约束的预计算优化
对于不变的约束,可以预计算雅可比矩阵以提高性能:
python
# 预计算(pymunk内部自动处理)
joint.pre_solve = True
2.4 车辆物理模型
2.4.1 车辆动力学基础
一个基本的车辆模型包含以下组件:
-
底盘(Chassis):车辆主体,承载大部分质量
-
车轮(Wheels):四个圆形刚体
-
悬挂(Suspension):弹簧阻尼系统,连接车轮和底盘
-
转向系统(Steering):控制前轮角度
-
驱动系统(Drivetrain):施加扭矩到车轮
2.4.2 悬挂系统建模
悬挂系统的核心是弹簧阻尼器,其行为由以下参数决定:
-
弹簧刚度(k):单位压缩产生的力(N/m)
-
阻尼系数(c):单位速度产生的阻尼力(N·s/m)
-
行程(travel):悬挂的最大压缩和伸展距离
-
预压(preload):静止状态下的压缩量
悬挂力计算:
python
def suspension_force(compression, velocity, k, c, preload=0):
# 压缩量(正值为压缩)
delta = compression + preload
# 弹簧力(胡克定律)
spring_force = -k * delta
# 阻尼力(与速度方向相反)
damping_force = -c * velocity
# 总力
total_force = spring_force + damping_force
# 确保力不会使悬挂拉伸(仅推力)
if total_force > 0: # 悬挂只能推,不能拉
total_force = 0
return total_force
2.4.3 轮胎力模型
轮胎与地面的交互是车辆物理中最复杂的部分。简化的"魔术公式"模型:
python
def tire_force(slip_ratio, slip_angle, friction_coef, load):
"""
计算轮胎力
参数:
slip_ratio: 滑移率 (v_wheel - v_vehicle)/v_vehicle
slip_angle: 侧偏角 (弧度)
friction_coef: 摩擦系数
load: 轮胎垂直载荷
"""
# 组合滑移
total_slip = math.sqrt(slip_ratio**2 + math.sin(slip_angle)**2)
# 魔术公式(简化版)
B = 10.0 # 刚度因子
C = 1.9 # 形状因子
D = friction_coef * load # 峰值因子
E = 0.97 # 曲率因子
# 归一化滑移
normalized_slip = B * total_slip
# 魔术公式
mu = D * math.sin(C * math.atan(normalized_slip - E * (normalized_slip - math.atan(normalized_slip))))
# 总力
force = mu * load
# 分解为纵向和侧向力
if total_slip > 0.001:
longitudinal = force * slip_ratio / total_slip
lateral = force * math.sin(slip_angle) / total_slip
else:
longitudinal = 0
lateral = 0
return longitudinal, lateral
2.4.4 转向几何
阿克曼转向几何确保车轮在转弯时围绕同一中心旋转:
python
def ackermann_steering(steering_angle, wheelbase, track_width):
"""
计算阿克曼转向角度
参数:
steering_angle: 方向盘转角 (弧度)
wheelbase: 轴距
track_width: 轮距
返回:
(inner_angle, outer_angle) 内外轮转角
"""
if abs(steering_angle) < 0.001:
return 0, 0
# 转弯半径
turn_radius = wheelbase / math.tan(steering_angle)
# 内轮转角
inner_angle = math.atan(wheelbase / (turn_radius - track_width/2))
# 外轮转角
outer_angle = math.atan(wheelbase / (turn_radius + track_width/2))
return inner_angle, outer_angle
2.5 连续碰撞检测
为什么需要CCD?
当物体高速运动时,可能会在单帧内穿过其他物体,导致碰撞检测失败。这就是"隧道效应"。
CCD解决方案:
-
扫描测试:检查物体从上一帧到当前帧的扫描体积
-
时间回溯:找到碰撞发生的精确时间
-
子步进:增加物理更新的频率
pymunk中的CCD实现
python
def ackermann_steering(steering_angle, wheelbase, track_width):
"""
计算阿克曼转向角度
参数:
steering_angle: 方向盘转角 (弧度)
wheelbase: 轴距
track_width: 轮距
返回:
(inner_angle, outer_angle) 内外轮转角
"""
if abs(steering_angle) < 0.001:
return 0, 0
# 转弯半径
turn_radius = wheelbase / math.tan(steering_angle)
# 内轮转角
inner_angle = math.atan(wheelbase / (turn_radius - track_width/2))
# 外轮转角
outer_angle = math.atan(wheelbase / (turn_radius + track_width/2))
return inner_angle, outer_angle
2.5 连续碰撞检测
为什么需要CCD?
当物体高速运动时,可能会在单帧内穿过其他物体,导致碰撞检测失败。这就是"隧道效应"。
CCD解决方案:
-
扫描测试:检查物体从上一帧到当前帧的扫描体积
-
时间回溯:找到碰撞发生的精确时间
-
子步进:增加物理更新的频率
pymunk中的CCD实现
python
# 为高速物体启用CCD
body.velocity_func = ccd_velocity
def ccd_velocity(body, gravity, damping, dt):
"""自定义速度函数,启用连续碰撞检测"""
# 保存原速度
original_velocity = body.velocity
# 调用默认速度更新
pymunk.Body.update_velocity(body, gravity, damping, dt)
# 计算位移
displacement = body.velocity * dt
# 如果位移过大,启用CCD
max_displacement = body.shapes[0].radius * 2
if displacement.length > max_displacement:
# 设置形状为传感器,pymunk会进行扫描测试
for shape in body.shapes:
shape.sensor = True
# 设置扫描测试的起始位置
shape.body.previous_position = body.position - displacement
return body.velocity
2.6 性能优化策略
约束求解的优化
约束求解是物理引擎中最耗时的部分之一。优化策略:
-
约束分组:将不相关的约束分组求解
-
温暖启动:使用上一帧的解作为初始猜测
-
迭代次数调整:根据精度需求调整迭代次数
python
# 设置空间求解参数
space.iterations = 10 # 默认10次,增加可提高精度但降低性能
space.damping = 0.9 # 全局阻尼,帮助稳定
休眠系统
静止的物体可以进入休眠状态,跳过物理计算:
python
# 启用休眠
body.sleep()
# 检查休眠状态
if body.is_sleeping:
# 跳过这个物体的物理计算
pass
# 唤醒物体
body.activate()
碰撞过滤
通过碰撞掩码避免不必要的碰撞检测:
python
# 设置碰撞类别
CAR_CATEGORY = 0b0001
GROUND_CATEGORY = 0b0010
OBSTACLE_CATEGORY = 0b0100
# 车轮只与地面碰撞
wheel_shape.filter = pymunk.ShapeFilter(
categories=CAR_CATEGORY,
mask=GROUND_CATEGORY
)
# 底盘与所有物体碰撞
chassis_shape.filter = pymunk.ShapeFilter(
categories=CAR_CATEGORY,
mask=GROUND_CATEGORY | OBSTACLE_CATEGORY
)
三、项目搭建:特技赛车游戏框架
3.1 项目结构与依赖
项目目录结构
bash
stunt_car_physics/
├── main.py # 游戏主入口
├── physics_engine.py # 高级物理引擎封装
├── vehicle.py # 车辆类定义
├── terrain.py # 地形生成与管理
├── camera.py # 智能相机系统
├── ui.py # 用户界面
├── stunt_tracker.py # 特技检测与评分
├── utils/
│ ├── constraint_debug.py # 约束调试可视化
│ ├── performance.py # 性能监控
│ ├── config.py # 配置常量
│ └── helpers.py # 辅助函数
├── assets/
│ ├── fonts/
│ ├── sounds/
│ └── textures/
└── requirements.txt
依赖安装
bash
# 基础依赖
pip install pymunk==6.6.0
pip install pygame==2.5.0
pip install numpy # 用于高级数学运算
pip install noise # 用于地形生成
# 开发工具
pip install pyinstrument # 性能分析
pip install memory_profiler # 内存分析
3.2 配置系统 (utils/config.py)
python
"""
特技赛车游戏配置
"""
import pygame
import math
# 窗口设置
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
FPS = 60
CAPTION = "物理特技赛车 - Pymunk约束与关节教程"
# 物理设置
PHYSICS_FPS = 240 # 物理更新频率
PIXELS_PER_METER = 50.0
GRAVITY = (0, 9.8 * PIXELS_PER_METER) # 9.8m/s²
DAMPING = 0.9 # 全局阻尼
# 颜色定义
COLORS = {
"background": (40, 44, 52),
"sky": (135, 206, 235),
"ground": (101, 67, 33),
"grass": (76, 175, 80),
"chassis": (220, 20, 60), # 红色
"wheel": (50, 50, 50), # 深灰
"suspension": (255, 165, 0), # 橙色
"constraint": (0, 255, 255, 100), # 青色,半透明
"thruster": (255, 215, 0), # 金色
"boost": (0, 191, 255), # 亮蓝色
"text": (255, 255, 255),
"ui_background": (30, 30, 40, 200),
"ui_border": (100, 100, 150),
"health_good": (0, 255, 0),
"health_warning": (255, 255, 0),
"health_critical": (255, 0, 0),
}
# 车辆默认参数
VEHICLE_CONFIG = {
"chassis": {
"width": 2.0, # 米
"height": 0.8, # 米
"mass": 800, # 千克
"density": 1.0,
"elasticity": 0.2,
"friction": 0.8,
},
"wheel": {
"radius": 0.4, # 米
"mass": 20, # 千克
"elasticity": 0.4,
"friction": 1.5, # 高摩擦模拟轮胎
},
"suspension": {
"stiffness": 20000, # N/m
"damping": 2000, # N·s/m
"rest_length": 0.5, # 米
"max_force": 10000, # 最大力
},
"drivetrain": {
"max_torque": 2000, # N·m
"brake_torque": 3000,
"differential_ratio": 3.42,
},
"steering": {
"max_angle": math.radians(30), # 最大转向角
"response_speed": 5.0, # 转向响应速度
},
"aerodynamics": {
"drag_coefficient": 0.3,
"downforce_coefficient": 1.2,
}
}
# 地形设置
TERRAIN_CONFIG = {
"width": 200, # 地形宽度(米)
"height_variance": 10, # 高度变化(米)
"segment_length": 1.0, # 每段长度(米)
"roughness": 0.5, # 粗糙度 0-1
"seed": 42, # 随机种子
}
# 游戏设置
GAME_CONFIG = {
"boost_power": 5000, # 助推器力量
"boost_duration": 3.0, # 助推持续时间
"boost_cooldown": 10.0, # 冷却时间
"max_health": 100,
"health_regen": 0.5, # 生命恢复/秒
"stunt_multipliers": {
"air_time": 2.0, # 空中时间乘数
"rotation": 1.5, # 旋转乘数
"flip": 3.0, # 翻转乘数
"combo": 1.2, # 连击乘数
}
}
# 输入配置
INPUT_CONFIG = {
"throttle": [pygame.K_UP, pygame.K_w],
"brake": [pygame.K_DOWN, pygame.K_s],
"left": [pygame.K_LEFT, pygame.K_a],
"right": [pygame.K_RIGHT, pygame.K_d],
"boost": [pygame.K_SPACE, pygame.K_LSHIFT],
"reset": [pygame.K_r],
"debug": [pygame.K_F1],
"pause": [pygame.K_p, pygame.K_ESCAPE],
}
# 相机设置
CAMERA_CONFIG = {
"smoothness": 0.1, # 平滑度 0-1
"look_ahead": 0.3, # 前瞻系数
"zoom_min": 0.5,
"zoom_max": 2.0,
"zoom_speed": 0.1,
}
3.3 高级物理引擎封装 (physics_engine.py)
python
"""
高级物理引擎封装
包含约束管理、性能优化、调试工具
"""
import pymunk
import pygame
import time
from collections import defaultdict
from utils.config import *
class AdvancedPhysicsEngine:
"""高级物理引擎,管理约束和性能优化"""
def __init__(self, gravity=GRAVITY, damping=DAMPING):
# 创建物理空间
self.space = pymunk.Space()
self.space.gravity = gravity
self.space.damping = damping
# 碰撞配置
self.space.collision_bias = 0.0001
self.space.collision_slop = 0.01
self.space.iterations = 20 # 增加迭代提高约束精度
# 约束管理
self.constraints = []
self.constraint_groups = defaultdict(list)
# 性能监控
self.step_times = []
self.max_step_time = 0.016 # 60FPS的限制
self.total_steps = 0
# 固定时间步长
self.fixed_dt = 1.0 / PHYSICS_FPS
self.accumulator = 0.0
self.alpha = 0.0 # 插值因子
# 休眠管理
self.sleeping_bodies = set()
self.idle_time_threshold = 2.0 # 2秒无运动进入休眠
# 调试信息
self.debug_info = {
"total_bodies": 0,
"active_bodies": 0,
"total_constraints": 0,
"collisions_per_step": 0,
"avg_step_time": 0,
}
# 约束断裂回调
self.constraint_broken_callbacks = []
def add_constraint(self, constraint, group="default"):
"""添加约束到指定组"""
self.space.add(constraint)
self.constraints.append(constraint)
self.constraint_groups[group].append(constraint)
# 设置约束断裂回调
if hasattr(constraint, 'max_force') and constraint.max_force > 0:
constraint.pre_solve = self._create_constraint_check(constraint)
return constraint
def _create_constraint_check(self, constraint):
"""创建约束断裂检查函数"""
def check(arbiter, space, data):
# 在实际断裂检查中,pymunk不直接提供回调
# 我们会在后处理中检查
return True
return check
def remove_constraint(self, constraint):
"""移除约束"""
if constraint in self.constraints:
self.space.remove(constraint)
self.constraints.remove(constraint)
# 从分组中移除
for group in self.constraint_groups.values():
if constraint in group:
group.remove(constraint)
def add(self, *args):
"""添加物体到空间"""
self.space.add(*args)
# 更新刚体计数
for obj in args:
if isinstance(obj, pymunk.Body):
self.debug_info["total_bodies"] += 1
self.debug_info["active_bodies"] += 1
def remove(self, *args):
"""从空间移除物体"""
self.space.remove(*args)
# 更新刚体计数
for obj in args:
if isinstance(obj, pymunk.Body):
self.debug_info["total_bodies"] -= 1
if obj in self.sleeping_bodies:
self.sleeping_bodies.remove(obj)
self.debug_info["active_bodies"] -= 1
def update(self, frame_time):
"""更新物理模拟,使用固定时间步长和插值"""
# 限制最大帧时间
frame_time = min(frame_time, 0.25)
self.accumulator += frame_time
# 记录步进开始时间
step_start = time.perf_counter()
# 固定步长更新
steps = 0
while self.accumulator >= self.fixed_dt and steps < 5: # 最多5步子步进
# 唤醒可能的活动物体
self._update_sleeping_bodies()
# 执行物理步进
self.space.step(self.fixed_dt)
# 检查约束断裂
self._check_broken_constraints()
self.accumulator -= self.fixed_dt
steps += 1
self.total_steps += 1
# 更新物体休眠计时器
self._update_idle_timers()
# 计算插值因子
self.alpha = self.accumulator / self.fixed_dt if self.fixed_dt > 0 else 0
# 性能监控
step_time = time.perf_counter() - step_start
self.step_times.append(step_time)
if len(self.step_times) > 100:
self.step_times.pop(0)
self.debug_info["avg_step_time"] = sum(self.step_times) / len(self.step_times) if self.step_times else 0
self.debug_info["active_bodies"] = len([b for b in self.space.bodies if b.body_type == pymunk.Body.DYNAMIC])
def _update_sleeping_bodies(self):
"""更新休眠物体"""
for body in list(self.sleeping_bodies):
# 检查是否需要唤醒
if (body.velocity.length_sq > 0.1 or
body.angular_velocity**2 > 0.01):
body.activate()
self.sleeping_bodies.remove(body)
def _update_idle_timers(self):
"""更新空闲计时器"""
# 简化的休眠系统
for body in self.space.bodies:
if (body.body_type == pymunk.Body.DYNAMIC and
body.velocity.length_sq < 0.01 and
body.angular_velocity**2 < 0.001):
# 如果不在休眠列表,检查是否满足休眠条件
if body not in self.sleeping_bodies:
if not hasattr(body, 'idle_time'):
body.idle_time = 0
body.idle_time += self.fixed_dt
if body.idle_time > self.idle_time_threshold:
self.sleeping_bodies.add(body)
# 在实际实现中,这里应该调用body.sleep()
else:
# 已经在休眠,跳过物理计算
pass
else:
# 物体在运动,重置空闲计时
if hasattr(body, 'idle_time'):
body.idle_time = 0
if body in self.sleeping_bodies:
self.sleeping_bodies.remove(body)
def _check_broken_constraints(self):
"""检查断裂的约束"""
broken_constraints = []
for constraint in self.constraints:
if hasattr(constraint, 'max_force') and constraint.max_force > 0:
# 估算约束力(冲量/时间)
force_estimate = abs(constraint.impulse) / self.fixed_dt
if force_estimate > constraint.max_force:
broken_constraints.append(constraint)
# 移除断裂的约束
for constraint in broken_constraints:
self.remove_constraint(constraint)
# 调用断裂回调
for callback in self.constraint_broken_callbacks:
callback(constraint)
def get_interpolated_position(self, body):
"""获取插值后的位置(用于平滑渲染)"""
if not hasattr(body, 'previous_position'):
body.previous_position = body.position
# 线性插值
x = body.previous_position.x + (body.position.x - body.previous_position.x) * self.alpha
y = body.previous_position.y + (body.position.y - body.previous_position.y) * self.alpha
# 更新上一帧位置
body.previous_position = body.position
return pymunk.Vec2d(x, y)
def raycast(self, start, end, radius=0, shape_filter=None, max_distance=float('inf')):
"""射线检测,返回所有命中点"""
# 分段检测,避免错过小物体
segment_length = 1.0 # 米
direction = (end - start).normalized()
distance = (end - start).length
hits = []
current_pos = start
while (current_pos - start).length < distance:
next_pos = current_pos + direction * min(segment_length, distance - (current_pos - start).length)
# 执行分段检测
result = self.space.segment_query_first(current_pos, next_pos, radius, shape_filter)
if result:
hit_point = result.point
if (hit_point - start).length <= max_distance:
hits.append({
"point": hit_point,
"normal": result.normal,
"shape": result.shape,
"distance": (hit_point - start).length
})
# 移动到碰撞点后一点继续检测
current_pos = hit_point + direction * 0.1
else:
current_pos = next_pos
return hits
def query_shape(self, shape, position, rotation=0, shape_filter=None):
"""查询与指定形状重叠的物体"""
# 创建临时形状用于查询
temp_body = pymunk.Body(body_type=pymunk.Body.STATIC)
temp_body.position = position
temp_body.angle = rotation
if isinstance(shape, pymunk.Circle):
temp_shape = pymunk.Circle(temp_body, shape.radius)
elif isinstance(shape, pymunk.Poly):
temp_shape = pymunk.Poly(temp_body, shape.get_vertices())
else:
return []
temp_shape.filter = shape_filter or pymunk.ShapeFilter()
# 执行查询
results = []
query_callback = lambda s, _: results.append(s)
self.space.shape_query(temp_shape, query_callback)
return results
def get_performance_stats(self):
"""获取性能统计"""
avg_step = self.debug_info["avg_step_time"] * 1000 # 转为毫秒
theoretical_fps = 1.0 / self.debug_info["avg_step_time"] if self.debug_info["avg_step_time"] > 0 else 0
stats = {
"step_time_ms": f"{avg_step:.2f}",
"theoretical_fps": f"{theoretical_fps:.0f}",
"total_steps": self.total_steps,
"total_bodies": self.debug_info["total_bodies"],
"active_bodies": self.debug_info["active_bodies"],
"sleeping_bodies": len(self.sleeping_bodies),
"total_constraints": len(self.constraints),
"constraint_groups": len(self.constraint_groups),
}
return stats
def pixel_to_meter(self, pixels):
"""像素转米"""
return pixels / PIXELS_PER_METER
def meter_to_pixel(self, meters):
"""米转像素"""
return meters * PIXELS_PER_METER
def world_to_screen(self, world_pos, camera_offset=(0, 0)):
"""世界坐标转屏幕坐标"""
x = world_pos.x * PIXELS_PER_METER + camera_offset[0]
y = SCREEN_HEIGHT - world_pos.y * PIXELS_PER_METER + camera_offset[1] # Y轴翻转
return (x, y)
def screen_to_world(self, screen_pos, camera_offset=(0, 0)):
"""屏幕坐标转世界坐标"""
x = (screen_pos[0] - camera_offset[0]) / PIXELS_PER_METER
y = (SCREEN_HEIGHT - screen_pos[1] + camera_offset[1]) / PIXELS_PER_METER
return pymunk.Vec2d(x, y)
3.4 车辆类实现 (vehicle.py - 核心部分)
python
"""
特技赛车车辆类
实现完整的车辆物理模型
"""
import pymunk
import math
import pygame
from utils.config import *
from utils.helpers import clamp, lerp, smooth_step
class StuntCar:
"""特技赛车类"""
def __init__(self, physics_engine, x=0, y=5, config=None):
self.physics = physics_engine
self.config = config or VEHICLE_CONFIG
# 车辆状态
self.state = {
"throttle": 0.0, # 油门 0-1
"brake": 0.0, # 刹车 0-1
"steering": 0.0, # 转向 -1到1
"gear": 1, # 档位
"speed": 0.0, # 速度 m/s
"rpm": 0.0, # 发动机转速
"boost": 1.0, # 助推能量 0-1
"health": 100.0, # 生命值
"is_grounded": False, # 是否接地
"air_time": 0.0, # 空中时间
"flips": 0, # 翻转次数
"stunt_score": 0, # 特技分数
}
# 物理组件
self.chassis = None
self.wheels = [] # 0:前左, 1:前右, 2:后左, 3:后右
self.suspensions = [] # 悬挂约束
self.motors = [] # 轮毂马达
self.steering_joints = [] # 转向关节
# 输入状态
self.input = {
"throttle": 0.0,
"brake": 0.0,
"steering": 0.0,
"boost": False,
}
# 创建车辆
self._create_chassis(x, y)
self._create_wheels()
self._create_suspensions()
self._create_drivetrain()
self._create_steering()
# 助推器
self.boost_active = False
self.boost_timer = 0.0
self.boost_cooldown = 0.0
# 特技追踪
self.stunt_tracker = StuntTracker(self)
# 调试信息
self.debug = {
"suspension_forces": [0, 0, 0, 0],
"wheel_speeds": [0, 0, 0, 0],
"tire_forces": [(0, 0), (0, 0), (0, 0), (0, 0)],
"contact_points": [None, None, None, None],
}
def _create_chassis(self, x, y):
"""创建底盘"""
config = self.config["chassis"]
# 创建底盘刚体
mass = config["mass"]
size = (config["width"], config["height"])
# 计算转动惯量(矩形)
moment = pymunk.moment_for_box(mass, size)
self.chassis = pymunk.Body(mass, moment)
self.chassis.position = (x, y)
self.chassis.center_of_gravity = (0, 0) # 质心在中心
# 创建底盘形状(矩形)
half_width = size[0] / 2
half_height = size[1] / 2
vertices = [
(-half_width, -half_height),
(-half_width, half_height),
(half_width, half_height),
(half_width, -half_height)
]
shape = pymunk.Poly(self.chassis, vertices)
shape.elasticity = config["elasticity"]
shape.friction = config["friction"]
shape.density = config["density"]
shape.color = COLORS["chassis"]
# 设置碰撞类别
shape.filter = pymunk.ShapeFilter(
categories=0b0001, # 车辆类别
mask=0b1110 # 与地面、障碍物、道具碰撞
)
self.physics.add(self.chassis, shape)
# 为CCD存储上一帧位置
self.chassis.previous_position = self.chassis.position
def _create_wheels(self):
"""创建四个车轮"""
config = self.config["wheel"]
chassis_config = self.config["chassis"]
# 车轮位置(相对于底盘中心)
wheelbase = chassis_config["width"] * 0.6 # 轴距
track_width = chassis_config["height"] * 0.8 # 轮距
positions = [
(-wheelbase/2, track_width/2), # 前左
(-wheelbase/2, -track_width/2), # 前右
(wheelbase/2, track_width/2), # 后左
(wheelbase/2, -track_width/2), # 后右
]
for i, (rel_x, rel_y) in enumerate(positions):
# 车轮世界坐标
world_pos = self.chassis.local_to_world((rel_x, rel_y))
# 创建车轮刚体
mass = config["mass"]
radius = config["radius"]
moment = pymunk.moment_for_circle(mass, 0, radius)
wheel = pymunk.Body(mass, moment)
wheel.position = world_pos
# 创建圆形形状
shape = pymunk.Circle(wheel, radius)
shape.elasticity = config["elasticity"]
shape.friction = config["friction"]
shape.color = COLORS["wheel"]
# 车轮只与地面碰撞
shape.filter = pymunk.ShapeFilter(
categories=0b0001, # 车辆类别
mask=0b0010 # 只与地面碰撞
)
# 存储车轮索引
shape.wheel_index = i
self.physics.add(wheel, shape)
self.wheels.append(wheel)
def _create_suspensions(self):
"""创建悬挂系统"""
config = self.config["suspension"]
chassis_config = self.config["chassis"]
# 悬挂锚点(相对于底盘)
wheelbase = chassis_config["width"] * 0.6
track_width = chassis_config["height"] * 0.8
chassis_anchors = [
(-wheelbase/2, track_width/2), # 前左
(-wheelbase/2, -track_width/2), # 前右
(wheelbase/2, track_width/2), # 后左
(wheelbase/2, -track_width/2), # 后右
]
for i, wheel in enumerate(self.wheels):
# 底盘锚点
chassis_anchor = chassis_anchors[i]
# 车轮锚点(车轮中心)
wheel_anchor = (0, 0)
# 创建阻尼弹簧(悬挂)
suspension = pymunk.DampedSpring(
self.chassis, wheel,
chassis_anchor, wheel_anchor,
config["rest_length"],
config["stiffness"],
config["damping"]
)
# 设置最大力
suspension.max_force = config["max_force"]
# 添加到物理空间
self.physics.add_constraint(suspension, f"suspension_{i}")
self.suspensions.append(suspension)
# 创建滑动关节限制悬挂行程
max_compression = config["rest_length"] * 0.5
max_extension = config["rest_length"] * 1.5
slide_joint = pymunk.SlideJoint(
self.chassis, wheel,
chassis_anchor, wheel_anchor,
max_compression, max_extension
)
self.physics.add_constraint(slide_joint, f"suspension_limit_{i}")
def _create_drivetrain(self):
"""创建传动系统"""
config = self.config["drivetrain"]
# 为后轮创建马达(后轮驱动)
for i in [2, 3]: # 后左、后右
# 创建旋转马达
motor = pymunk.SimpleMotor(self.chassis, self.wheels[i], 0)
motor.max_force = config["max_torque"]
self.physics.add_constraint(motor, f"motor_{i}")
self.motors.append({
"constraint": motor,
"wheel_index": i,
"is_driven": True,
"brake_torque": 0,
})
# 前轮自由旋转(转向轮)
for i in [0, 1]:
motor = pymunk.SimpleMotor(self.chassis, self.wheels[i], 0)
motor.max_force = 0 # 无驱动力
self.physics.add_constraint(motor, f"motor_{i}")
self.motors.append({
"constraint": motor,
"wheel_index": i,
"is_driven": False,
"brake_torque": 0,
})
def _create_steering(self):
"""创建转向系统"""
config = self.config["steering"]
# 为前轮创建旋转限制关节
for i in [0, 1]: # 前左、前右
# 创建旋转限制关节
# 注意:我们实际上需要旋转关节+限制,这里简化处理
pivot = pymunk.PivotJoint(self.chassis, self.wheels[i], (0, 0))
# 添加旋转限制
rotary_limit = pymunk.RotaryLimitJoint(
self.chassis, self.wheels[i],
-config["max_angle"], config["max_angle"]
)
self.physics.add_constraint(pivot, f"steering_pivot_{i}")
self.physics.add_constraint(rotary_limit, f"steering_limit_{i}")
self.steering_joints.append({
"pivot": pivot,
"limit": rotary_limit,
"wheel_index": i,
"target_angle": 0,
"current_angle": 0,
})
def update(self, dt):
"""更新车辆状态"""
# 更新输入响应
self._update_input(dt)
# 更新传动系统
self._update_drivetrain(dt)
# 更新转向系统
self._update_steering(dt)
# 更新助推器
self._update_boost(dt)
# 更新空气动力学
self._update_aerodynamics(dt)
# 更新车轮状态
self._update_wheels(dt)
# 更新特技追踪
self.stunt_tracker.update(dt)
# 更新车辆状态
self._update_state(dt)
# 更新调试信息
self._update_debug_info()
def _update_input(self, dt):
"""更新输入响应"""
config = self.config["steering"]
# 平滑转向输入
steering_input = self.input["steering"]
current_steering = self.state["steering"]
# 转向响应
steering_response = config["response_speed"]
if abs(steering_input) > 0.1:
# 有输入时快速响应
new_steering = lerp(current_steering, steering_input, steering_response * dt)
else:
# 无输入时缓慢回中
new_steering = lerp(current_steering, 0, steering_response * 0.5 * dt)
self.state["steering"] = clamp(new_steering, -1, 1)
# 油门和刹车
self.state["throttle"] = clamp(self.input["throttle"], 0, 1)
self.state["brake"] = clamp(self.input["brake"], 0, 1)
# 助推输入
if self.input["boost"] and self.state["boost"] > 0.1 and self.boost_cooldown <= 0:
self.boost_active = True
self.boost_timer = GAME_CONFIG["boost_duration"]
def _update_drivetrain(self, dt):
"""更新传动系统"""
config = self.config["drivetrain"]
throttle = self.state["throttle"]
brake = self.state["brake"]
# 计算发动机输出扭矩
engine_torque = 0
if throttle > 0:
# 简单发动机模型
max_torque = config["max_torque"]
# 扭矩曲线(简化)
rpm = self.state["rpm"]
peak_rpm = 5000
torque_curve = 1.0 - abs(rpm - peak_rpm) / peak_rpm
torque_curve = clamp(torque_curve, 0.3, 1.0)
engine_torque = throttle * max_torque * torque_curve
# 计算刹车扭矩
brake_torque = brake * config["brake_torque"]
# 应用扭矩到驱动轮
for motor_info in self.motors:
if motor_info["is_driven"]:
# 驱动轮:发动机扭矩 - 刹车扭矩
net_torque = engine_torque - brake_torque
motor_info["constraint"].rate = net_torque * dt * 0.1 # 简化
# 存储刹车扭矩用于轮胎力计算
motor_info["brake_torque"] = brake_torque
def _update_steering(self, dt):
"""更新转向系统"""
if not self.steering_joints:
return
steering = self.state["steering"]
max_angle = self.config["steering"]["max_angle"]
# 计算目标角度
target_angle = steering * max_angle
# 阿克曼转向几何
wheelbase = self.config["chassis"]["width"] * 0.6
track_width = self.config["chassis"]["height"] * 0.8
if abs(target_angle) > 0.001:
turn_radius = wheelbase / math.tan(target_angle)
# 内轮转角(前左)
inner_angle = math.atan(wheelbase / (turn_radius - track_width/2))
inner_angle = clamp(inner_angle, -max_angle, max_angle)
# 外轮转角(前右)
outer_angle = math.atan(wheelbase / (turn_radius + track_width/2))
outer_angle = clamp(outer_angle, -max_angle, max_angle)
# 根据转向方向分配角度
if target_angle > 0: # 右转
angles = [inner_angle, outer_angle] # 左内右外
else: # 左转
angles = [outer_angle, -inner_angle] # 左外右内
else:
angles = [0, 0]
# 应用转向角度
for i, joint_info in enumerate(self.steering_joints):
if i < 2: # 前轮
# 平滑转向
current_angle = joint_info["current_angle"]
target = angles[i]
# 转向速度限制
max_steer_speed = max_angle * 5 # 弧度/秒
angle_diff = target - current_angle
max_change = max_steer_speed * dt
if abs(angle_diff) > max_change:
new_angle = current_angle + math.copysign(max_change, angle_diff)
else:
new_angle = target
joint_info["current_angle"] = new_angle
joint_info["target_angle"] = target
# 应用角度到关节
# 注意:这里简化处理,实际需要更复杂的关节设置
# 对于PivotJoint,我们需要控制相对角度
def _update_boost(self, dt):
"""更新助推器"""
if self.boost_active:
self.boost_timer -= dt
if self.boost_timer <= 0:
self.boost_active = False
self.boost_cooldown = GAME_CONFIG["boost_cooldown"]
else:
# 应用助推力
boost_power = GAME_CONFIG["boost_power"]
boost_direction = pymunk.Vec2d(math.cos(self.chassis.angle),
math.sin(self.chassis.angle))
self.chassis.apply_force_at_local_point(boost_direction * boost_power)
# 消耗助推能量
self.state["boost"] -= dt / GAME_CONFIG["boost_duration"]
else:
if self.boost_cooldown > 0:
self.boost_cooldown -= dt
# 恢复助推能量
if self.state["boost"] < 1.0:
recharge_rate = 1.0 / GAME_CONFIG["boost_cooldown"]
self.state["boost"] += recharge_rate * dt
self.state["boost"] = min(self.state["boost"], 1.0)
def _update_aerodynamics(self, dt):
"""更新空气动力学"""
config = self.config["aerodynamics"]
velocity = self.chassis.velocity
# 空气阻力
speed_sq = velocity.length_sq
drag_force = -velocity.normalized() * speed_sq * config["drag_coefficient"]
self.chassis.apply_force_at_local_point(drag_force)
# 下压力(需要接触点)
if self.state["is_grounded"]:
# 简化的下压力模型
downforce = speed_sq * config["downforce_coefficient"]
# 在下压力作用点施加力(前部和后部)
wheelbase = self.config["chassis"]["width"] * 0.6
# 前部下压力
front_point = (-wheelbase/2, 0)
self.chassis.apply_force_at_local_point((0, -downforce * 0.6), front_point)
# 后部下压力
rear_point = (wheelbase/2, 0)
self.chassis.apply_force_at_local_point((0, -downforce * 0.4), rear_point)
def _update_wheels(self, dt):
"""更新车轮状态和轮胎力"""
# 检测车轮接地状态
grounded_wheels = 0
for i, wheel in enumerate(self.wheels):
# 执行射线检测检查接地
wheel_pos = wheel.position
ray_start = wheel_pos
ray_end = wheel_pos + (0, -self.config["wheel"]["radius"] * 1.5)
# 只检测地面
shape_filter = pymunk.ShapeFilter(mask=0b0010)
result = self.physics.space.segment_query_first(ray_start, ray_end, 0, shape_filter)
if result:
# 车轮接地
grounded_wheels += 1
contact_point = result.point
contact_normal = result.normal
# 计算轮胎力
tire_force = self._calculate_tire_force(i, wheel, contact_point, contact_normal)
# 应用轮胎力
if tire_force:
longitudinal, lateral = tire_force
# 应用力到车轮
force_vector = pymunk.Vec2d(lateral, longitudinal)
wheel.apply_force_at_local_point(force_vector)
# 反作用力到底盘
self.chassis.apply_force_at_local_point(-force_vector)
# 存储调试信息
self.debug["contact_points"][i] = contact_point
else:
# 车轮悬空
self.debug["contact_points"][i] = None
# 更新接地状态
self.state["is_grounded"] = grounded_wheels > 0
self.state["air_time"] = 0 if self.state["is_grounded"] else self.state["air_time"] + dt
def _calculate_tire_force(self, wheel_index, wheel, contact_point, contact_normal):
"""计算轮胎力(简化模型)"""
# 获取车轮数据
wheel_config = self.config["wheel"]
friction = wheel_config["friction"]
# 计算滑移率
wheel_velocity = wheel.velocity_at_local_point((0, 0))
chassis_velocity = self.chassis.velocity_at_local_point((0, 0))
# 接触点速度
contact_velocity = (wheel_velocity + chassis_velocity) * 0.5
# 纵向滑移
longitudinal_speed = contact_velocity.dot(contact_normal)
slip_ratio = 0
if abs(longitudinal_speed) > 0.1:
wheel_surface_speed = wheel.angular_velocity * wheel_config["radius"]
slip_ratio = (wheel_surface_speed - longitudinal_speed) / abs(longitudinal_speed)
# 侧向滑移(转向角)
steering_angle = 0
if wheel_index < 2: # 前轮
steering_angle = self.steering_joints[wheel_index]["current_angle"]
# 简化轮胎模型
slip_angle = steering_angle
# 垂直载荷(悬挂力)
suspension_force = 0
if wheel_index < len(self.suspensions):
suspension = self.suspensions[wheel_index]
suspension_force = abs(suspension.impulse) / self.physics.fixed_dt
# 魔术公式简化版
B = 10.0
C = 1.9
D = friction * (suspension_force + 100) # 基础载荷
E = 0.97
total_slip = math.sqrt(slip_ratio**2 + math.sin(slip_angle)**2)
if total_slip < 0.001:
return (0, 0)
normalized_slip = B * total_slip
mu = D * math.sin(C * math.atan(normalized_slip - E * (normalized_slip - math.atan(normalized_slip))))
total_force = mu * (suspension_force + 100)
# 分解力
if total_slip > 0:
longitudinal = total_force * slip_ratio / total_slip
lateral = total_force * math.sin(slip_angle) / total_slip
else:
longitudinal = 0
lateral = 0
# 存储调试信息
self.debug["tire_forces"][wheel_index] = (longitudinal, lateral)
return longitudinal, lateral
def _update_state(self, dt):
"""更新车辆状态信息"""
# 速度
velocity = self.chassis.velocity
self.state["speed"] = velocity.length
# RPM(简化计算)
wheel_speed = 0
for wheel in self.wheels[2:]: # 后轮
wheel_speed += abs(wheel.angular_velocity)
avg_wheel_speed = wheel_speed / 2 if len(self.wheels) > 2 else 0
gear_ratio = self.config["drivetrain"]["differential_ratio"]
self.state["rpm"] = avg_wheel_speed * gear_ratio * 60 / (2 * math.pi)
# 生命值恢复
if self.state["health"] < 100 and self.state["is_grounded"]:
self.state["health"] += GAME_CONFIG["health_regen"] * dt
self.state["health"] = min(self.state["health"], 100)
def _update_debug_info(self):
"""更新调试信息"""
# 悬挂力
for i, suspension in enumerate(self.suspensions):
if i < 4:
force = abs(suspension.impulse) / self.physics.fixed_dt
self.debug["suspension_forces"][i] = force
# 车轮速度
for i, wheel in enumerate(self.wheels):
if i < 4:
self.debug["wheel_speeds"][i] = wheel.angular_velocity
def apply_input(self, throttle=0, brake=0, steering=0, boost=False):
"""应用玩家输入"""
self.input["throttle"] = throttle
self.input["brake"] = brake
self.input["steering"] = steering
self.input["boost"] = boost
def get_wheel_position(self, index, interpolated=True):
"""获取车轮位置(屏幕坐标)"""
if index < 0 or index >= len(self.wheels):
return (0, 0)
wheel = self.wheels[index]
if interpolated:
pos = self.physics.get_interpolated_position(wheel)
else:
pos = wheel.position
return self.physics.world_to_screen(pos)
def get_chassis_position(self, interpolated=True):
"""获取底盘位置(屏幕坐标)"""
if interpolated:
pos = self.physics.get_interpolated_position(self.chassis)
else:
pos = self.chassis.position
return self.physics.world_to_screen(pos)
def draw(self, screen, camera_offset=(0, 0), debug=False):
"""绘制车辆"""
# 绘制悬挂(弹簧)
for i, suspension in enumerate(self.suspensions):
if i < 4:
# 获取锚点位置
chassis_pos = self.chassis.position + suspension.a
wheel_pos = self.wheels[i].position + suspension.b
# 转换为屏幕坐标
screen_a = self.physics.world_to_screen(chassis_pos, camera_offset)
screen_b = self.physics.world_to_screen(wheel_pos, camera_offset)
# 绘制弹簧
pygame.draw.line(screen, COLORS["suspension"], screen_a, screen_b, 3)
# 绘制车轮
for i, wheel in enumerate(self.wheels):
if i < 4:
pos = self.get_wheel_position(i, interpolated=True)
radius = self.config["wheel"]["radius"] * PIXELS_PER_METER
# 车轮主体
pygame.draw.circle(screen, COLORS["wheel"],
(int(pos[0]), int(pos[1])), int(radius))
# 轮胎花纹
angle = wheel.angle
dir_x = math.cos(angle) * radius
dir_y = -math.sin(angle) * radius # Y轴翻转
pygame.draw.line(screen, (100, 100, 100),
(int(pos[0] - dir_x), int(pos[1] - dir_y)),
(int(pos[0] + dir_x), int(pos[1] + dir_y)), 2)
# 绘制底盘
chassis_pos = self.get_chassis_position(interpolated=True)
width = self.config["chassis"]["width"] * PIXELS_PER_METER
height = self.config["chassis"]["height"] * PIXELS_PER_METER
angle = self.chassis.angle
# 创建旋转后的矩形
rect = pygame.Rect(0, 0, width, height)
rect.center = (int(chassis_pos[0]), int(chassis_pos[1]))
# 绘制底盘(使用旋转后的表面)
chassis_surface = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.rect(chassis_surface, COLORS["chassis"], (0, 0, width, height), border_radius=5)
# 绘制车辆方向
dir_length = width * 0.6
dir_x = math.cos(angle) * dir_length
dir_y = -math.sin(angle) * dir_length
pygame.draw.line(chassis_surface, (255, 255, 0),
(width/2, height/2), (width/2 + dir_x, height/2 + dir_y), 3)
# 旋转并绘制
rotated_surface = pygame.transform.rotate(chassis_surface, math.degrees(-angle))
rotated_rect = rotated_surface.get_rect(center=rect.center)
screen.blit(rotated_surface, rotated_rect)
# 绘制助推效果
if self.boost_active:
boost_pos = (chassis_pos[0] + math.cos(angle) * width * 0.6,
chassis_pos[1] - math.sin(angle) * width * 0.6)
# 助推粒子效果
boost_size = 20 + 10 * math.sin(pygame.time.get_ticks() * 0.01)
for i in range(5):
offset = (random.uniform(-10, 10), random.uniform(-5, 5))
particle_pos = (boost_pos[0] + offset[0], boost_pos[1] + offset[1])
particle_size = boost_size * random.uniform(0.5, 1.5)
# 创建渐变表面
particle_surface = pygame.Surface((int(particle_size*2), int(particle_size*2)), pygame.SRCALPHA)
for r in range(int(particle_size), 0, -1):
alpha = int(200 * (r / particle_size))
color = (COLORS["boost"][0], COLORS["boost"][1], COLORS["boost"][2], alpha)
pygame.draw.circle(particle_surface, color,
(int(particle_size), int(particle_size)), r)
screen.blit(particle_surface,
(particle_pos[0] - particle_size, particle_pos[1] - particle_size))
# 调试绘制
if debug:
self._draw_debug(screen, camera_offset)
def _draw_debug(self, screen, camera_offset):
"""绘制调试信息"""
font = pygame.font.SysFont(None, 20)
# 绘制接触点
for i, contact_point in enumerate(self.debug["contact_points"]):
if contact_point:
screen_pos = self.physics.world_to_screen(contact_point, camera_offset)
pygame.draw.circle(screen, (0, 255, 0),
(int(screen_pos[0]), int(screen_pos[1])), 5)
# 显示轮胎力
longitudinal, lateral = self.debug["tire_forces"][i]
force_text = f"F:{longitudinal:.0f},{lateral:.0f}"
text = font.render(force_text, True, (255, 255, 255))
screen.blit(text, (screen_pos[0] + 10, screen_pos[1] + i*20))
# 绘制速度向量
chassis_pos = self.get_chassis_position(interpolated=True)
velocity = self.chassis.velocity * PIXELS_PER_METER * 0.1 # 缩放
end_pos = (chassis_pos[0] + velocity.x, chassis_pos[1] - velocity.y) # Y轴翻转
pygame.draw.line(screen, (255, 255, 0), chassis_pos, end_pos, 2)
# 显示车辆状态
state_text = [
f"速度: {self.state['speed']*3.6:.1f} km/h",
f"转速: {self.state['rpm']:.0f} RPM",
f"油门: {self.state['throttle']:.2f}",
f"刹车: {self.state['brake']:.2f}",
f"转向: {self.state['steering']:.2f}",
f"助推: {self.state['boost']:.1%}",
f"空中: {self.state['air_time']:.2f}s",
f"生命: {self.state['health']:.0f}",
]
for i, text in enumerate(state_text):
surface = font.render(text, True, (255, 255, 255))
screen.blit(surface, (10, 10 + i*20))
class StuntTracker:
"""特技追踪与评分系统"""
def __init__(self, vehicle):
self.vehicle = vehicle
self.state = {
"current_stunt": None,
"stunt_start_time": 0,
"stunt_score": 0,
"combo_multiplier": 1.0,
"flips_in_air": 0,
"rotation_in_air": 0,
"max_height": 0,
"last_ground_time": 0,
}
self.active_tricks = set()
self.completed_tricks = []
def update(self, dt):
"""更新特技追踪"""
chassis = self.vehicle.chassis
is_grounded = self.vehicle.state["is_grounded"]
if not is_grounded:
# 空中特技检测
self._track_aerial_stunts(dt)
else:
# 着陆,结算特技
if self.state["current_stunt"]:
self._complete_stunt()
# 重置空中特技状态
self.state["flips_in_air"] = 0
self.state["rotation_in_air"] = 0
self.state["max_height"] = chassis.position.y
# 更新连击乘数
self._update_combo(dt)
def _track_aerial_stunts(self, dt):
"""追踪空中特技"""
chassis = self.vehicle.chassis
# 追踪高度
self.state["max_height"] = max(self.state["max_height"], chassis.position.y)
# 检测翻转
angular_velocity = chassis.angular_velocity
if abs(angular_velocity) > 2.0: # 快速旋转
self.state["rotation_in_air"] += angular_velocity * dt
# 每2π弧度记录一次翻转
if abs(self.state["rotation_in_air"]) >= 2 * math.pi:
self.state["flips_in_air"] += 1
self.state["rotation_in_air"] %= 2 * math.pi
# 记录翻转特技
self.active_tricks.add("flip")
def _complete_stunt(self):
"""完成特技并计算分数"""
stunt = self.state["current_stunt"]
air_time = self.vehicle.state["air_time"]
height = self.state["max_height"]
# 基础分数
base_score = 0
if "flip" in self.active_tricks:
# 翻转分数
flip_score = self.state["flips_in_air"] * 100
flip_multiplier = GAME_CONFIG["stunt_multipliers"]["flip"]
base_score += flip_score * flip_multiplier
# 空中时间加分
air_time_score = air_time * 50
air_multiplier = GAME_CONFIG["stunt_multipliers"]["air_time"]
base_score += air_time_score * air_multiplier
# 高度加分
height_score = max(0, height - 5) * 20
base_score += height_score
# 连击乘数
total_score = base_score * self.state["combo_multiplier"]
# 记录完成的特技
self.completed_tricks.append({
"name": stunt or "Aerial",
"score": total_score,
"flips": self.state["flips_in_air"],
"air_time": air_time,
"height": height,
})
# 更新车辆特技分数
self.vehicle.state["stunt_score"] += total_score
# 增加连击乘数
self.state["combo_multiplier"] *= GAME_CONFIG["stunt_multipliers"]["combo"]
self.state["combo_multiplier"] = min(self.state["combo_multiplier"], 5.0)
# 重置当前特技
self.state["current_stunt"] = None
self.active_tricks.clear()
# 打印特技得分
print(f"特技完成: {stunt or 'Aerial'} - 得分: {total_score:.0f} (连击x{self.state['combo_multiplier']:.1f})")
def _update_combo(self, dt):
"""更新连击状态"""
# 如果一段时间没有特技,重置连击
COMBO_TIMEOUT = 5.0 # 5秒连击超时
if self.completed_tricks:
last_trick_time = self.completed_tricks[-1].get("time", 0)
current_time = pygame.time.get_ticks() / 1000.0
if current_time - last_trick_time > COMBO_TIMEOUT:
self.state["combo_multiplier"] = 1.0
self.completed_tricks.clear() # 清空连击列表
3.5 地形生成 (terrain.py)
python
"""
地形生成与管理
创建可交互的物理地形
"""
import pymunk
import pygame
import noise
import random
import math
from utils.config import *
from utils.helpers import lerp, smooth_step
class Terrain:
"""物理地形类"""
def __init__(self, physics_engine, width=TERRAIN_CONFIG["width"],
height_variance=TERRAIN_CONFIG["height_variance"]):
self.physics = physics_engine
self.width = width
self.height_variance = height_variance
# 地形数据
self.segments = []
self.ground_shapes = []
self.ground_body = None
# 生成地形
self._generate_terrain()
# 添加障碍物
self._add_obstacles()
# 添加跳台
self._add_jumps()
def _generate_terrain(self):
"""使用Perlin噪声生成地形"""
config = TERRAIN_CONFIG
seed = config["seed"]
# 创建地面刚体(静态)
self.ground_body = pymunk.Body(body_type=pymunk.Body.STATIC)
# 生成地形高度图
base_height = 5.0 # 基础高度(米)
points = []
# 使用Perlin噪声生成高度
for x in range(0, int(self.width), int(config["segment_length"] * PIXELS_PER_METER)):
# 转换为米
x_m = x / PIXELS_PER_METER
# 多频噪声叠加
height = base_height
height += noise.pnoise1(x_m * 0.1 + seed, 4) * self.height_variance
# 添加一些陡坡
if 20 < x_m < 40:
height += noise.pnoise1(x_m * 0.5 + seed, 2) * 3
# 添加一些平坦区域
if 60 < x_m < 80:
height = base_height
points.append((x_m, height))
# 确保起点和终点平坦
points[0] = (points[0][0], base_height)
points[-1] = (points[-1][0], base_height)
# 创建地形形状
for i in range(len(points) - 1):
x1, y1 = points[i]
x2, y2 = points[i + 1]
# 创建线段形状
segment = pymunk.Segment(self.ground_body, (x1, y1), (x2, y2), 0.5)
segment.elasticity = 0.6
segment.friction = 1.2 # 高摩擦模拟地面
# 设置碰撞类别
segment.filter = pymunk.ShapeFilter(
categories=0b0010, # 地面类别
mask=0b0001 # 只与车辆碰撞
)
# 存储地形颜色(根据高度)
height_ratio = (y1 + y2) / 2 / (base_height + self.height_variance)
if height_ratio < 1.1:
segment.color = COLORS["ground"]
else:
# 较高区域使用草的颜色
segment.color = COLORS["grass"]
self.ground_shapes.append(segment)
self.segments.append(((x1, y1), (x2, y2)))
# 添加到物理空间
self.physics.add(self.ground_body, *self.ground_shapes)
def _add_obstacles(self):
"""添加障碍物"""
# 随机添加一些障碍物
num_obstacles = random.randint(5, 10)
for _ in range(num_obstacles):
# 随机位置
x = random.uniform(10, self.width - 10)
y = self._get_height_at(x) + random.uniform(1, 3)
# 随机类型
obstacle_type = random.choice(["box", "triangle", "circle"])
if obstacle_type == "box":
self._create_box_obstacle(x, y)
elif obstacle_type == "triangle":
self._create_triangle_obstacle(x, y)
else: # circle
self._create_circle_obstacle(x, y)
def _create_box_obstacle(self, x, y):
"""创建盒子障碍物"""
width = random.uniform(1, 3)
height = random.uniform(1, 3)
# 创建刚体
mass = width * height * 100 # 根据体积计算质量
moment = pymunk.moment_for_box(mass, (width, height))
body = pymunk.Body(mass, moment)
body.position = (x, y)
# 创建形状
half_width = width / 2
half_height = height / 2
vertices = [
(-half_width, -half_height),
(-half_width, half_height),
(half_width, half_height),
(half_width, -half_height)
]
shape = pymunk.Poly(body, vertices)
shape.elasticity = 0.3
shape.friction = 0.8
shape.color = (139, 69, 19) # 棕色
# 设置碰撞类别
shape.filter = pymunk.ShapeFilter(
categories=0b0100, # 障碍物类别
mask=0b0001 # 只与车辆碰撞
)
self.physics.add(body, shape)
def _create_triangle_obstacle(self, x, y):
"""创建三角形障碍物"""
size = random.uniform(1, 2)
mass = size * 100
vertices = [
(0, -size),
(-size, size),
(size, size)
]
moment = pymunk.moment_for_poly(mass, vertices)
body = pymunk.Body(mass, moment)
body.position = (x, y)
shape = pymunk.Poly(body, vertices)
shape.elasticity = 0.5
shape.friction = 0.6
shape.color = (128, 128, 128) # 灰色
shape.filter = pymunk.ShapeFilter(
categories=0b0100,
mask=0b0001
)
self.physics.add(body, shape)
def _create_circle_obstacle(self, x, y):
"""创建圆形障碍物"""
radius = random.uniform(0.5, 1.5)
mass = radius * 200
moment = pymunk.moment_for_circle(mass, 0, radius)
body = pymunk.Body(mass, moment)
body.position = (x, y)
shape = pymunk.Circle(body, radius)
shape.elasticity = 0.7
shape.friction = 0.5
shape.color = (105, 105, 105) # 深灰色
shape.filter = pymunk.ShapeFilter(
categories=0b0100,
mask=0b0001
)
self.physics.add(body, shape)
def _add_jumps(self):
"""添加跳台"""
# 添加一些固定跳台
jumps = [
(30, 45, 15), # 位置, 宽度, 高度
(80, 60, 20),
(120, 40, 25),
]
for x, width, height in jumps:
self._create_jump_ramp(x, width, height)
def _create_jump_ramp(self, x, width, height):
"""创建跳台斜坡"""
# 起始点
start_x = x
start_y = self._get_height_at(start_x)
# 结束点
end_x = x + width
end_y = start_y + height
# 创建斜坡形状
segment = pymunk.Segment(self.ground_body, (start_x, start_y), (end_x, end_y), 0.5)
segment.elasticity = 0.8
segment.friction = 0.9
segment.color = (255, 140, 0) # 橙色
segment.filter = pymunk.ShapeFilter(
categories=0b0010,
mask=0b0001
)
# 添加着陆平台
platform_length = 20
platform_start = (end_x, end_y)
platform_end = (end_x + platform_length, end_y)
platform = pymunk.Segment(self.ground_body, platform_start, platform_end, 0.5)
platform.elasticity = 0.6
platform.friction = 1.0
platform.color = COLORS["ground"]
platform.filter = pymunk.ShapeFilter(
categories=0b0010,
mask=0b0001
)
self.physics.add(segment, platform)
self.ground_shapes.extend([segment, platform])
# 记录跳台
self.segments.append(((start_x, start_y), (end_x, end_y)))
self.segments.append((platform_start, platform_end))
def _get_height_at(self, x):
"""获取地形在x处的高度"""
for (x1, y1), (x2, y2) in self.segments:
if x1 <= x <= x2 or x2 <= x <= x1:
# 线性插值
t = (x - x1) / (x2 - x1) if x2 != x1 else 0
return y1 + (y2 - y1) * t
# 如果x超出范围,返回默认高度
return 5.0
def draw(self, screen, camera_offset=(0, 0)):
"""绘制地形"""
# 绘制背景
screen.fill(COLORS["sky"])
# 绘制地面
for segment in self.ground_shapes:
if hasattr(segment, 'a'):
# 线段
a = segment.a
b = segment.b
# 转换为屏幕坐标
screen_a = self.physics.world_to_screen(a, camera_offset)
screen_b = self.physics.world_to_screen(b, camera_offset)
# 根据颜色绘制
color = getattr(segment, 'color', COLORS["ground"])
pygame.draw.line(screen, color, screen_a, screen_b, 10) # 10像素宽
# 绘制地面阴影
shadow_color = (color[0]//2, color[1]//2, color[2]//2)
pygame.draw.line(screen, shadow_color,
(screen_a[0], screen_a[1] + 3),
(screen_b[0], screen_b[1] + 3), 8)
# 绘制网格(用于距离参考)
self._draw_grid(screen, camera_offset)
def _draw_grid(self, screen, camera_offset):
"""绘制距离网格"""
grid_size = 10 # 米
grid_color = (255, 255, 255, 30) # 半透明白色
# 计算可见范围
screen_width_m = SCREEN_WIDTH / PIXELS_PER_METER
screen_height_m = SCREEN_HEIGHT / PIXELS_PER_METER
camera_x = -camera_offset[0] / PIXELS_PER_METER
camera_y = -camera_offset[1] / PIXELS_PER_METER
start_x = int(camera_x / grid_size) * grid_size
end_x = int((camera_x + screen_width_m) / grid_size) * grid_size + grid_size
start_y = int(camera_y / grid_size) * grid_size
end_y = int((camera_y + screen_height_m) / grid_size) * grid_size + grid_size
# 绘制垂直线
for x in range(int(start_x), int(end_x), grid_size):
screen_x = x * PIXELS_PER_METER + camera_offset[0]
pygame.draw.line(screen, grid_color,
(screen_x, 0), (screen_x, SCREEN_HEIGHT), 1)
# 绘制水平线
for y in range(int(start_y), int(end_y), grid_size):
screen_y = SCREEN_HEIGHT - (y * PIXELS_PER_METER + camera_offset[1])
pygame.draw.line(screen, grid_color,
(0, screen_y), (SCREEN_WIDTH, screen_y), 1)
# 绘制坐标标签
font = pygame.font.SysFont(None, 20)
for x in range(int(start_x), int(end_x), grid_size * 2):
for y in range(int(start_y), int(end_y), grid_size * 2):
screen_x = x * PIXELS_PER_METER + camera_offset[0] + 5
screen_y = SCREEN_HEIGHT - (y * PIXELS_PER_METER + camera_offset[1]) - 20
label = f"{x},{y}"
text = font.render(label, True, grid_color)
screen.blit(text, (screen_x, screen_y))
3.6 智能相机系统 (camera.py)
python
"""
智能相机系统
跟随车辆并提供平滑的运动
"""
import pygame
import math
from utils.config import *
from utils.helpers import clamp, lerp, smooth_step
class Camera:
"""智能相机类"""
def __init__(self, width=SCREEN_WIDTH, height=SCREEN_HEIGHT):
self.width = width
self.height = height
# 相机位置
self.position = pygame.Vector2(0, 0)
self.target_position = pygame.Vector2(0, 0)
# 相机缩放
self.zoom = 1.0
self.target_zoom = 1.0
# 平滑参数
self.smoothness = CAMERA_CONFIG["smoothness"]
self.look_ahead = CAMERA_CONFIG["look_ahead"]
# 相机限制
self.bounds = None
self.shake_intensity = 0
self.shake_decay = 0.9
# 轨迹预测
self.trajectory_points = []
self.max_trajectory_points = 20
def update(self, target_pos, target_vel, dt):
"""更新相机位置"""
# 计算目标位置(包括前瞻)
look_ahead = target_vel * self.look_ahead
self.target_position = pygame.Vector2(target_pos.x, target_pos.y) + look_ahead
# 平滑移动
self.position.x = lerp(self.position.x, self.target_position.x, self.smoothness)
self.position.y = lerp(self.position.y, self.target_position.y, self.smoothness)
# 应用屏幕震动
if self.shake_intensity > 0.1:
import random
self.position.x += random.uniform(-1, 1) * self.shake_intensity
self.position.y += random.uniform(-1, 1) * self.shake_intensity
self.shake_intensity *= self.shake_decay
# 应用边界限制
if self.bounds:
self.position.x = clamp(self.position.x, self.bounds.left, self.bounds.right)
self.position.y = clamp(self.position.y, self.bounds.top, self.bounds.bottom)
# 动态缩放
speed = target_vel.length()
target_zoom = 1.0 - min(speed * 0.01, 0.3) # 速度越快,视野越广
# 平滑缩放
self.zoom = lerp(self.zoom, target_zoom, 0.1)
self.zoom = clamp(self.zoom, CAMERA_CONFIG["zoom_min"], CAMERA_CONFIG["zoom_max"])
# 更新轨迹预测
self._update_trajectory(target_pos, target_vel, dt)
def _update_trajectory(self, target_pos, target_vel, dt):
"""更新车辆轨迹预测"""
# 基于当前速度和重力预测轨迹
gravity = pygame.Vector2(0, 9.8)
time_step = 0.1
max_time = 3.0
self.trajectory_points = []
pos = pygame.Vector2(target_pos.x, target_pos.y)
vel = pygame.Vector2(target_vel.x, target_vel.y)
for t in range(int(max_time / time_step)):
# 计算下一位置
pos += vel * time_step
vel += gravity * time_step
# 存储轨迹点
self.trajectory_points.append(pos.copy())
# 如果预测点低于地面,停止
if pos.y < 0:
break
def add_shake(self, intensity):
"""添加屏幕震动"""
self.shake_intensity = min(self.shake_intensity + intensity, 10.0)
def get_offset(self):
"""获取相机偏移量(用于渲染)"""
# 计算基于缩放的中心偏移
offset_x = self.position.x - self.width / (2 * self.zoom)
offset_y = self.position.y - self.height / (2 * self.zoom)
return pygame.Vector2(offset_x, offset_y)
def world_to_screen(self, world_pos):
"""世界坐标转屏幕坐标"""
offset = self.get_offset()
# 应用缩放和平移
screen_x = (world_pos.x - offset.x) * self.zoom
screen_y = (world_pos.y - offset.y) * self.zoom
return pygame.Vector2(screen_x, screen_y)
def screen_to_world(self, screen_pos):
"""屏幕坐标转世界坐标"""
offset = self.get_offset()
# 反向变换
world_x = screen_pos.x / self.zoom + offset.x
world_y = screen_pos.y / self.zoom + offset.y
return pygame.Vector2(world_x, world_y)
def set_bounds(self, left, top, right, bottom):
"""设置相机边界"""
self.bounds = pygame.Rect(left, top, right - left, bottom - top)
def draw_trajectory(self, screen, terrain):
"""绘制轨迹预测"""
if len(self.trajectory_points) < 2:
return
# 绘制轨迹线
points = []
for i, point in enumerate(self.trajectory_points):
# 检查是否与地形相交
terrain_height = terrain._get_height_at(point.x)
if point.y < terrain_height:
# 碰到地面,停止绘制
break
# 转换为屏幕坐标
screen_pos = self.world_to_screen(point)
points.append((screen_pos.x, screen_pos.y))
if len(points) >= 2:
# 绘制轨迹线
for i in range(len(points) - 1):
alpha = int(255 * (1 - i / len(points)))
color = (255, 255, 0, alpha)
# 绘制线段
pygame.draw.line(screen, color, points[i], points[i+1], 2)
# 绘制轨迹点
for i, point in enumerate(points[::5]): # 每5个点绘制一个
alpha = int(255 * (1 - i / (len(points) / 5)))
color = (255, 165, 0, alpha)
pygame.draw.circle(screen, color, (int(point[0]), int(point[1])), 3)
def draw_debug(self, screen):
"""绘制相机调试信息"""
font = pygame.font.SysFont(None, 20)
info = [
f"相机位置: ({self.position.x:.1f}, {self.position.y:.1f})",
f"相机缩放: {self.zoom:.2f}",
f"震动强度: {self.shake_intensity:.2f}",
]
for i, text in enumerate(info):
surface = font.render(text, True, (255, 255, 255))
screen.blit(surface, (SCREEN_WIDTH - 200, 10 + i * 20))
3.7 用户界面 (ui.py)
python
"""
用户界面
显示游戏状态和控制信息
"""
import pygame
import math
from utils.config import *
class UI:
"""用户界面类"""
def __init__(self):
# 字体
self.font_small = pygame.font.SysFont(None, 24)
self.font_medium = pygame.font.SysFont(None, 32)
self.font_large = pygame.font.SysFont(None, 48)
# UI状态
self.show_debug = False
self.show_help = False
# 动画
self.animation_time = 0
self.message_queue = []
# 创建UI表面
self.ui_surface = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
def update(self, dt):
"""更新UI状态"""
self.animation_time += dt
# 更新消息队列
for msg in self.message_queue[:]:
msg["time"] += dt
if msg["time"] > msg["duration"]:
self.message_queue.remove(msg)
def draw(self, screen, vehicle_state, camera, performance_stats):
"""绘制用户界面"""
# 清除UI表面
self.ui_surface.fill((0, 0, 0, 0))
# 绘制HUD
self._draw_hud(screen, vehicle_state)
# 绘制调试信息
if self.show_debug:
self._draw_debug_info(screen, vehicle_state, camera, performance_stats)
# 绘制帮助信息
if self.show_help:
self._draw_help(screen)
# 绘制消息
self._draw_messages(screen)
# 将UI表面绘制到屏幕上
screen.blit(self.ui_surface, (0, 0))
def _draw_hud(self, screen, vehicle_state):
"""绘制平视显示器"""
# 速度表
speed_kmh = vehicle_state["speed"] * 3.6
self._draw_speedometer(screen, speed_kmh, 50, SCREEN_HEIGHT - 100)
# 转速表
rpm = vehicle_state["rpm"]
self._draw_tachometer(screen, rpm, 200, SCREEN_HEIGHT - 100)
# 助推能量
boost = vehicle_state["boost"]
self._draw_boost_meter(screen, boost, SCREEN_WIDTH - 150, SCREEN_HEIGHT - 100)
# 生命值
health = vehicle_state["health"]
self._draw_health_bar(screen, health, SCREEN_WIDTH - 200, 20)
# 特技分数
score = vehicle_state["stunt_score"]
self._draw_score(screen, score, SCREEN_WIDTH // 2, 20)
# 空中时间
if vehicle_state["air_time"] > 0.5:
self._draw_air_time(screen, vehicle_state["air_time"], SCREEN_WIDTH // 2, 60)
def _draw_speedometer(self, screen, speed, x, y):
"""绘制速度表"""
# 表盘背景
radius = 40
center = (x, y)
# 绘制表盘
pygame.draw.circle(screen, (30, 30, 40, 200), center, radius)
pygame.draw.circle(screen, COLORS["ui_border"], center, radius, 3)
# 绘制速度值
speed_text = f"{speed:.0f}"
text_surface = self.font_medium.render(speed_text, True, COLORS["text"])
text_rect = text_surface.get_rect(center=center)
screen.blit(text_surface, text_rect)
# 绘制单位
unit_surface = self.font_small.render("km/h", True, (150, 150, 150))
unit_rect = unit_surface.get_rect(center=(x, y + 20))
screen.blit(unit_surface, unit_rect)
# 绘制指针
angle = (speed / 200) * 270 # 0-200 km/h
angle = math.radians(min(angle, 270) - 135) # -135° 到 135°
pointer_length = radius - 5
end_x = x + pointer_length * math.cos(angle)
end_y = y + pointer_length * math.sin(angle)
pygame.draw.line(screen, (255, 50, 50), center, (end_x, end_y), 3)
def _draw_tachometer(self, screen, rpm, x, y):
"""绘制转速表"""
# 表盘背景
radius = 40
center = (x, y)
# 绘制表盘
pygame.draw.circle(screen, (30, 30, 40, 200), center, radius)
pygame.draw.circle(screen, COLORS["ui_border"], center, radius, 3)
# 绘制转速值
rpm_text = f"{rpm:.0f}"
text_surface = self.font_medium.render(rpm_text, True, COLORS["text"])
text_rect = text_surface.get_rect(center=center)
screen.blit(text_surface, text_rect)
# 绘制单位
unit_surface = self.font_small.render("RPM", True, (150, 150, 150))
unit_rect = unit_surface.get_rect(center=(x, y + 20))
screen.blit(unit_surface, unit_rect)
# 绘制红区
redline_start = 5000
if rpm > redline_start:
# 闪烁效果
if int(self.animation_time * 10) % 2 == 0:
pygame.draw.circle(screen, (255, 0, 0, 100), center, radius)
def _draw_boost_meter(self, screen, boost, x, y):
"""绘制助推能量表"""
width = 20
height = 100
# 背景
pygame.draw.rect(screen, (30, 30, 40, 200), (x, y, width, height))
pygame.draw.rect(screen, COLORS["ui_border"], (x, y, width, height), 2)
# 能量填充
fill_height = int(boost * height)
fill_y = y + height - fill_height
# 颜色渐变
if boost > 0.7:
color = (0, 255, 0) # 绿色
elif boost > 0.3:
color = (255, 255, 0) # 黄色
else:
color = (255, 0, 0) # 红色
pygame.draw.rect(screen, color, (x, fill_y, width, fill_height))
# 标签
label = "BOOST"
label_surface = self.font_small.render(label, True, COLORS["text"])
label_rect = label_surface.get_rect(center=(x + width/2, y - 15))
screen.blit(label_surface, label_rect)
# 数值
value_text = f"{boost:.0%}"
value_surface = self.font_small.render(value_text, True, COLORS["text"])
value_rect = value_surface.get_rect(center=(x + width/2, y + height + 15))
screen.blit(value_surface, value_rect)
def _draw_health_bar(self, screen, health, x, y):
"""绘制生命值条"""
width = 200
height = 20
# 背景
pygame.draw.rect(screen, (30, 30, 40, 200), (x, y, width, height))
pygame.draw.rect(screen, COLORS["ui_border"], (x, y, width, height), 2)
# 生命值填充
fill_width = int(health / 100 * width)
# 颜色
if health > 70:
color = COLORS["health_good"]
elif health > 30:
color = COLORS["health_warning"]
else:
# 低生命值时闪烁
if int(self.animation_time * 5) % 2 == 0:
color = COLORS["health_critical"]
else:
color = (255, 100, 100)
pygame.draw.rect(screen, color, (x, y, fill_width, height))
# 生命值文本
health_text = f"HEALTH: {health:.0f}/100"
text_surface = self.font_small.render(health_text, True, COLORS["text"])
text_rect = text_surface.get_rect(center=(x + width/2, y + height/2))
screen.blit(text_surface, text_rect)
def _draw_score(self, screen, score, x, y):
"""绘制特技分数"""
score_text = f"SCORE: {int(score):,}"
text_surface = self.font_large.render(score_text, True, (255, 215, 0)) # 金色
text_rect = text_surface.get_rect(center=(x, y))
# 阴影
shadow_surface = self.font_large.render(score_text, True, (0, 0, 0))
shadow_rect = text_rect.copy()
shadow_rect.x += 2
shadow_rect.y += 2
screen.blit(shadow_surface, shadow_rect)
screen.blit(text_surface, text_rect)
def _draw_air_time(self, screen, air_time, x, y):
"""绘制空中时间"""
air_text = f"AIR TIME: {air_time:.2f}s"
text_surface = self.font_medium.render(air_text, True, (135, 206, 235)) # 天蓝色
text_rect = text_surface.get_rect(center=(x, y))
# 缩放动画
scale = 1.0 + 0.1 * math.sin(self.animation_time * 10)
scaled_surface = pygame.transform.scale_by(text_surface, scale)
scaled_rect = scaled_surface.get_rect(center=(x, y))
screen.blit(scaled_surface, scaled_rect)
def _draw_debug_info(self, screen, vehicle_state, camera, performance_stats):
"""绘制调试信息"""
x, y = 10, 10
line_height = 20
# 性能统计
perf_lines = [
"=== 性能统计 ===",
f"物理步进: {performance_stats.get('step_time_ms', '0.00')}ms",
f"物理FPS: {performance_stats.get('theoretical_fps', '0')}",
f"总步数: {performance_stats.get('total_steps', 0)}",
f"刚体总数: {performance_stats.get('total_bodies', 0)}",
f"活动刚体: {performance_stats.get('active_bodies', 0)}",
f"休眠刚体: {performance_stats.get('sleeping_bodies', 0)}",
f"约束总数: {performance_stats.get('total_constraints', 0)}",
"",
]
# 车辆状态
vehicle_lines = [
"=== 车辆状态 ===",
f"位置: ({vehicle_state.get('chassis_x', 0):.1f}, {vehicle_state.get('chassis_y', 0):.1f})",
f"速度: {vehicle_state.get('speed', 0):.1f} m/s",
f"转向: {vehicle_state.get('steering', 0):.2f}",
f"接地: {'是' if vehicle_state.get('is_grounded') else '否'}",
f"悬挂力: {vehicle_state.get('suspension_forces', [0,0,0,0])}",
f"轮胎力: {vehicle_state.get('tire_forces', [(0,0)]*4)}",
"",
]
# 相机状态
camera_lines = [
"=== 相机状态 ===",
f"位置: ({camera.position.x:.1f}, {camera.position.y:.1f})",
f"缩放: {camera.zoom:.2f}",
f"震动: {camera.shake_intensity:.2f}",
]
# 绘制所有信息
all_lines = perf_lines + vehicle_lines + camera_lines
for i, line in enumerate(all_lines):
if line: # 跳过空行
text_surface = self.font_small.render(line, True, (200, 200, 200))
screen.blit(text_surface, (x, y + i * line_height))
def _draw_help(self, screen):
"""绘制帮助信息"""
help_text = [
"=== 控制说明 ===",
"方向键/WASD: 控制车辆",
"空格/SHIFT: 助推器",
"R: 重置车辆位置",
"F1: 切换调试信息",
"F2: 切换帮助信息",
"P/ESC: 暂停游戏",
"",
"=== 特技说明 ===",
"· 空中时间越长,分数越高",
"· 完成翻转获得额外分数",
"· 连续特技获得连击乘数",
"· 使用助推器进行超级跳跃",
]
# 创建半透明背景
help_surface = pygame.Surface((400, 300), pygame.SRCALPHA)
help_surface.fill((0, 0, 0, 200))
# 绘制边框
pygame.draw.rect(help_surface, (100, 100, 150), (0, 0, 400, 300), 2)
# 绘制文本
for i, line in enumerate(help_text):
text_surface = self.font_small.render(line, True, (255, 255, 255))
help_surface.blit(text_surface, (10, 10 + i * 20))
# 绘制到屏幕中心
screen.blit(help_surface, (SCREEN_WIDTH//2 - 200, SCREEN_HEIGHT//2 - 150))
def _draw_messages(self, screen):
"""绘制消息队列"""
y = SCREEN_HEIGHT - 150
for i, msg in enumerate(self.message_queue[:3]): # 最多显示3条
text = msg["text"]
alpha = int(255 * (1 - msg["time"] / msg["duration"]))
text_surface = self.font_medium.render(text, True, (255, 255, 255))
text_surface.set_alpha(alpha)
rect = text_surface.get_rect(center=(SCREEN_WIDTH//2, y - i * 40))
# 阴影
shadow_surface = self.font_medium.render(text, True, (0, 0, 0))
shadow_surface.set_alpha(alpha)
shadow_rect = rect.copy()
shadow_rect.x += 2
shadow_rect.y += 2
screen.blit(shadow_surface, shadow_rect)
screen.blit(text_surface, rect)
def add_message(self, text, duration=3.0):
"""添加消息到队列"""
self.message_queue.append({
"text": text,
"time": 0,
"duration": duration
})
def toggle_debug(self):
"""切换调试信息显示"""
self.show_debug = not self.show_debug
def toggle_help(self):
"""切换帮助信息显示"""
self.show_help = not self.show_help
3.8 约束调试可视化 (utils/constraint_debug.py)
python
"""
约束调试可视化工具
显示约束的力和状态
"""
import pygame
import pymunk
import math
from utils.config import *
class ConstraintDebug:
"""约束调试可视化"""
def __init__(self, physics_engine):
self.physics = physics_engine
self.enabled = False
# 显示选项
self.show_forces = True
self.show_torques = True
self.show_limits = True
self.show_springs = True
# 颜色
self.force_color = (255, 100, 100, 200) # 红色
self.torque_color = (100, 255, 100, 200) # 绿色
self.limit_color = (100, 100, 255, 200) # 蓝色
self.spring_color = (255, 255, 100, 200) # 黄色
def draw(self, screen, camera_offset=(0, 0)):
"""绘制所有约束的调试信息"""
if not self.enabled:
return
for constraint in self.physics.constraints:
if isinstance(constraint, pymunk.DampedSpring) and self.show_springs:
self._draw_spring(screen, constraint, camera_offset)
elif isinstance(constraint, pymunk.SimpleMotor) and self.show_torques:
self._draw_motor(screen, constraint, camera_offset)
elif isinstance(constraint, (pymunk.PivotJoint, pymunk.PinJoint)) and self.show_forces:
self._draw_joint(screen, constraint, camera_offset)
elif isinstance(constraint, (pymunk.RotaryLimitJoint, pymunk.SlideJoint)) and self.show_limits:
self._draw_limit(screen, constraint, camera_offset)
def _draw_spring(self, screen, spring, camera_offset):
"""绘制弹簧约束"""
# 获取锚点位置
body_a = spring.a
body_b = spring.b
# 获取世界坐标中的锚点
anchor_a = body_a.local_to_world(spring.anchor_a)
anchor_b = body_b.local_to_world(spring.anchor_b)
# 转换为屏幕坐标
screen_a = self.physics.world_to_screen(anchor_a, camera_offset)
screen_b = self.physics.world_to_screen(anchor_b, camera_offset)
# 绘制弹簧
pygame.draw.line(screen, self.spring_color, screen_a, screen_b, 3)
# 绘制弹簧线圈
length = math.sqrt((screen_b[0] - screen_a[0])**2 + (screen_b[1] - screen_a[1])**2)
if length > 0:
segments = 8
for i in range(segments + 1):
t = i / segments
x = screen_a[0] + (screen_b[0] - screen_a[0]) * t
y = screen_a[1] + (screen_b[1] - screen_a[1]) * t
# 垂直于线段的方向
dx = -(screen_b[1] - screen_a[1]) / length
dy = (screen_b[0] - screen_a[0]) / length
# 绘制线圈
coil_radius = 5
coil_x = x + dx * coil_radius * math.sin(t * math.pi * 4)
coil_y = y + dy * coil_radius * math.sin(t * math.pi * 4)
pygame.draw.circle(screen, self.spring_color, (int(coil_x), int(coil_y)), 2)
# 显示弹簧力
force = spring.impulse / self.physics.fixed_dt
if abs(force) > 10:
mid_x = (screen_a[0] + screen_b[0]) / 2
mid_y = (screen_a[1] + screen_b[1]) / 2
font = pygame.font.SysFont(None, 16)
force_text = f"{force:.0f}N"
text = font.render(force_text, True, self.spring_color)
screen.blit(text, (mid_x + 5, mid_y - 10))
def _draw_motor(self, screen, motor, camera_offset):
"""绘制马达约束"""
body_a = motor.a
body_b = motor.b
# 获取刚体中心
center_a = self.physics.world_to_screen(body_a.position, camera_offset)
center_b = self.physics.world_to_screen(body_b.position, camera_offset)
# 绘制连接线
pygame.draw.line(screen, self.torque_color, center_a, center_b, 2)
# 显示扭矩
torque = motor.rate * 100 # 简化
if abs(torque) > 1:
mid_x = (center_a[0] + center_b[0]) / 2
mid_y = (center_a[1] + center_b[1]) / 2
# 绘制扭矩箭头
angle = math.atan2(center_b[1] - center_a[1], center_b[0] - center_a[0])
# 根据扭矩方向旋转箭头
if torque < 0:
angle += math.pi
arrow_length = 20
arrow_x = mid_x + arrow_length * math.cos(angle)
arrow_y = mid_y + arrow_length * math.sin(angle)
pygame.draw.line(screen, self.torque_color, (mid_x, mid_y), (arrow_x, arrow_y), 2)
# 绘制箭头头部
head_angle = math.pi / 6
head_length = 8
# 左翼
left_angle = angle + math.pi - head_angle
left_x = arrow_x + head_length * math.cos(left_angle)
left_y = arrow_y + head_length * math.sin(left_angle)
# 右翼
right_angle = angle + math.pi + head_angle
right_x = arrow_x + head_length * math.cos(right_angle)
right_y = arrow_y + head_length * math.sin(right_angle)
pygame.draw.polygon(screen, self.torque_color,
[(arrow_x, arrow_y), (left_x, left_y), (right_x, right_y)])
# 显示扭矩值
font = pygame.font.SysFont(None, 16)
torque_text = f"{torque:.0f}Nm"
text = font.render(torque_text, True, self.torque_color)
screen.blit(text, (mid_x + 10, mid_y - 20))
def _draw_joint(self, screen, joint, camera_offset):
"""绘制关节约束"""
if not hasattr(joint, 'a') or not hasattr(joint, 'b'):
return
body_a = joint.a
body_b = joint.b
# 获取世界坐标中的锚点
if hasattr(joint, 'anchor_a') and hasattr(joint, 'anchor_b'):
anchor_a = body_a.local_to_world(joint.anchor_a)
anchor_b = body_b.local_to_world(joint.anchor_b)
else:
anchor_a = body_a.position
anchor_b = body_b.position
# 转换为屏幕坐标
screen_a = self.physics.world_to_screen(anchor_a, camera_offset)
screen_b = self.physics.world_to_screen(anchor_b, camera_offset)
# 绘制关节
pygame.draw.circle(screen, self.force_color, (int(screen_a[0]), int(screen_a[1])), 6)
pygame.draw.circle(screen, self.force_color, (int(screen_b[0]), int(screen_b[1])), 6)
pygame.draw.line(screen, self.force_color, screen_a, screen_b, 2)
# 显示关节力
force = joint.impulse / self.physics.fixed_dt if hasattr(joint, 'impulse') else 0
if abs(force) > 10:
mid_x = (screen_a[0] + screen_b[0]) / 2
mid_y = (screen_a[1] + screen_b[1]) / 2
font = pygame.font.SysFont(None, 16)
force_text = f"{force:.0f}N"
text = font.render(force_text, True, self.force_color)
screen.blit(text, (mid_x + 5, mid_y - 10))
def _draw_limit(self, screen, constraint, camera_offset):
"""绘制限制约束"""
if isinstance(constraint, pymunk.RotaryLimitJoint):
self._draw_rotary_limit(screen, constraint, camera_offset)
elif isinstance(constraint, pymunk.SlideJoint):
self._draw_slide_limit(screen, constraint, camera_offset)
def _draw_rotary_limit(self, screen, constraint, camera_offset):
"""绘制旋转限制"""
body_a = constraint.a
body_b = constraint.b
# 获取刚体中心
center = self.physics.world_to_screen(body_a.position, camera_offset)
# 绘制限制范围
min_angle = constraint.min
max_angle = constraint.max
radius = 30
start_angle = min_angle - body_a.angle
end_angle = max_angle - body_a.angle
# 绘制限制弧
rect = pygame.Rect(center[0] - radius, center[1] - radius,
radius * 2, radius * 2)
pygame.draw.arc(screen, self.limit_color, rect,
start_angle, end_angle, 2)
# 显示限制角度
font = pygame.font.SysFont(None, 16)
angle_text = f"{math.degrees(min_angle):.0f}° to {math.degrees(max_angle):.0f}°"
text = font.render(angle_text, True, self.limit_color)
screen.blit(text, (center[0] - 40, center[1] - 50))
def _draw_slide_limit(self, screen, constraint, camera_offset):
"""绘制滑动限制"""
body_a = constraint.a
body_b = constraint.b
# 获取世界坐标中的锚点
anchor_a = body_a.local_to_world(constraint.anchor_a)
anchor_b = body_b.local_to_world(constraint.anchor_b)
# 转换为屏幕坐标
screen_a = self.physics.world_to_screen(anchor_a, camera_offset)
screen_b = self.physics.world_to_screen(anchor_b, camera_offset)
# 绘制最小和最大距离限制
min_dist = constraint.min * PIXELS_PER_METER
max_dist = constraint.max * PIXELS_PER_METER
# 计算方向向量
dx = screen_b[0] - screen_a[0]
dy = screen_b[1] - screen_a[1]
length = math.sqrt(dx*dx + dy*dy) if dx != 0 or dy != 0 else 1
if length > 0:
dir_x = dx / length
dir_y = dy / length
# 最小距离点
min_x = screen_a[0] + dir_x * min_dist
min_y = screen_a[1] + dir_y * min_dist
# 最大距离点
max_x = screen_a[0] + dir_x * max_dist
max_y = screen_a[1] + dir_y * max_dist
# 绘制限制范围
pygame.draw.line(screen, self.limit_color, (min_x, min_y), (max_x, max_y), 2)
# 绘制限制标记
pygame.draw.circle(screen, self.limit_color, (int(min_x), int(min_y)), 4)
pygame.draw.circle(screen, self.limit_color, (int(max_x), int(max_y)), 4)
# 显示距离限制
font = pygame.font.SysFont(None, 16)
dist_text = f"{constraint.min:.1f}m to {constraint.max:.1f}m"
text = font.render(dist_text, True, self.limit_color)
mid_x = (min_x + max_x) / 2
mid_y = (min_y + max_y) / 2
screen.blit(text, (mid_x + 5, mid_y - 10))
def toggle(self):
"""切换调试显示"""
self.enabled = not self.enabled
def set_options(self, show_forces=None, show_torques=None,
show_limits=None, show_springs=None):
"""设置显示选项"""
if show_forces is not None:
self.show_forces = show_forces
if show_torques is not None:
self.show_torques = show_torques
if show_limits is not None:
self.show_limits = show_limits
if show_springs is not None:
self.show_springs = show_springs
3.9 主游戏类 (main.py)
python
"""
特技赛车主游戏文件
整合所有组件
"""
import pygame
import sys
import math
from utils.config import *
from physics_engine import AdvancedPhysicsEngine
from vehicle import StuntCar
from terrain import Terrain
from camera import Camera
from ui import UI
from utils.constraint_debug import ConstraintDebug
class StuntCarGame:
"""特技赛车游戏主类"""
def __init__(self):
# 初始化pygame
pygame.init()
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption(CAPTION)
self.clock = pygame.time.Clock()
# 创建物理引擎
self.physics = AdvancedPhysicsEngine()
# 创建地形
self.terrain = Terrain(self.physics)
# 创建车辆
self.car = StuntCar(self.physics, x=5, y=10)
# 创建相机
self.camera = Camera()
# 创建用户界面
self.ui = UI()
# 创建调试工具
self.constraint_debug = ConstraintDebug(self.physics)
# 游戏状态
self.running = True
self.paused = False
self.game_time = 0
# 输入状态
self.keys_pressed = {
"throttle": False,
"brake": False,
"left": False,
"right": False,
"boost": False,
}
# 帧时间管理
self.last_time = pygame.time.get_ticks()
self.frame_times = []
# 设置相机边界
terrain_width = self.terrain.width
self.camera.set_bounds(0, -10, terrain_width, 50)
# 添加约束断裂回调
self.physics.constraint_broken_callbacks.append(self._on_constraint_broken)
def _on_constraint_broken(self, constraint):
"""约束断裂回调"""
print(f"约束断裂! 类型: {type(constraint).__name__}")
# 添加屏幕震动
self.camera.add_shake(3.0)
# 显示消息
self.ui.add_message("零件损坏!", 2.0)
# 减少车辆生命值
self.car.state["health"] -= 20
def handle_events(self):
"""处理输入事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
# 控制输入
if event.key in INPUT_CONFIG["throttle"]:
self.keys_pressed["throttle"] = True
elif event.key in INPUT_CONFIG["brake"]:
self.keys_pressed["brake"] = True
elif event.key in INPUT_CONFIG["left"]:
self.keys_pressed["left"] = True
elif event.key in INPUT_CONFIG["right"]:
self.keys_pressed["right"] = True
elif event.key in INPUT_CONFIG["boost"]:
self.keys_pressed["boost"] = True
# 功能键
elif event.key in INPUT_CONFIG["reset"]:
self.reset_car()
elif event.key in INPUT_CONFIG["debug"]:
self.ui.toggle_debug()
self.constraint_debug.toggle()
elif event.key == pygame.K_F2:
self.ui.toggle_help()
elif event.key in INPUT_CONFIG["pause"]:
self.paused = not self.paused
elif event.type == pygame.KEYUP:
# 控制输入
if event.key in INPUT_CONFIG["throttle"]:
self.keys_pressed["throttle"] = False
elif event.key in INPUT_CONFIG["brake"]:
self.keys_pressed["brake"] = False
elif event.key in INPUT_CONFIG["left"]:
self.keys_pressed["left"] = False
elif event.key in INPUT_CONFIG["right"]:
self.keys_pressed["right"] = False
elif event.key in INPUT_CONFIG["boost"]:
self.keys_pressed["boost"] = False
def update_input(self):
"""更新输入状态"""
# 计算油门/刹车
throttle = 1.0 if self.keys_pressed["throttle"] else 0.0
brake = 1.0 if self.keys_pressed["brake"] else 0.0
# 计算转向
steering = 0.0
if self.keys_pressed["left"]:
steering -= 1.0
if self.keys_pressed["right"]:
steering += 1.0
# 助推
boost = self.keys_pressed["boost"]
# 应用输入到车辆
self.car.apply_input(throttle, brake, steering, boost)
def update(self, dt):
"""更新游戏逻辑"""
if self.paused:
return
self.game_time += dt
# 更新输入
self.update_input()
# 更新物理
self.physics.update(dt)
# 更新车辆
self.car.update(dt)
# 更新相机
car_pos = self.car.chassis.position
car_vel = self.car.chassis.velocity
self.camera.update(car_pos, car_vel, dt)
# 更新UI
self.ui.update(dt)
# 检查车辆状态
self._check_car_state()
# 性能监控
self._update_performance_stats(dt)
def _check_car_state(self):
"""检查车辆状态"""
# 检查是否掉落
if self.car.chassis.position.y < -10:
self.reset_car()
self.ui.add_message("车辆掉落!", 2.0)
# 检查生命值
if self.car.state["health"] <= 0:
self.reset_car()
self.car.state["health"] = 100
self.ui.add_message("车辆损坏! 已重置", 3.0)
# 检测特技完成
if self.car.state["air_time"] > 1.0 and not self.car.state["is_grounded"]:
# 空中特技
if self.car.state["air_time"] > 3.0:
self.ui.add_message("史诗级空中时间!", 2.0)
elif self.car.state["air_time"] > 2.0:
self.ui.add_message("超长空中时间!", 2.0)
elif self.car.state["air_time"] > 1.0:
self.ui.add_message("空中时间!", 1.0)
def _update_performance_stats(self, dt):
"""更新性能统计"""
# 计算平均帧时间
self.frame_times.append(dt)
if len(self.frame_times) > 100:
self.frame_times.pop(0)
def reset_car(self):
"""重置车辆位置"""
# 找到最近的合理位置
car_x = self.car.chassis.position.x
ground_y = self.terrain._get_height_at(car_x) + 2
# 重置车辆位置
self.car.chassis.position = (car_x, ground_y)
self.car.chassis.velocity = (0, 0)
self.car.chassis.angular_velocity = 0
# 重置车轮
for wheel in self.car.wheels:
wheel.angular_velocity = 0
# 添加屏幕震动
self.camera.add_shake(2.0)
# 重置特技追踪
self.car.stunt_tracker.state["combo_multiplier"] = 1.0
self.car.stunt_tracker.completed_tricks.clear()
def draw(self):
"""绘制游戏"""
# 获取相机偏移
camera_offset = self.camera.get_offset()
# 绘制地形
self.terrain.draw(self.screen, camera_offset)
# 绘制车辆
self.car.draw(self.screen, camera_offset, debug=self.ui.show_debug)
# 绘制约束调试
self.constraint_debug.draw(self.screen, camera_offset)
# 绘制轨迹预测
if self.ui.show_debug:
car_pos = self.car.chassis.position
car_vel = self.car.chassis.velocity
self.camera.draw_trajectory(self.screen, self.terrain)
# 绘制UI
vehicle_state = {
"speed": self.car.state["speed"],
"rpm": self.car.state["rpm"],
"boost": self.car.state["boost"],
"health": self.car.state["health"],
"stunt_score": self.car.state["stunt_score"],
"air_time": self.car.state["air_time"],
"is_grounded": self.car.state["is_grounded"],
"chassis_x": self.car.chassis.position.x,
"chassis_y": self.car.chassis.position.y,
"steering": self.car.state["steering"],
"suspension_forces": self.car.debug["suspension_forces"],
"tire_forces": self.car.debug["tire_forces"],
}
performance_stats = self.physics.get_performance_stats()
self.ui.draw(self.screen, vehicle_state, self.camera, performance_stats)
# 绘制暂停界面
if self.paused:
self._draw_pause_screen()
# 更新显示
pygame.display.flip()
def _draw_pause_screen(self):
"""绘制暂停界面"""
# 半透明覆盖
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
self.screen.blit(overlay, (0, 0))
# 暂停文本
font = pygame.font.SysFont(None, 72)
pause_text = font.render("游戏暂停", True, (255, 255, 255))
pause_rect = pause_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 - 50))
self.screen.blit(pause_text, pause_rect)
# 提示文本
font = pygame.font.SysFont(None, 32)
hint_text = font.render("按 P 或 ESC 继续游戏", True, (200, 200, 200))
hint_rect = hint_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 + 20))
self.screen.blit(hint_text, hint_rect)
def run(self):
"""主游戏循环"""
while self.running:
# 计算帧时间
current_time = pygame.time.get_ticks()
dt = (current_time - self.last_time) / 1000.0
self.last_time = current_time
# 限制最大帧时间
dt = min(dt, 0.1)
# 处理输入
self.handle_events()
# 更新游戏
self.update(dt)
# 绘制游戏
self.draw()
# 控制帧率
self.clock.tick(FPS)
# 退出游戏
pygame.quit()
sys.exit()
if __name__ == "__main__":
game = StuntCarGame()
game.run()
四、优化与技巧:高级调试与性能优化(约1500字)
4.1 高级调试技巧
实时参数调整系统
创建一个实时调整物理参数的系统,无需重新启动游戏:
python
class ParameterTuner:
"""实时参数调整器"""
def __init__(self, vehicle):
self.vehicle = vehicle
self.parameters = {
"suspension_stiffness": vehicle.config["suspension"]["stiffness"],
"suspension_damping": vehicle.config["suspension"]["damping"],
"max_steering_angle": math.degrees(vehicle.config["steering"]["max_angle"]),
"wheel_friction": vehicle.config["wheel"]["friction"],
}
self.active_param = 0
self.param_names = list(self.parameters.keys())
def update(self, keys):
"""更新参数"""
if keys[pygame.K_TAB]:
self.active_param = (self.active_param + 1) % len(self.parameters)
param_name = self.param_names[self.active_param]
if keys[pygame.K_UP]:
self.parameters[param_name] *= 1.1
self.apply_parameter(param_name)
elif keys[pygame.K_DOWN]:
self.parameters[param_name] *= 0.9
self.apply_parameter(param_name)
def apply_parameter(self, param_name):
"""应用参数到车辆"""
if param_name == "suspension_stiffness":
self.vehicle.config["suspension"]["stiffness"] = self.parameters[param_name]
for spring in self.vehicle.suspensions:
spring.stiffness = self.parameters[param_name]
elif param_name == "suspension_damping":
self.vehicle.config["suspension"]["damping"] = self.parameters[param_name]
for spring in self.vehicle.suspensions:
spring.damping = self.parameters[param_name]
elif param_name == "max_steering_angle":
angle_rad = math.radians(self.parameters[param_name])
self.vehicle.config["steering"]["max_angle"] = angle_rad
for joint in self.vehicle.steering_joints:
if hasattr(joint, "limit"):
joint.limit.min = -angle_rad
joint.limit.max = angle_rad
elif param_name == "wheel_friction":
self.vehicle.config["wheel"]["friction"] = self.parameters[param_name]
for i, wheel in enumerate(self.vehicle.wheels):
for shape in wheel.shapes:
shape.friction = self.parameters[param_name]
def draw(self, screen):
"""绘制参数调整器"""
font = pygame.font.SysFont(None, 24)
y = 100
for i, (name, value) in enumerate(self.parameters.items()):
color = (255, 255, 0) if i == self.active_param else (200, 200, 200)
if "angle" in name:
text = f"{name}: {value:.1f}°"
else:
text = f"{name}: {value:.1f}"
surface = font.render(text, True, color)
screen.blit(surface, (10, y + i * 25))
物理状态记录与回放
记录物理状态以便调试和回放:
python
class PhysicsRecorder:
"""物理状态记录器"""
def __init__(self, physics_engine, record_interval=0.1):
self.physics = physics_engine
self.record_interval = record_interval
self.records = []
self.timer = 0
def update(self, dt):
"""更新记录器"""
self.timer += dt
if self.timer >= self.record_interval:
self.record_state()
self.timer = 0
def record_state(self):
"""记录当前物理状态"""
state = {
"time": pygame.time.get_ticks() / 1000.0,
"bodies": [],
"constraints": [],
}
# 记录刚体状态
for body in self.physics.space.bodies:
body_state = {
"position": (body.position.x, body.position.y),
"angle": body.angle,
"velocity": (body.velocity.x, body.velocity.y),
"angular_velocity": body.angular_velocity,
}
state["bodies"].append(body_state)
# 记录约束状态
for constraint in self.physics.constraints:
constraint_state = {
"type": type(constraint).__name__,
"impulse": constraint.impulse if hasattr(constraint, 'impulse') else 0,
}
state["constraints"].append(constraint_state)
self.records.append(state)
# 限制记录数量
if len(self.records) > 1000:
self.records.pop(0)
def save_recording(self, filename):
"""保存记录到文件"""
import json
import pickle
# 转换为可序列化的格式
serializable_records = []
for record in self.records:
serializable_records.append({
"time": record["time"],
"bodies": record["bodies"],
"constraints": record["constraints"],
})
with open(filename, 'wb') as f:
pickle.dump(serializable_records, f)
print(f"记录已保存到 {filename}")
def load_recording(self, filename):
"""从文件加载记录"""
import pickle
with open(filename, 'rb') as f:
self.records = pickle.load(f)
print(f"从 {filename} 加载了 {len(self.records)} 条记录")
4.2 性能优化高级技巧
多线程物理计算
将物理计算移动到单独的线程以提高性能:
python
import threading
import queue
import time
class ThreadedPhysicsEngine(AdvancedPhysicsEngine):
"""多线程物理引擎"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 线程控制
self.physics_thread = None
self.running = False
# 通信队列
self.input_queue = queue.Queue()
self.output_queue = queue.Queue()
# 状态同步
self.state_lock = threading.Lock()
self.current_state = None
def start_thread(self):
"""启动物理线程"""
self.running = True
self.physics_thread = threading.Thread(target=self._physics_thread_func)
self.physics_thread.daemon = True
self.physics_thread.start()
def stop_thread(self):
"""停止物理线程"""
self.running = False
if self.physics_thread:
self.physics_thread.join(timeout=1.0)
def _physics_thread_func(self):
"""物理线程函数"""
while self.running:
try:
# 从队列获取输入
try:
dt, commands = self.input_queue.get(timeout=0.001)
except queue.Empty:
continue
# 处理命令
for command in commands:
if command[0] == 'add':
self.space.add(*command[1:])
elif command[0] == 'remove':
self.space.remove(*command[1:])
elif command[0] == 'apply_force':
body, force, point = command[1:]
body.apply_force_at_local_point(force, point)
# 更新物理
self.update(dt)
# 获取当前状态
with self.state_lock:
self.current_state = self._capture_state()
# 发送状态到主线程
self.output_queue.put(self.current_state)
except Exception as e:
print(f"物理线程错误: {e}")
def _capture_state(self):
"""捕获当前物理状态"""
state = {
'bodies': [],
'time': time.time(),
}
for body in self.space.bodies:
state['bodies'].append({
'position': (body.position.x, body.position.y),
'angle': body.angle,
'velocity': (body.velocity.x, body.velocity.y),
})
return state
def update_async(self, dt, commands=None):
"""异步更新物理"""
if commands is None:
commands = []
# 发送更新请求到物理线程
self.input_queue.put((dt, commands))
# 尝试获取最新状态
try:
while True:
state = self.output_queue.get_nowait()
self.current_state = state
except queue.Empty:
pass
def get_interpolated_state(self, alpha):
"""获取插值状态(用于渲染)"""
if not hasattr(self, 'previous_state') or self.current_state is None:
return {}
# 线性插值
interpolated = {'bodies': []}
for i, (prev_body, curr_body) in enumerate(zip(
self.previous_state['bodies'],
self.current_state['bodies']
)):
interp_body = {}
for key in ['position', 'velocity']:
if key in prev_body and key in curr_body:
x1, y1 = prev_body[key]
x2, y2 = curr_body[key]
interp_body[key] = (
x1 + (x2 - x1) * alpha,
y1 + (y2 - y1) * alpha
)
# 角度插值需要考虑角度环绕
if 'angle' in prev_body and 'angle' in curr_body:
a1 = prev_body['angle']
a2 = curr_body['angle']
# 处理角度环绕,确保插值走最短路径
diff = a2 - a1
if diff > math.pi:
diff -= 2 * math.pi
elif diff < -math.pi:
diff += 2 * math.pi
interp_body['angle'] = a1 + diff * alpha
interpolated['bodies'].append(interp_body)
return interpolated
def sync_with_render(self, alpha):
"""与渲染线程同步,更新上一帧状态并返回插值状态"""
if not hasattr(self, 'previous_state'):
self.previous_state = self.current_state
interpolated = self.get_interpolated_state(alpha)
# 更新前一帧状态
self.previous_state = self.current_state
return interpolated
空间分区优化
优化碰撞检测的空间分区策略:
python
class OptimizedPhysicsEngine(AdvancedPhysicsEngine):
"""优化空间分区的物理引擎"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 自定义空间分区设置
self.cell_size = 2.0 # 单元格大小(米)
self.dynamic_tree_depth = 16
# 设置空间分区
self._setup_spatial_partitioning()
def _setup_spatial_partitioning(self):
"""设置空间分区"""
# 启用空间哈希用于静态物体
self.space.use_spatial_hash = True
# 设置哈希单元格大小
# 根据场景中物体的大小调整
avg_object_size = 1.0 # 平均物体大小(米)
self.space.resize_static_hash(self.cell_size, 1000)
self.space.resize_active_hash(self.cell_size, 1000)
# 设置动态树深度
self.space.dynamic_tree_depth = self.dynamic_tree_depth
# 设置碰撞处理优化
self.space.collision_persistence = 3 # 碰撞持续帧数
def add_static_batch(self, shapes):
"""批量添加静态物体,优化性能"""
# 静态物体应该一次性添加,以便空间哈希优化
self.space.add(*shapes)
# 重新计算空间哈希
self.space.reindex_static()
def query_nearby_shapes(self, position, radius, shape_filter=None):
"""查询附近的形状,使用优化的空间查询"""
# 创建查询边界框
bb = pymunk.BB(
position.x - radius, position.y - radius,
position.x + radius, position.y + radius
)
# 执行边界框查询
shapes = []
def query_callback(shape, _):
shapes.append(shape)
return True
self.space.bb_query(bb, shape_filter, query_callback)
return shapes
def optimize_collision_pairs(self):
"""优化碰撞对,减少不必要的碰撞检测"""
# 根据物体类别设置碰撞掩码
for shape in self.space.shapes:
if hasattr(shape, 'body'):
# 根据物体速度动态调整碰撞掩码
if shape.body.body_type == pymunk.Body.DYNAMIC:
speed_sq = shape.body.velocity.length_sq
# 低速物体减少碰撞检测
if speed_sq < 1.0: # 速度小于1m/s
# 只与静态物体和低速物体碰撞
shape.filter = pymunk.ShapeFilter(
categories=shape.filter.categories,
mask=0b0010 # 只与地面碰撞
)
内存池与对象重用
减少内存分配,提高性能:
python
class PhysicsObjectPool:
"""物理对象池,重用不再使用的对象"""
def __init__(self, physics_engine, object_type, initial_size=10):
self.physics = physics_engine
self.object_type = object_type
self.pool = [] # 可用对象列表
self.active = [] # 活动对象列表
# 预创建对象
for _ in range(initial_size):
obj = self._create_object()
self._deactivate(obj)
self.pool.append(obj)
def _create_object(self):
"""创建新对象"""
# 根据对象类型创建
if self.object_type == "wheel":
# 创建车轮
mass = 20
radius = 0.4
moment = pymunk.moment_for_circle(mass, 0, radius)
body = pymunk.Body(mass, moment)
shape = pymunk.Circle(body, radius)
return {"body": body, "shape": shape, "type": "wheel"}
elif self.object_type == "box":
# 创建盒子
mass = 10
size = (1, 1)
moment = pymunk.moment_for_box(mass, size)
body = pymunk.Body(mass, moment)
half_width = size[0] / 2
half_height = size[1] / 2
vertices = [
(-half_width, -half_height),
(-half_width, half_height),
(half_width, half_height),
(half_width, -half_height)
]
shape = pymunk.Poly(body, vertices)
return {"body": body, "shape": shape, "type": "box"}
return None
def acquire(self, position, **kwargs):
"""从池中获取对象"""
if not self.pool:
# 池为空,创建新对象
obj = self._create_object()
else:
obj = self.pool.pop()
# 激活对象
self._activate(obj, position, **kwargs)
self.active.append(obj)
return obj
def release(self, obj):
"""释放对象回池中"""
if obj in self.active:
self.active.remove(obj)
self._deactivate(obj)
self.pool.append(obj)
def _activate(self, obj, position, **kwargs):
"""激活对象"""
body = obj["body"]
shape = obj["shape"]
# 设置位置
body.position = position
# 重置物理状态
body.velocity = (0, 0)
body.angular_velocity = 0
body.force = (0, 0)
body.torque = 0
# 根据类型设置参数
if obj["type"] == "wheel":
if "radius" in kwargs:
shape.radius = kwargs["radius"]
if "friction" in kwargs:
shape.friction = kwargs["friction"]
# 添加到物理空间
self.physics.add(body, shape)
def _deactivate(self, obj):
"""停用对象"""
body = obj["body"]
shape = obj["shape"]
# 从物理空间移除
self.physics.remove(body, shape)
# 移动到远处(避免干扰)
body.position = (-1000, -1000)
def update(self, dt):
"""更新对象池"""
# 检查活动对象,如果不再需要则释放
for obj in self.active[:]:
body = obj["body"]
# 如果物体掉出世界,释放它
if body.position.y < -100:
self.release(obj)
# 如果物体静止太久,释放它
if (body.velocity.length_sq < 0.01 and
body.angular_velocity**2 < 0.001):
if not hasattr(body, 'idle_time'):
body.idle_time = 0
body.idle_time += dt
if body.idle_time > 5.0: # 5秒空闲
self.release(obj)
else:
if hasattr(body, 'idle_time'):
body.idle_time = 0
五、总结与扩展:从特技车到物理模拟
5.1 本篇核心知识点总结
1. 约束与关节系统
-
PinJoint/PivotJoint:固定点连接,允许旋转
-
SlideJoint:限制两点间距离
-
DampedSpring:弹簧阻尼系统,车辆悬挂核心
-
RotaryLimitJoint:限制旋转角度
-
SimpleMotor:提供恒定扭矩或速度
-
GearJoint:保持固定传动比
2. 车辆物理建模
-
悬挂系统:弹簧刚度、阻尼系数、行程限制
-
传动系统:发动机扭矩曲线、刹车系统
-
转向系统:阿克曼转向几何
-
轮胎模型:魔术公式简化版
-
空气动力学:阻力、下压力模拟
3. 高级碰撞检测
-
连续碰撞检测:防止高速物体穿透
-
射线检测:接地检测、视线检测
-
形状查询:区域重叠检测
-
碰撞过滤:优化碰撞检测性能
4. 性能优化
-
固定时间步长:确保物理稳定性
-
空间分区:BVH树、空间哈希
-
对象池:减少内存分配开销
-
多线程:物理计算与渲染分离
-
休眠系统:跳过静止物体计算
5. 调试与可视化
-
约束力可视化:实时显示约束状态
-
轨迹预测:基于物理的运动预测
-
参数调整:运行时修改物理参数
-
状态记录:物理状态记录与回放
5.2 实际应用场景扩展
1. 游戏开发
-
赛车游戏:真实车辆物理模拟
-
平台游戏:复杂机械机关
-
解谜游戏:基于约束的物理谜题
-
沙盒游戏:可组合的物理结构
2. 仿真与教育
-
机械仿真:连杆机构、齿轮传动
-
物理教学:约束系统可视化
-
机器人学:关节式机器人模拟
-
工程分析:结构应力分析
3. 交互应用
-
UI物理效果:弹簧动画、物理滚动
-
数据可视化:物理引导的布局
-
艺术创作:生成艺术、物理动画
-
VR/AR互动:物理交互反馈
5.3 高级挑战任务
挑战1:全功能车辆编辑器
创建可实时调整车辆参数的编辑器:
python
class VehicleEditor:
def __init__(self, vehicle):
self.vehicle = vehicle
self.params = self._extract_params(vehicle)
self.ui = ParamEditorUI(self.params)
def _extract_params(self, vehicle):
"""提取车辆所有可调参数"""
params = {
"动力系统": {
"最大扭矩": {"value": 2000, "min": 500, "max": 5000, "step": 100},
"刹车扭矩": {"value": 3000, "min": 1000, "max": 10000, "step": 100},
"传动比": {"value": 3.42, "min": 1.0, "max": 10.0, "step": 0.1},
},
"悬挂系统": {
"弹簧刚度": {"value": 20000, "min": 5000, "max": 100000, "step": 1000},
"阻尼系数": {"value": 2000, "min": 500, "max": 10000, "step": 100},
"行程长度": {"value": 0.5, "min": 0.1, "max": 2.0, "step": 0.05},
},
# ... 更多参数
}
return params
挑战2:物理特技评分系统
完善特技评分系统,包含:
-
空中翻转、旋转评分
-
连击系统与乘数
-
特技组合奖励
-
历史记录与回放
挑战3:多人物理同步
实现多人游戏的物理同步:
python
class PhysicsSync:
"""物理状态同步"""
def __init__(self, physics_engine):
self.physics = physics_engine
self.snapshot_rate = 0.1 # 快照频率
self.snapshots = [] # 状态快照
def capture_snapshot(self):
"""捕获当前物理状态快照"""
snapshot = {
"time": time.time(),
"bodies": [],
"constraints": [],
}
# 序列化所有刚体
for body in self.physics.space.bodies:
snapshot["bodies"].append({
"id": id(body),
"position": (body.position.x, body.position.y),
"angle": body.angle,
"velocity": (body.velocity.x, body.velocity.y),
"angular_velocity": body.angular_velocity,
})
self.snapshots.append(snapshot)
return snapshot
def interpolate_state(self, target_time, alpha=0.0):
"""插值到目标时间的物理状态"""
# 找到目标时间前后的快照
before = None
after = None
for snapshot in self.snapshots:
if snapshot["time"] <= target_time:
before = snapshot
else:
after = snapshot
break
if before and after:
# 在两个快照间插值
t = (target_time - before["time"]) / (after["time"] - before["time"])
t = t * (1 - alpha) + alpha # 应用额外插值
return self._interpolate_snapshots(before, after, t)
return before or after
def predict_state(self, current_time, predict_time=0.1):
"""预测未来物理状态"""
if len(self.snapshots) < 2:
return None
# 使用最近两个快照计算加速度
latest = self.snapshots[-1]
second_latest = self.snapshots[-2]
dt = latest["time"] - second_latest["time"]
if dt <= 0:
return latest
predicted = {"time": current_time + predict_time, "bodies": []}
for i, (body_now, body_prev) in enumerate(zip(latest["bodies"], second_latest["bodies"])):
# 计算加速度
vel_now = body_now["velocity"]
vel_prev = body_prev["velocity"]
accel = (
(vel_now[0] - vel_prev[0]) / dt,
(vel_now[1] - vel_prev[1]) / dt
)
# 预测位置
pos = (
body_now["position"][0] + vel_now[0] * predict_time + 0.5 * accel[0] * predict_time**2,
body_now["position"][1] + vel_now[1] * predict_time + 0.5 * accel[1] * predict_time**2
)
# 预测速度
vel = (
vel_now[0] + accel[0] * predict_time,
vel_now[1] + accel[1] * predict_time
)
predicted["bodies"].append({
"id": body_now["id"],
"position": pos,
"velocity": vel,
"angle": body_now["angle"],
"angular_velocity": body_now["angular_velocity"],
})
return predicted
5.4 性能基准测试
创建性能测试套件,评估不同优化策略的效果:
python
class PhysicsBenchmark:
"""物理性能基准测试"""
def __init__(self):
self.results = {}
def run_vehicle_stress_test(self, num_vehicles=10):
"""车辆压力测试"""
physics = AdvancedPhysicsEngine()
vehicles = []
start_time = time.time()
# 创建多辆车辆
for i in range(num_vehicles):
x = i * 5
y = 10
vehicle = StuntCar(physics, x, y)
vehicles.append(vehicle)
creation_time = time.time() - start_time
# 运行模拟
steps = 100
step_times = []
for step in range(steps):
step_start = time.time()
# 更新物理
physics.update(1.0/60.0)
# 更新车辆
for vehicle in vehicles:
vehicle.update(1.0/60.0)
step_time = time.time() - step_start
step_times.append(step_time)
avg_step_time = sum(step_times) / len(step_times)
min_step_time = min(step_times)
max_step_time = max(step_times)
result = {
"num_vehicles": num_vehicles,
"creation_time": creation_time,
"avg_step_time": avg_step_time,
"min_step_time": min_step_time,
"max_step_time": max_step_time,
"fps": 1.0 / avg_step_time if avg_step_time > 0 else 0,
}
self.results["vehicle_stress"] = result
return result
def run_constraint_complexity_test(self, num_constraints=100):
"""约束复杂度测试"""
physics = AdvancedPhysicsEngine()
# 创建基础物体
bodies = []
for i in range(10):
body = pymunk.Body(1, 1)
body.position = (i * 2, 5)
shape = pymunk.Circle(body, 0.5)
physics.add(body, shape)
bodies.append(body)
# 添加约束
start_time = time.time()
constraints = []
for i in range(num_constraints):
a = random.choice(bodies)
b = random.choice([b for b in bodies if b != a])
# 随机选择约束类型
constraint_type = random.choice(["pin", "spring", "gear"])
if constraint_type == "pin":
constraint = pymunk.PinJoint(a, b, (0, 0), (0, 0))
elif constraint_type == "spring":
constraint = pymunk.DampedSpring(a, b, (0, 0), (0, 0), 1, 1000, 100)
else: # gear
constraint = pymunk.GearJoint(a, b, 0, 1)
physics.add_constraint(constraint)
constraints.append(constraint)
creation_time = time.time() - start_time
# 运行模拟
steps = 100
step_times = []
for step in range(steps):
step_start = time.time()
physics.update(1.0/60.0)
step_times.append(time.time() - step_start)
avg_step_time = sum(step_times) / len(step_times)
result = {
"num_constraints": num_constraints,
"creation_time": creation_time,
"avg_step_time": avg_step_time,
"fps": 1.0 / avg_step_time if avg_step_time > 0 else 0,
}
self.results["constraint_complexity"] = result
return result
def print_results(self):
"""打印测试结果"""
print("=== 物理性能基准测试结果 ===")
for test_name, result in self.results.items():
print(f"\n测试: {test_name}")
for key, value in result.items():
if isinstance(value, float):
print(f" {key}: {value:.4f}")
else:
print(f" {key}: {value}")
5.5 常见问题与解决方案
1. 约束系统不稳定
问题:车辆悬挂抖动或翻转
解决方案:
python
def stabilize_constraints(vehicle, dt):
"""稳定约束系统"""
# 增加约束迭代次数
vehicle.physics.space.iterations = 30
# 添加约束误差纠正
for constraint in vehicle.suspensions:
# 计算当前长度与目标长度的误差
current_length = (constraint.a.position - constraint.b.position).length
error = current_length - constraint.rest_length
# 应用纠正力
if abs(error) > 0.1: # 误差阈值
correct_force = error * 1000 # 纠正系数
direction = (constraint.b.position - constraint.a.position).normalized()
constraint.a.apply_force_at_local_point(direction * correct_force)
constraint.b.apply_force_at_local_point(-direction * correct_force)
2. 车辆操控过于敏感
问题:转向和油门响应过快
解决方案:
python
def smooth_controls(vehicle, dt):
"""平滑控制输入"""
# 输入平滑
smoothing_factor = 0.9
# 转向平滑
vehicle.state["steering"] = (
vehicle.state["steering"] * smoothing_factor +
vehicle.input["steering"] * (1 - smoothing_factor)
)
# 油门平滑
vehicle.state["throttle"] = (
vehicle.state["throttle"] * smoothing_factor +
vehicle.input["throttle"] * (1 - smoothing_factor)
)
# 根据速度调整转向灵敏度
speed = vehicle.state["speed"]
max_speed = 50 # m/s
# 速度越高,转向越不灵敏
steering_sensitivity = 1.0 - min(speed / max_speed, 0.8)
vehicle.state["steering"] *= steering_sensitivity
3. 内存泄漏检测
问题:游戏运行时间越长,内存占用越高
解决方案:
python
import gc
import objgraph
def check_memory_leaks():
"""检查内存泄漏"""
gc.collect() # 强制垃圾回收
# 统计对象数量
objects = gc.get_objects()
pymunk_objects = [obj for obj in objects if "pymunk" in str(type(obj))]
print(f"总对象数: {len(objects)}")
print(f"Pymunk对象数: {len(pymunk_objects)}")
# 检查循环引用
gc.set_debug(gc.DEBUG_SAVEALL)
# 显示最常见的对象类型
objgraph.show_most_common_types(limit=20)
# 检查特定类型的泄漏
pymunk_types = defaultdict(int)
for obj in pymunk_objects:
pymunk_types[type(obj).__name__] += 1
print("\nPymunk对象类型统计:")
for obj_type, count in sorted(pymunk_types.items(), key=lambda x: x[1], reverse=True):
print(f" {obj_type}: {count}")
5.6 下一步学习方向
继续本系列教程
-
第三篇:复杂碰撞篇 - 学习高级碰撞处理技巧
-
第四篇:角色控制篇 - 实现物理角色控制器
-
第五篇:流体与粒子篇 - 模拟流体和粒子系统
-
第六篇:工具与编辑器篇 - 创建物理编辑工具
高级主题研究
-
软体物理:绳索、布料、软体模拟
-
流体动力学:水流、烟雾、爆炸效果
-
破坏系统:基于物理的物体破坏
-
车辆动力学:更真实的轮胎模型、差速器
-
网络物理:多人游戏的物理同步
相关资源推荐
-
pymunk官方文档:http://www.pymunk.org
-
Chipmunk物理引擎:https://chipmunk-physics.net
-
《游戏物理引擎开发》:物理引擎实现原理
-
《车辆动力学与控制》:深入理解车辆物理
-
Box2D物理引擎:另一款优秀的2D物理引擎
5.7 项目扩展建议
创建完整的特技赛车游戏
-
赛道编辑器:创建自定义赛道
-
车辆改装系统:升级车辆性能
-
成就系统:挑战各种特技成就
-
回放系统:记录和分享精彩瞬间
-
多人模式:在线特技对战
开发物理教学工具
-
约束可视化:实时显示约束力和运动
-
参数影响分析:展示参数变化的物理影响
-
物理实验场景:预设各种物理实验
-
教育小游戏:通过游戏学习物理原理
结语
通过本篇《摇摆特技车》的实现,你已经掌握了pymunk约束系统的核心概念和高级应用。从简单的弹簧悬挂到复杂的车辆动力学,你已经具备了创建复杂物理模拟的能力。
记住,物理游戏开发的关键在于理解现实与游戏性的平衡。完全真实的物理模拟可能并不有趣,而恰到好处的"物理作弊"往往能创造更好的游戏体验。
继续实践是巩固知识的最佳方式。尝试修改车辆参数,观察操控特性的变化。创建自己的赛道,设计新的特技挑战。或者,完全重新设计车辆,比如创建摩托车、坦克甚至飞行器。
在开发过程中遇到问题时,不要忘记使用我们创建的调试工具。可视化约束力、追踪物理状态、实时调整参数,这些工具能大大加快你的开发速度。
物理游戏开发是一个充满创造性的领域。每一个约束、每一个关节、每一个物理参数,都是你创造独特游戏体验的工具。祝你在这个领域中玩得开心,创造出令人惊叹的物理游戏!
最后:保存好这个特技赛车项目,它不仅是一个学习工具,更是你未来物理游戏开发的坚实基础。在下一篇教程中,我们将探索更复杂的碰撞世界,期待你的继续学习!