Pymunk物理引擎深度解析:从入门到实战的2D物理模拟全攻略

摘要

本文全面、系统地介绍Pymunk物理引擎的核心概念、架构设计、实战应用和性能优化策略。Pymunk作为Chipmunk物理引擎的Python绑定,是2D游戏开发、物理模拟、教育演示等领域的强大工具。文章将从基础理论出发,结合大量实战代码,深入探讨Pymunk的各个功能模块,为开发者提供全面的学习指南和最佳实践。

目录

  1. Pymunk物理引擎概述

  2. 核心概念深度解析

  3. 物理空间与坐标系系统

  4. 刚体系统:从静态到动态

  5. 碰撞形状与几何体

  6. 约束与关节系统

  7. 碰撞检测与响应机制

  8. 性能优化与调试技巧

  9. 实战案例:物理沙盒实现

  10. 高级应用与扩展

  11. 常见问题解决方案

  12. 学习资源与社区


1. Pymunk物理引擎概述

1.1 什么是Pymunk?

Pymunk是Chipmunk物理引擎的Python接口,专门为2D物理模拟设计。它提供了完整的物理模拟功能,包括:

  • 刚体动力学

  • 碰撞检测

  • 关节约束

  • 连续碰撞检测

  • 物理材质系统

1.2 为什么选择Pymunk?

主要优势:

bash 复制代码
# 安装简单
pip install pymunk

# 跨平台支持
# Windows, macOS, Linux, iOS, Android

# 性能优异
# 底层使用C语言编写,Python接口轻量高效

技术特点:

  • 纯Python API,易于学习和使用

  • 丰富的文档和社区支持

  • 与Pygame、Pyglet、PyOpenGL等图形库无缝集成

  • 支持多种物理特性:重力、摩擦、弹性、阻尼

1.3 应用场景

  1. 游戏开发:2D平台游戏、物理谜题、弹球游戏

  2. 物理仿真:机械系统、粒子系统、结构分析

  3. 教育工具:物理教学演示、交互式实验

  4. 数据可视化:物理过程的可视化展示

  5. 原型开发:快速验证物理概念

2. 核心概念深度解析

2.1 物理世界的四个基本元素

Pymunk的核心架构围绕四个基本元素构建:

python 复制代码
import pymunk

# 1. 空间 (Space) - 物理世界的容器
space = pymunk.Space()

# 2. 刚体 (Body) - 物体的运动和位置
body = pymunk.Body(mass=1, moment=10)

# 3. 形状 (Shape) - 物体的碰撞边界
shape = pymunk.Circle(body, radius=10)

# 4. 约束 (Constraint) - 连接物体的关系
constraint = pymunk.PivotJoint(body1, body2, (0, 0))

2.2 坐标系与单位系统

Pymunk使用右手坐标系,单位灵活:

python 复制代码
# 物理单位系统建议
# 长度:像素(pixel)
# 质量:千克(kg)
# 时间:秒(s)
# 角度:弧度(radian)

# 单位转换示例
def convert_units(pixels_per_meter=50.0):
    """物理单位转换函数"""
    class UnitConverter:
        def __init__(self, ppm):
            self.ppm = ppm  # 像素/米
        
        def to_pixels(self, meters):
            """米转像素"""
            return meters * self.ppm
        
        def to_meters(self, pixels):
            """像素转米"""
            return pixels / self.ppm
        
        def apply_gravity(self, g_mps2=9.81):
            """应用真实重力"""
            return g_mps2 * self.ppm
    
    return UnitConverter(pixels_per_meter)

# 使用示例
converter = convert_units(50.0)
space.gravity = (0, -converter.apply_gravity())

3. 物理空间与坐标系系统

3.1 空间(Space)的深度解析

物理空间是Pymunk的核心容器,管理所有物理实体:

python 复制代码
class AdvancedSpace:
    def __init__(self, width=800, height=600):
        # 创建物理空间
        self.space = pymunk.Space()
        
        # 基础物理参数
        self.space.gravity = (0, 900)  # 像素/秒²
        
        # 空间迭代参数
        self.space.iterations = 10     # 速度迭代次数
        self.space.damping = 0.9       # 全局阻尼
        
        # 空间划分优化
        self.setup_spatial_partitioning()
        
        # 碰撞处理器
        self.setup_collision_handlers()
    
    def setup_spatial_partitioning(self):
        """设置空间划分优化"""
        # 方法1:空间哈希(适合均匀分布的小物体)
        cell_size = 100
        self.space.use_spatial_hash(cell_size)
        
        # 方法2:四叉树(适合分布不均匀的场景)
        # 自动管理,无需显式设置
        
        # 获取空间统计信息
        stats = self.space.get_stats()
        print(f"空间统计: {stats}")
    
    def setup_collision_handlers(self):
        """设置碰撞处理器"""
        # 添加默认碰撞处理器
        handler = self.space.add_default_collision_handler()
        
        # 碰撞开始回调
        handler.begin = self.on_collision_begin
        
        # 碰撞结束回调
        handler.separate = self.on_collision_separate
        
        # 预求解回调(碰撞解决前)
        handler.pre_solve = self.on_collision_presolve
        
        # 后求解回调(碰撞解决后)
        handler.post_solve = self.on_collision_postsolve
    
    def update(self, dt):
        """更新物理空间"""
        # 固定时间步长
        fixed_dt = 1.0 / 60.0
        
        # 子步进更新,提高稳定性
        num_substeps = 2
        substep_dt = fixed_dt / num_substeps
        
        for _ in range(num_substeps):
            self.space.step(substep_dt)
        
        # 清理移除的物体
        self.cleanup_removed_objects()
    
    def cleanup_removed_objects(self):
        """清理被移除的物体"""
        # 手动清理不再需要的形状和约束
        pass

3.2 空间统计与性能监控

python 复制代码
class SpaceMonitor:
    def __init__(self, space):
        self.space = space
        self.stats_history = []
        self.max_history = 100
        
    def collect_stats(self):
        """收集空间统计信息"""
        stats = self.space.get_stats()
        
        # 记录统计数据
        frame_stats = {
            'time': time.time(),
            'dynamic_bodies': stats.dynamic_bodies,
            'static_bodies': stats.static_bodies,
            'shapes': stats.shapes,
            'constraints': stats.constraints,
            'arbiters': stats.arbiters,
            'contacts': stats.contacts,
            'joints': stats.joints
        }
        
        self.stats_history.append(frame_stats)
        
        # 保持历史记录长度
        if len(self.stats_history) > self.max_history:
            self.stats_history.pop(0)
        
        return frame_stats
    
    def analyze_performance(self):
        """分析性能数据"""
        if not self.stats_history:
            return None
        
        recent_stats = self.stats_history[-10:]  # 最近10帧
        
        analysis = {
            'avg_shapes': sum(s['shapes'] for s in recent_stats) / len(recent_stats),
            'avg_constraints': sum(s['constraints'] for s in recent_stats) / len(recent_stats),
            'peak_contacts': max(s['contacts'] for s in recent_stats),
            'memory_usage': self.estimate_memory_usage()
        }
        
        return analysis
    
    def estimate_memory_usage(self):
        """估计内存使用"""
        stats = self.space.get_stats()
        
        # 粗略估计(单位:字节)
        body_memory = (stats.dynamic_bodies + stats.static_bodies) * 256
        shape_memory = stats.shapes * 128
        constraint_memory = stats.constraints * 192
        
        return body_memory + shape_memory + constraint_memory
    
    def print_stats(self):
        """打印统计信息"""
        stats = self.collect_stats()
        
        print("=== 物理空间统计 ===")
        print(f"刚体: {stats['dynamic_bodies']}动态, {stats['static_bodies']}静态")
        print(f"形状: {stats['shapes']}")
        print(f"约束: {stats['constraints']}")
        print(f"接触点: {stats['contacts']}")
        print(f"仲裁器: {stats['arbiters']}")
        print("===================")

4. 刚体系统:从静态到动态

4.1 刚体类型详解

Pymunk支持三种刚体类型:

