Pymunk 2D物理游戏开发教程系列 第二篇:约束与关节篇 -《摇摆特技车》

游戏主题:2D特技赛车平衡游戏

代码行数:680行完整可运行游戏

学习目标:掌握pymunk约束与关节系统,实现复杂机械结构的物理模拟

最终效果:一个具有真实悬挂系统、可操控的2D特技赛车,可在各种地形上行驶、飞跃和表演特技


一、开篇引入:机械之美与关节艺术

从弹球到机械:物理游戏的进阶之路

在上一篇《弹球大作战》中,我们探索了刚体、形状和碰撞的基础世界。现在,我们将进入一个更加精彩的领域:约束与关节。如果说刚体是物理世界的"单词",那么关节就是将这些单词连接成复杂"句子"的语法规则。

想象一下:

  • 《桥梁建筑师》中那些精巧的桁架结构

  • 《人类一败涂地》中软绵绵角色的滑稽动作

  • 《Besiege》中复杂的战争机器

  • 《Spintires》中真实的车辆物理

这些游戏的核心魔法,都源自约束与关节系统的精妙运用。通过关节,我们可以创造从简单的铰链门到复杂的多足机器人的一切。

为什么学习约束与关节?

1. 模拟现实世界的连接方式

现实世界中很少有完全自由的物体。大多数物体都以某种方式连接:

  • 铰链连接的门窗

  • 弹簧悬挂的车辆

  • 齿轮传动的机械

  • 绳索牵引的装置

2. 创造有趣的游戏机制

关节不仅仅是物理模拟,更是游戏设计的工具:

  • 物理谜题:玩家需要理解约束来解谜

  • 载具操控:真实的车辆、飞机、机器人控制

  • 角色动画:更自然的角色动作和布娃娃系统

  • 可破坏环境:关节断裂产生的动态破坏效果

3. 性能优化的利器

相比大量自由运动的刚体,通过关节连接的刚体系统:

  • 计算更高效

  • 行为更可控

  • 更容易调试和调整

本篇学习目标:打造你的物理特技车

我们将通过创建一个2D特技赛车游戏,系统学习pymunk的约束系统。这辆车将具备:

  1. 真实的悬挂系统:弹簧阻尼悬挂,适应不平地形

  2. 精确的转向控制:前轮转向,后轮驱动

  3. 物理特技能力:飞跃、翻滚、平衡

  4. 复杂的碰撞响应:连续碰撞检测,防止高速穿透

  5. 可调参数系统:实时调整车辆性能

技术栈升级

除了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 车辆动力学基础

一个基本的车辆模型包含以下组件:

  1. 底盘(Chassis):车辆主体,承载大部分质量

  2. 车轮(Wheels):四个圆形刚体

  3. 悬挂(Suspension):弹簧阻尼系统,连接车轮和底盘

  4. 转向系统(Steering):控制前轮角度

  5. 驱动系统(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解决方案

  1. 扫描测试:检查物体从上一帧到当前帧的扫描体积

  2. 时间回溯:找到碰撞发生的精确时间

  3. 子步进:增加物理更新的频率

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解决方案

  1. 扫描测试:检查物体从上一帧到当前帧的扫描体积

  2. 时间回溯:找到碰撞发生的精确时间

  3. 子步进:增加物理更新的频率

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 性能优化策略

约束求解的优化

约束求解是物理引擎中最耗时的部分之一。优化策略:

  1. 约束分组:将不相关的约束分组求解

  2. 温暖启动:使用上一帧的解作为初始猜测

  3. 迭代次数调整:根据精度需求调整迭代次数

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 下一步学习方向

继续本系列教程
  • 第三篇:复杂碰撞篇​ - 学习高级碰撞处理技巧

  • 第四篇:角色控制篇​ - 实现物理角色控制器

  • 第五篇:流体与粒子篇​ - 模拟流体和粒子系统

  • 第六篇:工具与编辑器篇​ - 创建物理编辑工具

高级主题研究
  1. 软体物理:绳索、布料、软体模拟

  2. 流体动力学:水流、烟雾、爆炸效果

  3. 破坏系统:基于物理的物体破坏

  4. 车辆动力学:更真实的轮胎模型、差速器

  5. 网络物理:多人游戏的物理同步

相关资源推荐
  • pymunk官方文档http://www.pymunk.org

  • Chipmunk物理引擎https://chipmunk-physics.net

  • 《游戏物理引擎开发》:物理引擎实现原理

  • 《车辆动力学与控制》:深入理解车辆物理

  • Box2D物理引擎:另一款优秀的2D物理引擎

5.7 项目扩展建议

创建完整的特技赛车游戏
  1. 赛道编辑器:创建自定义赛道

  2. 车辆改装系统:升级车辆性能

  3. 成就系统:挑战各种特技成就

  4. 回放系统:记录和分享精彩瞬间

  5. 多人模式:在线特技对战

开发物理教学工具
  1. 约束可视化:实时显示约束力和运动

  2. 参数影响分析:展示参数变化的物理影响

  3. 物理实验场景:预设各种物理实验

  4. 教育小游戏:通过游戏学习物理原理

结语

通过本篇《摇摆特技车》的实现,你已经掌握了pymunk约束系统的核心概念和高级应用。从简单的弹簧悬挂到复杂的车辆动力学,你已经具备了创建复杂物理模拟的能力。

记住,物理游戏开发的关键在于理解现实与游戏性的平衡。完全真实的物理模拟可能并不有趣,而恰到好处的"物理作弊"往往能创造更好的游戏体验。

继续实践是巩固知识的最佳方式。尝试修改车辆参数,观察操控特性的变化。创建自己的赛道,设计新的特技挑战。或者,完全重新设计车辆,比如创建摩托车、坦克甚至飞行器。

在开发过程中遇到问题时,不要忘记使用我们创建的调试工具。可视化约束力、追踪物理状态、实时调整参数,这些工具能大大加快你的开发速度。

物理游戏开发是一个充满创造性的领域。每一个约束、每一个关节、每一个物理参数,都是你创造独特游戏体验的工具。祝你在这个领域中玩得开心,创造出令人惊叹的物理游戏!

最后:保存好这个特技赛车项目,它不仅是一个学习工具,更是你未来物理游戏开发的坚实基础。在下一篇教程中,我们将探索更复杂的碰撞世界,期待你的继续学习!

相关推荐
Trouvaille ~2 小时前
零基础入门 LangChain 与 LangGraph(三):环境搭建、包安装与第一个 LangChain 程序
python·ai·chatgpt·langchain·大模型·openai·langgraph
喜欢喝果茶.2 小时前
Qt翻译接口 -逐条翻译(免费级)
开发语言·python
南 阳2 小时前
Python从入门到精通day60
开发语言·python
不知名的老吴2 小时前
返回多个值:让函数输出更丰富又不复杂
开发语言·python
larance2 小时前
python包 解压修改后重新打成whl 包
开发语言·python
薛定猫AI2 小时前
【技术干货】Gemma 4 上手深度指南:本地多模态大模型的新基线
人工智能·架构·自动化
萤火阳光2 小时前
43|Python 异步生态深度:aiohttp/aiomysql/aioredis 全链路异步实战
开发语言·网络·python
Elastic 中国社区官方博客2 小时前
组合 OpenTelemetry 参考架构
大数据·数据库·elasticsearch·搜索引擎·架构
威联通安全存储3 小时前
云原生数据湖:QuObjects 本地 S3 对象存储解析
python·云原生