python 复制代码
class BodySystem:
    def __init__(self, space):
        self.space = space
        
    def create_dynamic_body(self, mass, moment, position=(0, 0)):
        """创建动态刚体"""
        # 动态刚体:有质量,受力和碰撞影响
        body = pymunk.Body(mass, moment)
        body.position = position
        
        # 设置物理属性
        body.velocity_limit = 1000      # 速度限制
        body.angular_velocity_limit = 10  # 角速度限制
        body.sleeping_threshold = 0.5   # 休眠阈值
        
        # 添加到空间
        self.space.add(body)
        
        return body
    
    def create_kinematic_body(self, position=(0, 0)):
        """创建运动学刚体"""
        # 运动学刚体:无质量,不受力,可编程控制
        body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
        body.position = position
        
        # 可以设置速度和角速度
        body.velocity = (100, 0)
        body.angular_velocity = 1.0
        
        self.space.add(body)
        return body
    
    def create_static_body(self, position=(0, 0)):
        """创建静态刚体"""
        # 静态刚体:无限质量,不受力,用于地形
        body = pymunk.Body(body_type=pymunk.Body.STATIC)
        body.position = position
        
        self.space.add(body)
        return body
    
    def apply_forces_and_impulses(self, body):
        """施力与冲量"""
        # 1. 持续力(牛顿)
        force_vector = (100, 0)  # 向右100牛顿
        body.apply_force_at_world_point(force_vector, body.position)
        
        # 2. 冲量(瞬时力)
        impulse_vector = (0, -500)  # 向上500牛顿·秒
        body.apply_impulse_at_world_point(impulse_vector, body.position)
        
        # 3. 扭矩(旋转力)
        torque = 50  # 顺时针扭矩
        body.torque = torque
        
        # 4. 角冲量
        angular_impulse = 20
        body.angular_velocity += angular_impulse / body.moment
    
    def calculate_moment_of_inertia(self, shape_type, mass, dimensions):
        """计算转动惯量"""
        if shape_type == "circle":
            radius = dimensions["radius"]
            moment = pymunk.moment_for_circle(mass, 0, radius)
            
        elif shape_type == "box":
            width, height = dimensions["width"], dimensions["height"]
            moment = pymunk.moment_for_box(mass, (width, height))
            
        elif shape_type == "poly":
            vertices = dimensions["vertices"]
            moment = pymunk.moment_for_poly(mass, vertices)
            
        else:
            raise ValueError(f"未知形状类型: {shape_type}")
        
        return moment
    
    def body_state_management(self, body):
        """刚体状态管理"""
        # 获取刚体状态
        state = {
            'position': body.position,
            'velocity': body.velocity,
            'angle': body.angle,
            'angular_velocity': body.angular_velocity,
            'force': body.force,
            'torque': body.torque,
            'is_sleeping': body.is_sleeping
        }
        
        # 休眠管理
        if body.is_sleeping:
            print(f"刚体 {body} 正在休眠")
        
        # 唤醒刚体
        body.activate()
        
        return state

4.2 刚体控制模式

python 复制代码
class BodyController:
    def __init__(self, body):
        self.body = body
        self.target_position = body.position
        self.target_velocity = (0, 0)
        self.control_mode = "position"  # position, velocity, force
        
    def update(self, dt):
        """根据控制模式更新刚体"""
        if self.control_mode == "position":
            self.position_control(dt)
        elif self.control_mode == "velocity":
            self.velocity_control(dt)
        elif self.control_mode == "force":
            self.force_control(dt)
    
    def position_control(self, dt):
        """位置控制(PID控制器)"""
        kp = 1000.0  # 比例增益
        kd = 100.0   # 微分增益
        
        # 计算误差
        error = pymunk.Vec2d(self.target_position) - self.body.position
        
        # 计算所需速度
        desired_velocity = error * kp
        
        # 计算速度误差
        velocity_error = desired_velocity - self.body.velocity
        
        # 计算力
        force = velocity_error * kd
        
        # 应用力
        self.body.apply_force_at_world_point(force, self.body.position)
    
    def velocity_control(self, dt):
        """速度控制"""
        kp = 500.0
        
        # 计算速度误差
        velocity_error = pymunk.Vec2d(self.target_velocity) - self.body.velocity
        
        # 计算力
        force = velocity_error * kp
        
        # 应用力
        self.body.apply_force_at_world_point(force, self.body.position)
    
    def force_control(self, dt):
        """力控制"""
        # 直接应用力
        force = pymunk.Vec2d(self.target_velocity) * self.body.mass
        self.body.apply_force_at_world_point(force, self.body.position)
    
    def set_target(self, position=None, velocity=None):
        """设置目标"""
        if position is not None:
            self.target_position = position
            self.control_mode = "position"
        elif velocity is not None:
            self.target_velocity = velocity
            self.control_mode = "velocity"

5. 碰撞形状与几何体

5.1 形状类型详解

Pymunk支持多种基本几何形状:

python 复制代码
class ShapeFactory:
    def __init__(self, space):
        self.space = space
        
    def create_circle_shape(self, body, radius, offset=(0, 0)):
        """创建圆形形状"""
        shape = pymunk.Circle(body, radius, offset)
        
        # 设置物理属性
        shape.friction = 0.7
        shape.elasticity = 0.8
        shape.density = 1.0
        
        # 设置碰撞类别
        shape.filter = pymunk.ShapeFilter(categories=0x1)
        
        # 添加用户数据
        shape.user_data = {"type": "circle", "radius": radius}
        
        self.space.add(shape)
        return shape
    
    def create_box_shape(self, body, width, height):
        """创建矩形形状"""
        shape = pymunk.Poly.create_box(body, (width, height))
        
        shape.friction = 0.5
        shape.elasticity = 0.3
        shape.density = 2.0
        
        self.space.add(shape)
        return shape
    
    def create_polygon_shape(self, body, vertices):
        """创建多边形形状"""
        # 确保顶点是凸多边形
        shape = pymunk.Poly(body, vertices)
        
        shape.friction = 0.6
        shape.elasticity = 0.5
        
        self.space.add(shape)
        return shape
    
    def create_segment_shape(self, body, a, b, radius=1):
        """创建线段形状"""
        shape = pymunk.Segment(body, a, b, radius)
        
        shape.friction = 0.9
        shape.elasticity = 0.1
        
        self.space.add(shape)
        return shape
    
    def create_composite_shape(self, body, shapes_config):
        """创建复合形状"""
        shapes = []
        
        for config in shapes_config:
            if config["type"] == "circle":
                shape = self.create_circle_shape(
                    body, 
                    config["radius"], 
                    config.get("offset", (0, 0))
                )
            elif config["type"] == "box":
                shape = self.create_box_shape(
                    body,
                    config["width"],
                    config["height"]
                )
            shapes.append(shape)
        
        return shapes
    
    def create_convex_hull(self, points, body=None):
        """创建凸包形状"""
        if body is None:
            mass = 1.0
            moment = pymunk.moment_for_poly(mass, points)
            body = pymunk.Body(mass, moment)
        
        # 计算凸包
        hull_points = self.compute_convex_hull(points)
        
        shape = pymunk.Poly(body, hull_points)
        self.space.add(body, shape)
        
        return body, shape
    
    def compute_convex_hull(self, points):
        """计算凸包(Graham Scan算法)"""
        if len(points) <= 3:
            return points
        
        # 找到最左下角的点
        def find_pivot(points):
            pivot = points[0]
            for p in points[1:]:
                if p.y < pivot.y or (p.y == pivot.y and p.x < pivot.x):
                    pivot = p
            return pivot
        
        pivot = find_pivot(points)
        
        # 极角排序
        def polar_angle(p):
            return math.atan2(p.y - pivot.y, p.x - pivot.x)
        
        sorted_points = sorted(points, key=polar_angle)
        
        # Graham Scan
        hull = [pivot, sorted_points[0]]
        
        for point in sorted_points[1:]:
            while len(hull) >= 2:
                a, b = hull[-2], hull[-1]
                if self.cross_product(a, b, point) <= 0:
                    hull.pop()
                else:
                    break
            hull.append(point)
        
        return hull
    
    def cross_product(self, a, b, c):
        """叉积计算方向"""
        return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)

5.2 碰撞过滤与图层系统

python 复制代码
class CollisionFilterSystem:
    def __init__(self):
        # 定义碰撞类别
        self.categories = {
            "TERRAIN": 0b00000001,
            "PLAYER": 0b00000010,
            "ENEMY": 0b00000100,
            "PROJECTILE": 0b00001000,
            "POWERUP": 0b00010000,
            "SENSOR": 0b00100000,
        }
        
        # 定义碰撞掩码(谁能与谁碰撞)
        self.masks = {
            "TERRAIN": self.categories["PLAYER"] | self.categories["ENEMY"] | self.categories["PROJECTILE"],
            "PLAYER": self.categories["TERRAIN"] | self.categories["ENEMY"] | self.categories["PROJECTILE"] | self.categories["POWERUP"],
            "ENEMY": self.categories["TERRAIN"] | self.categories["PLAYER"] | self.categories["PROJECTILE"],
            "PROJECTILE": self.categories["TERRAIN"] | self.categories["PLAYER"] | self.categories["ENEMY"],
            "POWERUP": self.categories["PLAYER"],
            "SENSOR": 0,  # 传感器不产生碰撞
        }
    
    def create_shape_filter(self, category_name, group=0):
        """创建形状过滤器"""
        category = self.categories.get(category_name, 0)
        mask = self.masks.get(category_name, 0xFFFFFFFF)
        
        return pymunk.ShapeFilter(
            categories=category,
            mask=mask,
            group=group
        )
    
    def setup_collision_groups(self):
        """设置碰撞组"""
        # 同一组内的形状永远不会碰撞
        return {
            "player_group": 1,
            "enemy_group": 2,
            "projectile_group": 3
        }

6. 约束与关节系统

6.1 约束类型详解

python 复制代码
class ConstraintSystem:
    def __init__(self, space):
        self.space = space
        
    def create_pivot_joint(self, body_a, body_b, anchor_a, anchor_b):
        """创建枢轴关节"""
        joint = pymunk.PivotJoint(body_a, body_b, anchor_a, anchor_b)
        
        # 设置关节限制
        joint.max_force = 1000  # 最大力
        joint.max_bias = 100    # 最大偏差
        
        self.space.add(joint)
        return joint
    
    def create_slide_joint(self, body_a, body_b, anchor_a, anchor_b, min_len, max_len):
        """创建滑动关节"""
        joint = pymunk.SlideJoint(body_a, body_b, anchor_a, anchor_b, min_len, max_len)
        
        joint.max_force = 500
        joint.max_bias = 50
        
        self.space.add(joint)
        return joint
    
    def create_pin_joint(self, body_a, body_b, anchor_a, anchor_b):
        """创建销关节"""
        joint = pymunk.PinJoint(body_a, body_b, anchor_a, anchor_b)
        
        joint.max_force = 800
        
        self.space.add(joint)
        return joint
    
    def create_gear_joint(self, body_a, body_b, phase, ratio):
        """创建齿轮关节"""
        joint = pymunk.GearJoint(body_a, body_b, phase, ratio)
        
        joint.max_force = 1000
        joint.max_bias = 100
        
        self.space.add(joint)
        return joint
    
    def create_motor_joint(self, body_a, body_b, rate):
        """创建马达关节"""
        joint = pymunk.SimpleMotor(body_a, body_b, rate)
        
        joint.max_force = 200
        joint.max_bias = 20
        
        self.space.add(joint)
        return joint
    
    def create_rotary_spring(self, body_a, body_b, rest_angle, stiffness, damping):
        """创建旋转弹簧"""
        joint = pymunk.DampedRotarySpring(body_a, body_b, rest_angle, stiffness, damping)
        
        self.space.add(joint)
        return joint
    
    def create_distance_joint_with_spring(self, body_a, body_b, anchor_a, anchor_b, 
                                          rest_length, stiffness, damping):
        """创建带弹簧的距离关节"""
        joint = pymunk.DampedSpring(body_a, body_b, anchor_a, anchor_b, 
                                   rest_length, stiffness, damping)
        
        self.space.add(joint)
        return joint

6.2 复杂机械结构示例

python 复制代码
class MechanicalStructures:
    def __init__(self, space):
        self.space = space
        
    def create_pendulum(self, anchor_position, length, bob_mass):
        """创建单摆"""
        # 锚点(静态刚体)
        static_body = self.space.static_body
        
        # 摆锤
        bob_body = pymunk.Body(bob_mass, pymunk.moment_for_circle(bob_mass, 0, 10))
        bob_body.position = (anchor_position[0], anchor_position[1] + length)
        
        # 摆锤形状
        bob_shape = pymunk.Circle(bob_body, 10)
        bob_shape.friction = 0.3
        
        # 枢轴关节
        pivot_joint = pymunk.PivotJoint(static_body, bob_body, anchor_position, (0, 0))
        
        # 添加到空间
        self.space.add(bob_body, bob_shape, pivot_joint)
        
        return {
            'bob_body': bob_body,
            'bob_shape': bob_shape,
            'pivot_joint': pivot_joint
        }
    
    def create_double_pendulum(self, anchor_position, length1, length2, mass1, mass2):
        """创建双摆"""
        # 锚点
        static_body = self.space.static_body
        
        # 第一个摆锤
        body1 = pymunk.Body(mass1, pymunk.moment_for_circle(mass1, 0, 8))
        body1.position = (anchor_position[0], anchor_position[1] + length1)
        
        shape1 = pymunk.Circle(body1, 8)
        shape1.friction = 0.3
        
        # 连接第一个摆锤到锚点
        joint1 = pymunk.PivotJoint(static_body, body1, anchor_position, (0, 0))
        
        # 第二个摆锤
        body2 = pymunk.Body(mass2, pymunk.moment_for_circle(mass2, 0, 6))
        body2.position = (anchor_position[0], anchor_position[1] + length1 + length2)
        
        shape2 = pymunk.Circle(body2, 6)
        shape2.friction = 0.3
        
        # 连接两个摆锤
        joint2 = pymunk.PivotJoint(body1, body2, (0, 0), (0, -length1))
        
        # 添加到空间
        self.space.add(body1, shape1, joint1, body2, shape2, joint2)
        
        return {
            'bodies': [body1, body2],
            'shapes': [shape1, shape2],
            'joints': [joint1, joint2]
        }
    
    def create_chain(self, start_position, num_links, link_length, link_mass):
        """创建链条"""
        links = []
        shapes = []
        joints = []
        
        # 创建第一个链节
        prev_body = pymunk.Body(link_mass, pymunk.moment_for_box(link_mass, (link_length, 5)))
        prev_body.position = start_position
        
        prev_shape = pymunk.Poly.create_box(prev_body, (link_length, 5))
        
        # 固定第一个链节
        static_body = self.space.static_body
        joint = pymunk.PivotJoint(static_body, prev_body, start_position, (0, 0))
        joints.append(joint)
        
        self.space.add(prev_body, prev_shape, joint)
        links.append(prev_body)
        shapes.append(prev_shape)
        
        # 创建剩余链节
        for i in range(1, num_links):
            body = pymunk.Body(link_mass, pymunk.moment_for_box(link_mass, (link_length, 5)))
            body.position = (start_position[0] + i * link_length, start_position[1])
            
            shape = pymunk.Poly.create_box(body, (link_length, 5))
            
            # 连接链节
            joint = pymunk.PivotJoint(prev_body, body, (link_length/2, 0), (-link_length/2, 0))
            
            self.space.add(body, shape, joint)
            
            links.append(body)
            shapes.append(shape)
            joints.append(joint)
            
            prev_body = body
        
        return {
            'links': links,
            'shapes': shapes,
            'joints': joints
        }
    
    def create_car(self, position, width=80, height=30, wheel_radius=15):
        """创建汽车模型"""
        car_parts = {}
        
        # 创建车身
        chassis_body = pymunk.Body(100, pymunk.moment_for_box(100, (width, height)))
        chassis_body.position = position
        
        chassis_shape = pymunk.Poly.create_box(chassis_body, (width, height))
        chassis_shape.friction = 0.8
        
        self.space.add(chassis_body, chassis_shape)
        car_parts['chassis'] = {'body': chassis_body, 'shape': chassis_shape}
        
        # 创建前轮
        front_wheel_body = pymunk.Body(20, pymunk.moment_for_circle(20, 0, wheel_radius))
        front_wheel_body.position = (position[0] + width/3, position[1] - height/2 - wheel_radius)
        
        front_wheel_shape = pymunk.Circle(front_wheel_body, wheel_radius)
        front_wheel_shape.friction = 1.0
        
        self.space.add(front_wheel_body, front_wheel_shape)
        car_parts['front_wheel'] = {'body': front_wheel_body, 'shape': front_wheel_shape}
        
        # 创建后轮
        rear_wheel_body = pymunk.Body(20, pymunk.moment_for_circle(20, 0, wheel_radius))
        rear_wheel_body.position = (position[0] - width/3, position[1] - height/2 - wheel_radius)
        
        rear_wheel_shape = pymunk.Circle(rear_wheel_body, wheel_radius)
        rear_wheel_shape.friction = 1.0
        
        self.space.add(rear_wheel_body, rear_wheel_shape)
        car_parts['rear_wheel'] = {'body': rear_wheel_body, 'shape': rear_wheel_shape}
        
        # 连接车轮和车身
        front_spring = pymunk.DampedSpring(
            chassis_body, front_wheel_body,
            (width/3, -height/2), (0, 0),
            rest_length=0, stiffness=50, damping=10
        )
        
        rear_spring = pymunk.DampedSpring(
            chassis_body, rear_wheel_body,
            (-width/3, -height/2), (0, 0),
            rest_length=0, stiffness=50, damping=10
        )
        
        self.space.add(front_spring, rear_spring)
        car_parts['springs'] = [front_spring, rear_spring]
        
        return car_parts

7. 碰撞检测与响应机制

7.1 碰撞处理器详解

python 复制代码
class CollisionHandlerSystem:
    def __init__(self, space):
        self.space = space
        self.handlers = {}
        
    def add_collision_handler(self, type_a, type_b, callbacks=None):
        """添加碰撞处理器"""
        handler = self.space.add_collision_handler(type_a, type_b)
        
        if callbacks:
            if 'begin' in callbacks:
                handler.begin = callbacks['begin']
            if 'pre_solve' in callbacks:
                handler.pre_solve = callbacks['pre_solve']
            if 'post_solve' in callbacks:
                handler.post_solve = callbacks['post_solve']
            if 'separate' in callbacks:
                handler.separate = callbacks['separate']
        
        key = (type_a, type_b)
        self.handlers[key] = handler
        
        return handler
    
    def create_collision_example(self):
        """创建碰撞检测示例"""
        # 定义碰撞类型
        COLLTYPE_PLAYER = 1
        COLLTYPE_ENEMY = 2
        COLLTYPE_PROJECTILE = 3
        COLLTYPE_WALL = 4
        
        # 设置玩家-敌人碰撞处理器
        def player_enemy_begin(arbiter, space, data):
            """玩家与敌人碰撞开始"""
            print("玩家与敌人碰撞!")
            
            # 获取碰撞信息
            shapes = arbiter.shapes
            contacts = arbiter.contact_point_set
            
            # 计算碰撞法线
            normal = contacts.normal
            print(f"碰撞法线: {normal}")
            
            # 返回True允许碰撞,False忽略碰撞
            return True
        
        def player_enemy_pre_solve(arbiter, space, data):
            """碰撞预求解"""
            # 可以修改碰撞参数
            for contact in arbiter.contact_point_set.points:
                contact.distance = 0  # 穿透深度
                
            return True
        
        def player_enemy_post_solve(arbiter, space, data):
            """碰撞后求解"""
            # 计算碰撞冲量
            total_impulse = 0
            for contact in arbiter.contact_point_set.points:
                total_impulse += contact.normal_impulse
            
            print(f"碰撞冲量: {total_impulse}")
            
            # 应用伤害
            shapes = arbiter.shapes
            player_shape, enemy_shape = shapes
            
            # 假设每个形状都有user_data
            if hasattr(player_shape, 'user_data'):
                player_shape.user_data['health'] -= total_impulse * 0.1
        
        def player_enemy_separate(arbiter, space, data):
            """碰撞分离"""
            print("玩家与敌人分离!")
        
        # 注册处理器
        self.add_collision_handler(
            COLLTYPE_PLAYER, COLLTYPE_ENEMY,
            {
                'begin': player_enemy_begin,
                'pre_solve': player_enemy_pre_solve,
                'post_solve': player_enemy_post_solve,
                'separate': player_enemy_separate
            }
        )
    
    def raycast_query(self, start, end, radius=0, shape_filter=None):
        """射线检测"""
        # 执行射线检测
        query_info = self.space.segment_query_first(start, end, radius, shape_filter)
        
        if query_info:
            return {
                'shape': query_info.shape,
                'point': query_info.point,
                'normal': query_info.normal,
                'alpha': query_info.alpha
            }
        return None
    
    def bounding_box_query(self, bb, shape_filter=None):
        """边界框查询"""
        shapes = self.space.bb_query(bb, shape_filter)
        return shapes
    
    def point_query(self, point, max_distance=0, shape_filter=None):
        """点查询"""
        query_info = self.space.point_query_nearest(point, max_distance, shape_filter)
        
        if query_info:
            return {
                'shape': query_info.shape,
                'point': query_info.point,
                'distance': query_info.distance,
                'gradient': query_info.gradient
            }
        return None
    
    def shape_query(self, shape):
        """形状查询(检测与其他形状的重叠)"""
        query_info = self.space.shape_query(shape)
        
        results = []
        for info in query_info:
            results.append({
                'shape': info.shape,
                'contact_points': info.contact_point_set
            })
        
        return results

7.2 连续碰撞检测

python 复制代码
class ContinuousCollisionDetection:
    def __init__(self, space):
        self.space = space
        
    def enable_ccd_for_body(self, body, velocity_threshold=100):
        """为刚体启用连续碰撞检测"""
        # 设置CCD属性
        body.velocity_func = self.velocity_function
        
        # 为所有形状启用CCD
        for shape in body.shapes:
            shape.collision_type = 1  # 设置碰撞类型
            shape.sensor = False
            shape.ccd_velocity_threshold = velocity_threshold
            
    def velocity_function(self, body, gravity, damping, dt):
        """自定义速度函数,支持CCD"""
        # 保存当前位置
        prev_position = body.position
        
        # 计算新位置
        body.update_velocity(body, gravity, damping, dt)
        body.update_position(body, dt)
        
        # 检查是否发生高速移动
        velocity = body.velocity.length
        if velocity > 100:  # 速度阈值
            # 执行连续碰撞检测
            self.perform_ccd(body, prev_position, body.position)
    
    def perform_ccd(self, body, start, end):
        """执行连续碰撞检测"""
        # 创建检测形状
        for shape in body.shapes:
            # 使用射线检测
            query_info = self.space.segment_query_first(
                start, end, 
                shape.radius,  # 形状半径
                shape.filter
            )
            
            if query_info:
                # 发生碰撞,调整位置
                collision_point = query_info.point
                collision_normal = query_info.normal
                
                # 计算反弹
                self.handle_collision_response(body, collision_point, collision_normal)
    
    def handle_collision_response(self, body, point, normal):
        """处理碰撞响应"""
        # 计算反射速度
        velocity = body.velocity
        normal_velocity = velocity.dot(normal)
        
        if normal_velocity < 0:  # 朝向碰撞面
            # 计算反射
            reflection = velocity - (1 + body.restitution) * normal_velocity * normal
            body.velocity = reflection
            
            # 调整位置,避免穿透
            penetration_depth = 1.0
            body.position = point - normal * penetration_depth

8. 性能优化与调试技巧

8.1 性能优化策略

python 复制代码
class PerformanceOptimizer:
    def __init__(self, space):
        self.space = space
        self.performance_stats = {
            'frame_times': [],
            'update_times': [],
            'collision_queries': 0
        }
        
    def optimize_space_settings(self):
        """优化空间设置"""
        # 调整迭代次数
        self.space.iterations = 10  # 速度迭代
        self.space.damping = 0.95   # 全局阻尼
        
        # 设置空间哈希
        cell_size = 100
        self.space.use_spatial_hash(cell_size)
        
        # 启用休眠
        self.space.sleep_time_threshold = 0.5
        self.space.idle_speed_threshold = 0.5
        
    def batch_add_remove_objects(self, objects):
        """批量添加/移除对象"""
        # 批量添加
        self.space.add(*objects)
        
        # 批量移除
        self.space.remove(*objects)
        
    def object_pooling_system(self, pool_size=100):
        """对象池系统"""
        class ObjectPool:
            def __init__(self, create_func, reset_func):
                self.create_func = create_func
                self.reset_func = reset_func
                self.pool = []
                self.active_objects = []
                
            def get_object(self):
                """获取对象"""
                if self.pool:
                    obj = self.pool.pop()
                else:
                    obj = self.create_func()
                
                self.reset_func(obj)
                self.active_objects.append(obj)
                return obj
                
            def return_object(self, obj):
                """归还对象"""
                if obj in self.active_objects:
                    self.active_objects.remove(obj)
                    self.pool.append(obj)
                    
            def cleanup(self):
                """清理所有对象"""
                for obj in self.active_objects[:]:
                    self.return_object(obj)
        
        return ObjectPool
        
    def profile_performance(self):
        """性能分析"""
        import time
        import cProfile
        import pstats
        from pstats import SortKey
        
        def profile_update(dt):
            """分析更新函数性能"""
            pr = cProfile.Profile()
            pr.enable()
            
            # 执行更新
            self.space.step(dt)
            
            pr.disable()
            
            # 输出统计
            ps = pstats.Stats(pr)
            ps.sort_stats(SortKey.TIME)
            ps.print_stats(10)
            
        return profile_update

8.2 调试与可视化

python 复制代码
class DebugVisualizer:
    def __init__(self, screen, space):
        self.screen = screen
        self.space = space
        self.font = pygame.font.Font(None, 24)
        
    def draw_debug_info(self, position=(10, 10)):
        """绘制调试信息"""
        stats = self.space.get_stats()
        
        info_lines = [
            f"动态刚体: {stats.dynamic_bodies}",
            f"静态刚体: {stats.static_bodies}",
            f"形状: {stats.shapes}",
            f"约束: {stats.constraints}",
            f"接触点: {stats.contacts}",
            f"仲裁器: {stats.arbiters}"
        ]
        
        y_offset = position[1]
        for line in info_lines:
            text_surface = self.font.render(line, True, (255, 255, 255))
            self.screen.blit(text_surface, (position[0], y_offset))
            y_offset += 25
            
    def draw_collision_normals(self, color=(255, 0, 0)):
        """绘制碰撞法线"""
        for arbiter in self.space.arbiters:
            for contact in arbiter.contact_point_set.points:
                point = contact.point_a
                normal = contact.normal
                
                # 绘制接触点
                pygame.draw.circle(self.screen, color, 
                                 (int(point.x), int(point.y)), 3)
                
                # 绘制法线
                end_point = (point.x + normal.x * 20, 
                           point.y + normal.y * 20)
                pygame.draw.line(self.screen, color,
                               (int(point.x), int(point.y)),
                               (int(end_point[0]), int(end_point[1])), 2)
    
    def draw_bounding_boxes(self, color=(0, 255, 0)):
        """绘制边界框"""
        for shape in self.space.shapes:
            bb = shape.bb
            
            # 绘制边界框
            rect = pygame.Rect(bb.left, bb.bottom, 
                             bb.right - bb.left, bb.top - bb.bottom)
            pygame.draw.rect(self.screen, color, rect, 1)
    
    def draw_velocity_vectors(self, scale=0.1, color=(0, 0, 255)):
        """绘制速度向量"""
        for body in self.space.bodies:
            if body.body_type == pymunk.Body.DYNAMIC:
                start = body.position
                velocity = body.velocity
                
                if velocity.length > 0:
                    end = (start.x + velocity.x * scale,
                          start.y + velocity.y * scale)
                    
                    # 绘制向量
                    pygame.draw.line(self.screen, color,
                                   (int(start.x), int(start.y)),
                                   (int(end[0]), int(end[1])), 2)
                    
                    # 绘制箭头
                    angle = math.atan2(velocity.y, velocity.x)
                    arrow_length = 10
                    arrow_angle = math.pi / 6
                    
                    # 左箭头
                    left_angle = angle + math.pi - arrow_angle
                    left_end = (end[0] + arrow_length * math.cos(left_angle),
                              end[1] + arrow_length * math.sin(left_angle))
                    
                    # 右箭头
                    right_angle = angle + math.pi + arrow_angle
                    right_end = (end[0] + arrow_length * math.cos(right_angle),
                               end[1] + arrow_length * math.sin(right_angle))
                    
                    pygame.draw.line(self.screen, color,
                                   (int(end[0]), int(end[1])),
                                   (int(left_end[0]), int(left_end[1])), 2)
                    pygame.draw.line(self.screen, color,
                                   (int(end[0]), int(end[1])),
                                   (int(right_end[0]), int(right_end[1])), 2)

9. 实战案例:物理沙盒实现

9.1 物理沙盒核心实现

python 复制代码
class PhysicsSandbox:
    def __init__(self, width=800, height=600):
        # 初始化Pygame
        pygame.init()
        self.screen = pygame.display.set_mode((width, height))
        pygame.display.set_caption("Pymunk物理沙盒")
        
        # 创建物理空间
        self.space = pymunk.Space()
        self.space.gravity = (0, 900)  # 向下重力
        self.space.damping = 0.9  # 阻尼
        
        # 调试可视化
        self.debug = DebugVisualizer(self.screen, self.space)
        
        # 对象工厂
        self.shape_factory = ShapeFactory(self.space)
        self.constraint_system = ConstraintSystem(self.space)
        
        # 工具状态
        self.current_tool = "circle"  # 当前工具:circle, box, polygon, constraint
        self.selected_body = None
        self.dragging = False
        self.drag_start = None
        
        # 运行状态
        self.running = True
        self.clock = pygame.time.Clock()
        self.fps = 60
        self.paused = False
        self.show_debug = True
        
        # 颜色定义
        self.colors = {
            'background': (30, 30, 40),
            'static': (100, 100, 100),
            'dynamic': (70, 130, 180),
            'constraint': (220, 120, 70),
            'selected': (255, 215, 0),
            'grid': (50, 50, 60)
        }
        
        # 创建地面
        self.create_ground()
        
        # 工具属性
        self.circle_radius = 20
        self.box_size = (40, 30)
        self.polygon_vertices = [(-20, -20), (20, -20), (0, 20)]
        
        # 历史记录
        self.objects = []  # 存储所有创建的对象
        self.history = []  # 操作历史
        self.max_history = 20
        
    def create_ground(self):
        """创建地面和边界"""
        # 地面(静态刚体)
        ground_body = pymunk.Body(body_type=pymunk.Body.STATIC)
        ground_shape = pymunk.Segment(ground_body, (0, 580), (800, 580), 5)
        ground_shape.friction = 1.0
        ground_shape.elasticity = 0.5
        
        # 左墙
        left_wall = pymunk.Segment(ground_body, (0, 0), (0, 600), 5)
        left_wall.friction = 0.8
        
        # 右墙
        right_wall = pymunk.Segment(ground_body, (800, 0), (800, 600), 5)
        right_wall.friction = 0.8
        
        # 天花板
        ceiling = pymunk.Segment(ground_body, (0, 0), (800, 0), 5)
        
        # 添加到空间
        self.space.add(ground_shape, left_wall, right_wall, ceiling)
        
        # 记录对象
        self.objects.extend([
            {'type': 'ground', 'shape': ground_shape},
            {'type': 'left_wall', 'shape': left_wall},
            {'type': 'right_wall', 'shape': right_wall},
            {'type': 'ceiling', 'shape': ceiling}
        ])
    
    def create_circle(self, position, radius=None):
        """创建圆形刚体"""
        if radius is None:
            radius = self.circle_radius
            
        mass = radius * 2
        moment = pymunk.moment_for_circle(mass, 0, radius)
        body = pymunk.Body(mass, moment)
        body.position = position
        
        shape = pymunk.Circle(body, radius)
        shape.friction = 0.5
        shape.elasticity = 0.8
        shape.color = self.colors['dynamic']
        
        self.space.add(body, shape)
        
        obj = {'type': 'circle', 'body': body, 'shape': shape, 'radius': radius}
        self.objects.append(obj)
        self.add_to_history('create', obj)
        
        return obj
    
    def create_box(self, position, size=None):
        """创建矩形刚体"""
        if size is None:
            size = self.box_size
            
        width, height = size
        mass = width * height * 0.1
        moment = pymunk.moment_for_box(mass, (width, height))
        body = pymunk.Body(mass, moment)
        body.position = position
        
        shape = pymunk.Poly.create_box(body, (width, height))
        shape.friction = 0.6
        shape.elasticity = 0.5
        shape.color = self.colors['dynamic']
        
        self.space.add(body, shape)
        
        obj = {'type': 'box', 'body': body, 'shape': shape, 'size': size}
        self.objects.append(obj)
        self.add_to_history('create', obj)
        
        return obj
    
    def create_polygon(self, position, vertices=None):
        """创建多边形刚体"""
        if vertices is None:
            vertices = self.polygon_vertices
            
        # 计算多边形面积和质心
        mass = 1.0
        moment = pymunk.moment_for_poly(mass, vertices)
        body = pymunk.Body(mass, moment)
        body.position = position
        
        shape = pymunk.Poly(body, vertices)
        shape.friction = 0.4
        shape.elasticity = 0.7
        shape.color = self.colors['dynamic']
        
        self.space.add(body, shape)
        
        obj = {'type': 'polygon', 'body': body, 'shape': shape, 'vertices': vertices}
        self.objects.append(obj)
        self.add_to_history('create', obj)
        
        return obj
    
    def create_constraint(self, body_a, body_b, constraint_type="pivot"):
        """创建约束"""
        if constraint_type == "pivot":
            # 枢轴关节
            anchor_a = (0, 0)
            anchor_b = (0, 0)
            constraint = pymunk.PivotJoint(body_a, body_b, anchor_a, anchor_b)
        elif constraint_type == "spring":
            # 弹簧约束
            anchor_a = (0, 0)
            anchor_b = (0, 0)
            rest_length = 100
            stiffness = 50
            damping = 1
            constraint = pymunk.DampedSpring(body_a, body_b, anchor_a, anchor_b, 
                                           rest_length, stiffness, damping)
        elif constraint_type == "gear":
            # 齿轮关节
            phase = 0
            ratio = 1
            constraint = pymunk.GearJoint(body_a, body_b, phase, ratio)
        else:
            return None
        
        constraint.color = self.colors['constraint']
        self.space.add(constraint)
        
        obj = {'type': 'constraint', 'constraint': constraint, 'constraint_type': constraint_type}
        self.objects.append(obj)
        self.add_to_history('create', obj)
        
        return obj
    
    def add_to_history(self, action, obj):
        """添加到历史记录"""
        self.history.append({
            'action': action,
            'object': obj.copy() if obj else None,
            'timestamp': time.time()
        })
        
        # 限制历史记录长度
        if len(self.history) > self.max_history:
            self.history.pop(0)
    
    def undo_last_action(self):
        """撤销上一次操作"""
        if not self.history:
            return
            
        last_action = self.history.pop()
        
        if last_action['action'] == 'create':
            obj = last_action['object']
            if 'body' in obj:
                if obj['body'] in self.space.bodies:
                    self.space.remove(obj['body'])
            if 'shape' in obj and obj['shape'] in self.space.shapes:
                self.space.remove(obj['shape'])
            if 'constraint' in obj and obj['constraint'] in self.space.constraints:
                self.space.remove(obj['constraint'])
            
            # 从对象列表中移除
            if obj in self.objects:
                self.objects.remove(obj)
    
    def get_body_at_position(self, position, radius=10):
        """获取指定位置的刚体"""
        # 使用点查询
        query_info = self.space.point_query_nearest(position, radius, pymunk.ShapeFilter())
        
        if query_info:
            shape = query_info.shape
            for obj in self.objects:
                if 'shape' in obj and obj['shape'] == shape:
                    return obj['body'] if 'body' in obj else None
        
        return None
    
    def handle_events(self):
        """处理事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
                
            elif event.type == pygame.KEYDOWN:
                self.handle_keydown(event)
                
            elif event.type == pygame.MOUSEBUTTONDOWN:
                self.handle_mousebuttondown(event)
                
            elif event.type == pygame.MOUSEBUTTONUP:
                self.handle_mousebuttonup(event)
                
            elif event.type == pygame.MOUSEMOTION:
                self.handle_mousemotion(event)
    
    def handle_keydown(self, event):
        """处理键盘按下事件"""
        if event.key == pygame.K_ESCAPE:
            self.running = False
            
        elif event.key == pygame.K_SPACE:
            self.paused = not self.paused
            
        elif event.key == pygame.K_F1:
            self.show_debug = not self.show_debug
            
        elif event.key == pygame.K_z and pygame.key.get_mods() & pygame.KMOD_CTRL:
            self.undo_last_action()
            
        elif event.key == pygame.K_1:
            self.current_tool = "circle"
            print("工具:圆形")
            
        elif event.key == pygame.K_2:
            self.current_tool = "box"
            print("工具:矩形")
            
        elif event.key == pygame.K_3:
            self.current_tool = "polygon"
            print("工具:多边形")
            
        elif event.key == pygame.K_4:
            self.current_tool = "constraint"
            print("工具:约束")
            
        elif event.key == pygame.K_c:
            # 清空所有动态物体
            self.clear_dynamic_objects()
            
        elif event.key == pygame.K_g:
            # 切换重力
            if self.space.gravity == (0, 900):
                self.space.gravity = (0, 0)  # 无重力
            elif self.space.gravity == (0, 0):
                self.space.gravity = (0, -900)  # 向上重力
            else:
                self.space.gravity = (0, 900)  # 向下重力
    
    def handle_mousebuttondown(self, event):
        """处理鼠标按下事件"""
        pos = pygame.mouse.get_pos()
        
        if event.button == 1:  # 左键
            if self.current_tool == "constraint":
                # 选择第一个物体
                self.selected_body = self.get_body_at_position(pos)
                if self.selected_body:
                    print(f"选择了物体: {self.selected_body}")
            else:
                # 开始拖拽
                self.dragging = True
                self.drag_start = pos
                
        elif event.button == 3:  # 右键
            # 删除物体
            body = self.get_body_at_position(pos)
            if body:
                self.delete_body(body)
    
    def handle_mousebuttonup(self, event):
        """处理鼠标释放事件"""
        pos = pygame.mouse.get_pos()
        
        if event.button == 1:  # 左键
            if self.dragging and self.drag_start:
                # 计算拖拽方向和距离
                dx = pos[0] - self.drag_start[0]
                dy = pos[1] - self.drag_start[1]
                distance = math.sqrt(dx*dx + dy*dy)
                
                if distance > 10:  # 最小创建距离
                    # 创建物体
                    if self.current_tool == "circle":
                        self.create_circle(self.drag_start)
                    elif self.current_tool == "box":
                        self.create_box(self.drag_start)
                    elif self.current_tool == "polygon":
                        self.create_polygon(self.drag_start)
                
                self.dragging = False
                self.drag_start = None
                
            elif self.current_tool == "constraint" and self.selected_body:
                # 选择第二个物体并创建约束
                body_b = self.get_body_at_position(pos)
                if body_b and body_b != self.selected_body:
                    self.create_constraint(self.selected_body, body_b, "pivot")
                self.selected_body = None
    
    def handle_mousemotion(self, event):
        """处理鼠标移动事件"""
        if self.dragging and self.current_tool == "constraint":
            # 约束工具下拖拽时预览
            pass
    
    def delete_body(self, body):
        """删除刚体及其相关对象"""
        # 查找并移除相关对象
        to_remove = []
        for obj in self.objects:
            if 'body' in obj and obj['body'] == body:
                to_remove.append(obj)
        
        for obj in to_remove:
            # 从物理空间移除
            if 'body' in obj and obj['body'] in self.space.bodies:
                self.space.remove(obj['body'])
            if 'shape' in obj and obj['shape'] in self.space.shapes:
                self.space.remove(obj['shape'])
            if obj in self.objects:
                self.objects.remove(obj)
                
            # 添加到历史记录
            self.add_to_history('delete', obj)
    
    def clear_dynamic_objects(self):
        """清除所有动态物体"""
        to_remove = []
        for obj in self.objects:
            if 'body' in obj and obj['body'].body_type == pymunk.Body.DYNAMIC:
                to_remove.append(obj)
        
        for obj in to_remove:
            if 'body' in obj and obj['body'] in self.space.bodies:
                self.space.remove(obj['body'])
            if 'shape' in obj and obj['shape'] in self.space.shapes:
                self.space.remove(obj['shape'])
            if obj in self.objects:
                self.objects.remove(obj)
        
        print(f"已清除 {len(to_remove)} 个动态物体")
    
    def update(self, dt):
        """更新物理世界"""
        if not self.paused:
            # 固定时间步长更新
            self.space.step(1.0 / self.fps)
        
        # 清理超出边界的物体
        self.cleanup_out_of_bounds()
    
    def cleanup_out_of_bounds(self):
        """清理超出边界的物体"""
        to_remove = []
        
        for obj in self.objects:
            if 'body' in obj and obj['body'].body_type == pymunk.Body.DYNAMIC:
                body = obj['body']
                x, y = body.position
                
                # 如果物体完全离开屏幕
                if y > 700 or y < -100 or x < -100 or x > 900:
                    to_remove.append(obj)
        
        for obj in to_remove:
            self.delete_body(obj['body'])
    
    def draw(self):
        """绘制所有物体"""
        # 清屏
        self.screen.fill(self.colors['background'])
        
        # 绘制网格
        self.draw_grid()
        
        # 绘制所有物理对象
        self.draw_objects()
        
        # 绘制约束
        self.draw_constraints()
        
        # 绘制拖拽预览
        if self.dragging and self.drag_start:
            self.draw_drag_preview()
        
        # 绘制调试信息
        if self.show_debug:
            self.draw_debug_info()
        
        # 绘制UI
        self.draw_ui()
    
    def draw_grid(self, spacing=50):
        """绘制网格"""
        width, height = self.screen.get_size()
        
        # 垂直线
        for x in range(0, width, spacing):
            pygame.draw.line(self.screen, self.colors['grid'], 
                           (x, 0), (x, height), 1)
        
        # 水平线
        for y in range(0, height, spacing):
            pygame.draw.line(self.screen, self.colors['grid'], 
                           (0, y), (width, y), 1)
    
    def draw_objects(self):
        """绘制所有物理对象"""
        for obj in self.objects:
            if 'shape' in obj:
                self.draw_shape(obj['shape'])
            
            # 高亮选中的物体
            if 'body' in obj and obj['body'] == self.selected_body:
                self.draw_highlight(obj)
    
    def draw_shape(self, shape):
        """绘制形状"""
        if isinstance(shape, pymunk.Circle):
            self.draw_circle(shape)
        elif isinstance(shape, pymunk.Poly):
            self.draw_polygon(shape)
        elif isinstance(shape, pymunk.Segment):
            self.draw_segment(shape)
    
    def draw_circle(self, shape):
        """绘制圆形"""
        body = shape.body
        radius = shape.radius
        position = body.local_to_world(shape.offset)
        
        # 计算屏幕位置
        x, y = int(position.x), int(position.y)
        r = int(radius)
        
        # 绘制填充圆
        if hasattr(shape, 'color'):
            color = shape.color
        else:
            color = self.colors['dynamic'] if body.body_type == pymunk.Body.DYNAMIC else self.colors['static']
        
        pygame.draw.circle(self.screen, color, (x, y), r)
        pygame.draw.circle(self.screen, (0, 0, 0), (x, y), r, 2)
        
        # 绘制旋转指示线
        end_x = x + r * math.cos(body.angle)
        end_y = y + r * math.sin(body.angle)
        pygame.draw.line(self.screen, (255, 255, 255), (x, y), (end_x, end_y), 2)
    
    def draw_polygon(self, shape):
        """绘制多边形"""
        body = shape.body
        vertices = shape.get_vertices()
        
        # 转换到世界坐标
        world_vertices = []
        for vertex in vertices:
            world_vertex = body.local_to_world(vertex)
            world_vertices.append((int(world_vertex.x), int(world_vertex.y)))
        
        # 绘制填充多边形
        if hasattr(shape, 'color'):
            color = shape.color
        else:
            color = self.colors['dynamic'] if body.body_type == pymunk.Body.DYNAMIC else self.colors['static']
        
        if len(world_vertices) >= 3:
            pygame.draw.polygon(self.screen, color, world_vertices)
            pygame.draw.polygon(self.screen, (0, 0, 0), world_vertices, 2)
    
    def draw_segment(self, shape):
        """绘制线段"""
        body = shape.body
        a = body.local_to_world(shape.a)
        b = body.local_to_world(shape.b)
        radius = shape.radius
        
        # 绘制线段
        color = self.colors['static']
        pygame.draw.line(self.screen, color, 
                        (int(a.x), int(a.y)), 
                        (int(b.x), int(b.y)), 
                        max(1, int(radius * 2)))
    
    def draw_constraints(self):
        """绘制约束"""
        for obj in self.objects:
            if 'constraint' in obj:
                constraint = obj['constraint']
                
                if isinstance(constraint, pymunk.PivotJoint):
                    a = constraint.a.local_to_world(constraint.anchor_a)
                    b = constraint.b.local_to_world(constraint.anchor_b)
                    
                    # 绘制连接线
                    pygame.draw.line(self.screen, self.colors['constraint'],
                                   (int(a.x), int(a.y)),
                                   (int(b.x), int(b.y)), 2)
                    
                    # 绘制锚点
                    pygame.draw.circle(self.screen, (255, 100, 100), 
                                     (int(a.x), int(a.y)), 5)
                    pygame.draw.circle(self.screen, (255, 100, 100), 
                                     (int(b.x), int(b.y)), 5)
    
    def draw_highlight(self, obj):
        """高亮选中的物体"""
        if 'body' in obj:
            body = obj['body']
            
            # 绘制边界框
            bb = obj['shape'].bb
            rect = pygame.Rect(bb.left, bb.bottom, 
                             bb.right - bb.left, bb.top - bb.bottom)
            pygame.draw.rect(self.screen, self.colors['selected'], rect, 3)
            
            # 绘制质心
            x, y = int(body.position.x), int(body.position.y)
            pygame.draw.circle(self.screen, self.colors['selected'], (x, y), 8, 2)
    
    def draw_drag_preview(self):
        """绘制拖拽预览"""
        start = self.drag_start
        end = pygame.mouse.get_pos()
        
        if self.current_tool == "circle":
            # 绘制圆形预览
            radius = int(math.hypot(end[0] - start[0], end[1] - start[1]))
            pygame.draw.circle(self.screen, (255, 255, 255, 128), start, radius, 2)
            
        elif self.current_tool == "box":
            # 绘制矩形预览
            width = end[0] - start[0]
            height = end[1] - start[1]
            rect = pygame.Rect(start[0], start[1], width, height)
            pygame.draw.rect(self.screen, (255, 255, 255, 128), rect, 2)
    
    def draw_debug_info(self):
        """绘制调试信息"""
        # 绘制物理空间统计
        stats = self.space.get_stats()
        
        debug_info = [
            f"物理沙盒 - FPS: {int(self.clock.get_fps())}",
            f"工具: {self.current_tool} (1:圆, 2:方, 3:多边, 4:约束)",
            f"物体数: {len(self.objects)}",
            f"刚体: {stats.dynamic_bodies}动态, {stats.static_bodies}静态",
            f"形状: {stats.shapes}, 约束: {stats.constraints}",
            f"重力: {self.space.gravity} (按G切换)",
            f"空格: {'继续' if self.paused else '暂停'}",
            f"Ctrl+Z: 撤销, C: 清空, F1: 调试{'' if self.show_debug else '开'}",
        ]
        
        font = pygame.font.Font(None, 24)
        y_offset = 10
        
        for info in debug_info:
            text = font.render(info, True, (255, 255, 255))
            self.screen.blit(text, (10, y_offset))
            y_offset += 25
    
    def draw_ui(self):
        """绘制用户界面"""
        # 工具选择界面
        tools = [
            ("1", "圆形", self.current_tool == "circle"),
            ("2", "矩形", self.current_tool == "box"),
            ("3", "多边形", self.current_tool == "polygon"),
            ("4", "约束", self.current_tool == "constraint"),
        ]
        
        font = pygame.font.Font(None, 28)
        y_offset = 500
        
        for key, name, active in tools:
            color = (255, 215, 0) if active else (200, 200, 200)
            text = font.render(f"{key}. {name}", True, color)
            
            # 背景框
            bg_rect = pygame.Rect(10, y_offset - 5, text.get_width() + 10, text.get_height() + 10)
            pygame.draw.rect(self.screen, (40, 40, 50, 180), bg_rect)
            pygame.draw.rect(self.screen, color, bg_rect, 2 if active else 1)
            
            self.screen.blit(text, (15, y_offset))
            y_offset += 40
    
    def run(self):
        """运行主循环"""
        print("物理沙盒启动!")
        print("控制说明:")
        print("  鼠标左键拖拽: 创建物体")
        print("  鼠标右键: 删除物体")
        print("  1-4: 切换工具")
        print("  空格: 暂停/继续")
        print("  G: 切换重力方向")
        print("  C: 清除所有动态物体")
        print("  Ctrl+Z: 撤销")
        print("  F1: 显示/隐藏调试信息")
        print("  ESC: 退出")
        
        while self.running:
            # 处理事件
            self.handle_events()
            
            # 更新物理
            self.update(1.0 / self.fps)
            
            # 绘制
            self.draw()
            
            # 更新显示
            pygame.display.flip()
            
            # 控制帧率
            self.clock.tick(self.fps)
        
        pygame.quit()
        print("物理沙盒已关闭")

# 主程序入口
if __name__ == "__main__":
    # 创建并运行物理沙盒
    sandbox = PhysicsSandbox(1000, 700)
    sandbox.run()

10. 高级应用与扩展

10.1 自定义物理行为

python 复制代码
class CustomPhysicsBehaviors:
    def __init__(self, space):
        self.space = space
        
    def create_buoyancy(self, fluid_level, density, damping, velocity):
        """创建浮力区域"""
        # 浮力回调函数
        def buoyancy_force(body, gravity, damping, dt):
            # 计算浸入水中的部分
            shape = body.shapes[0]  # 假设只有一个形状
            bb = shape.bb
            
            # 计算浸入高度
            if bb.bottom < fluid_level:
                submerged_height = min(fluid_level - bb.bottom, bb.top - bb.bottom)
                submerged_ratio = submerged_height / (bb.top - bb.bottom)
                
                # 计算浮力
                buoyancy_force = (0, -density * submerged_ratio * body.mass * 9.8)
                
                # 应用浮力
                body.apply_force_at_world_point(buoyancy_force, body.position)
                
                # 应用阻尼
                body.velocity = body.velocity * damping
            
            # 应用原始重力
            body.update_velocity(body, gravity, damping, dt)
        
        return buoyancy_force
    
    def create_magnetic_field(self, position, strength, radius):
        """创建磁场"""
        def magnetic_force(body, dt):
            # 计算到磁场的距离
            direction = pymunk.Vec2d(position) - body.position
            distance = direction.length
            
            if distance < radius and distance > 0:
                # 计算磁力(与距离成反比)
                force_magnitude = strength * (1 - distance / radius)
                force = direction.normalized() * force_magnitude
                
                # 应用磁力
                body.apply_force_at_world_point(force, body.position)
        
        return magnetic_force
    
    def create_wind_field(self, direction, strength, area_bb):
        """创建风场"""
        def wind_force(body, dt):
            # 检查物体是否在风场区域内
            shape = body.shapes[0]
            if shape.bb.left < area_bb.right and shape.bb.right > area_bb.left and \
               shape.bb.bottom < area_bb.top and shape.bb.top > area_bb.bottom:
                
                # 计算风阻(与速度差成正比)
                relative_velocity = pymunk.Vec2d(direction) * strength - body.velocity
                wind_force = relative_velocity * 0.1 * body.mass
                
                # 应用风力
                body.apply_force_at_world_point(wind_force, body.position)
        
        return wind_force

10.2 物理材质系统

python 复制代码
class PhysicsMaterialSystem:
    def __init__(self):
        self.materials = {}
        
    def define_material(self, name, density=1.0, friction=0.5, elasticity=0.3, color=None):
        """定义物理材质"""
        self.materials[name] = {
            'density': density,
            'friction': friction,
            'elasticity': elasticity,
            'color': color or (255, 255, 255)
        }
        
    def apply_material(self, shape, material_name):
        """应用材质到形状"""
        if material_name in self.materials:
            material = self.materials[material_name]
            shape.density = material['density']
            shape.friction = material['friction']
            shape.elasticity = material['elasticity']
            
            if hasattr(shape, 'color'):
                shape.color = material['color']
                
    def create_predefined_materials(self):
        """创建预定义材质"""
        # 常见材料
        self.define_material("wood", density=0.5, friction=0.4, elasticity=0.2, color=(139, 69, 19))
        self.define_material("metal", density=7.8, friction=0.3, elasticity=0.1, color=(192, 192, 192))
        self.define_material("rubber", density=1.2, friction=0.8, elasticity=0.9, color=(0, 0, 0))
        self.define_material("ice", density=0.9, friction=0.1, elasticity=0.1, color=(173, 216, 230))
        self.define_material("glass", density=2.5, friction=0.2, elasticity=0.05, color=(211, 211, 211, 128))
        self.define_material("bouncy_ball", density=0.5, friction=0.7, elasticity=0.95, color=(255, 0, 0))
        
    def get_material_properties(self, material_name):
        """获取材质属性"""
        return self.materials.get(material_name, {})

11. 常见问题与解决方案

11.1 性能问题

问题1:模拟速度慢

python 复制代码
# 解决方案:优化空间设置
def optimize_performance(space):
    # 1. 调整迭代次数
    space.iterations = 10  # 降低精度提高速度
    
    # 2. 启用空间哈希
    space.use_spatial_hash(cell_size=50)
    
    # 3. 启用休眠
    space.sleep_time_threshold = 0.5
    
    # 4. 降低时间步长
    space.step(1/120)  # 120Hz更新
    
    # 5. 批量操作
    space.add(*objects)  # 批量添加
    space.remove(*objects)  # 批量移除

问题2:物体穿透

python 复制代码
# 解决方案:启用CCD和调整参数
def prevent_tunneling(body, shape):
    # 启用连续碰撞检测
    shape.collision_type = 1
    shape.ccd_velocity_threshold = 500
    
    # 增加迭代次数
    space.iterations = 20
    
    # 减小时间步长
    dt = 1/240  # 240Hz
    
    # 增加形状margin
    shape.margin = 0.1

11.2 物理稳定性

问题3:数值不稳定

python 复制代码
def improve_numerical_stability(space):
    # 1. 限制最大速度
    for body in space.bodies:
        if body.body_type == pymunk.Body.DYNAMIC:
            body.velocity_limit = 1000
            body.angular_velocity_limit = 20
    
    # 2. 增加阻尼
    space.damping = 0.99
    
    # 3. 限制穿透深度
    space.collision_slop = 0.1
    space.collision_bias = 0.2
    
    # 4. 使用子步进
    def update_with_substeps(dt, substeps=4):
        substep_dt = dt / substeps
        for _ in range(substeps):
            space.step(substep_dt)

11.3 碰撞检测问题

问题4:碰撞回调不触发

python 复制代码
def fix_collision_callbacks():
    # 1. 检查碰撞类型
    shape_a.collision_type = 1
    shape_b.collision_type = 2
    
    # 2. 注册碰撞处理器
    handler = space.add_collision_handler(1, 2)
    handler.begin = collision_begin_callback
    
    # 3. 检查过滤器
    shape_a.filter = pymunk.ShapeFilter(categories=0x1, mask=0x2)
    shape_b.filter = pymunk.ShapeFilter(categories=0x2, mask=0x1)
    
    # 4. 确保形状不是传感器
    shape_a.sensor = False
    shape_b.sensor = False

12. 核心知识归纳

12.1 知识体系总结

本教程系统性地讲解了Pymunk 2D物理引擎的完整知识体系,核心可归纳为以下五个层次:

第一层:核心概念与工作流

  • 空间(Space):所有物理模拟发生的容器,负责统筹刚体、形状、约束和力。

  • 刚体(Body) :物体的物理抽象,承载质量、力矩、位置、速度等物理属性,分为动态静态运动学三类。

  • 形状(Shape):附着在刚体上的几何轮廓(圆、多边形、线段),用于精确的碰撞检测。

  • 约束(Constraint):连接刚体、限制其相对运动的关节(如枢轴、弹簧、齿轮)。

第二层:物理交互机制

  • 碰撞处理流水线 :分为begin(开始)、pre_solve(预求解)、post_solve(后求解)、separate(分离)四个阶段,允许开发者精细控制碰撞行为。

  • 碰撞过滤 :通过ShapeFilter(类别、掩码、组)灵活控制哪些形状之间可以发生碰撞,是构建复杂交互(如触发器、子弹穿透)的基石。

  • 查询 :提供了segment_query(射线检测)、point_query(点查询)、bb_query(范围查询)等方法,用于游戏逻辑(如视线判断、拾取物品)。

第三层:性能与稳定性

  • 性能优化 :关键策略包括使用空间哈希 加速碰撞检测、启用刚体休眠 、调整迭代次数、以及采用对象池管理高频创建销毁的物体。

  • 数值稳定性:通过设置速度限制、增加阻尼、合理设置碰撞斜度(slop)和偏差(bias)来防止模拟"爆炸"和穿透。

  • 连续碰撞检测(CCD):为高速运动的物体启用,防止"隧道效应"。

第四层:架构与扩展

  • 组件化设计 :教程中演示的PhysicsManagerConstraintSystemPhysicsMaterialSystem等类,展示了如何将Pymunk功能模块化,构建可维护的大型项目。

  • 自定义行为 :通过重写velocity_func和应用持续力,可以轻松实现浮力、磁力、风场等复杂效果。

第五层:实践与调试

  • 物理沙盒:动手构建一个沙盒是理解和测试Pymunk所有功能的最佳方式。

  • 调试可视化:实时绘制碰撞法线、速度向量、边界框等信息,是开发和调整物理参数的必备工具。

12.2 核心资源导航

  • 官方文档与源码:遇到问题时,Pymunk官方文档和 Chipmunk2D文档是终极参考。源码位于 GitHub。

  • 扩展学习

    • 游戏框架集成:探索如何将Pymunk与Pygame、Pyglet、Pyxel等渲染框架深度结合。

    • 高级主题 :研究连续碰撞检测(CCD) ​ 的详细原理、软体模拟 的近似实现方法,以及如何与图形骨骼动画驱动相结合。

  • 社区与项目

    • Stack OverflowReddit的r/pygame板块相关Discord频道提问和分享。

    • 阅读开源游戏(如许多使用Pygame+Pymunk的游戏)的源码是绝佳的学习方式。

最后结语

Pymunk的强大在于其在易用性与灵活性之间取得了精妙的平衡。它既能让初学者快速看到物理效果,获得成就感,又能为资深开发者提供足够的底层控制,以构建独特而稳定的物理交互。希望这篇指南能成为您探索2D物理模拟世界的可靠地图。

相关推荐
sensen_kiss4 小时前
INT303 Coursework1 爬取影视网站数据(如何爬虫网站数据)
爬虫·python·学习
玄同7655 小时前
我的 Trae Skill 实践|使用 UV 工具一键搭建 Python 项目开发环境
开发语言·人工智能·python·langchain·uv·trae·vibe coding
Yorlen_Zhang5 小时前
Python Tkinter Text 控件完全指南:从基础编辑器到富文本应用
开发语言·python·c#
HAPPY酷5 小时前
C++ 和 Python 的“容器”对决:从万金油到核武器
开发语言·c++·python
gpfyyds6666 小时前
Python代码练习
开发语言·python
aiguangyuan7 小时前
使用LSTM进行情感分类:原理与实现剖析
人工智能·python·nlp
小小张说故事7 小时前
BeautifulSoup:Python网页解析的优雅利器
后端·爬虫·python
luoluoal7 小时前
基于python的医疗领域用户问答的意图识别算法研究(源码+文档)
python
Shi_haoliu7 小时前
python安装操作流程-FastAPI + PostgreSQL简单流程
python·postgresql·fastapi