摘要
本文全面、系统地介绍Pymunk物理引擎的核心概念、架构设计、实战应用和性能优化策略。Pymunk作为Chipmunk物理引擎的Python绑定,是2D游戏开发、物理模拟、教育演示等领域的强大工具。文章将从基础理论出发,结合大量实战代码,深入探讨Pymunk的各个功能模块,为开发者提供全面的学习指南和最佳实践。
目录
-
Pymunk物理引擎概述
-
核心概念深度解析
-
物理空间与坐标系系统
-
刚体系统:从静态到动态
-
碰撞形状与几何体
-
约束与关节系统
-
碰撞检测与响应机制
-
性能优化与调试技巧
-
实战案例:物理沙盒实现
-
高级应用与扩展
-
常见问题解决方案
-
学习资源与社区
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 应用场景
-
游戏开发:2D平台游戏、物理谜题、弹球游戏
-
物理仿真:机械系统、粒子系统、结构分析
-
教育工具:物理教学演示、交互式实验
-
数据可视化:物理过程的可视化展示
-
原型开发:快速验证物理概念
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):为高速运动的物体启用,防止"隧道效应"。
第四层:架构与扩展
-
组件化设计 :教程中演示的
PhysicsManager、ConstraintSystem、PhysicsMaterialSystem等类,展示了如何将Pymunk功能模块化,构建可维护的大型项目。 -
自定义行为 :通过重写
velocity_func和应用持续力,可以轻松实现浮力、磁力、风场等复杂效果。
第五层:实践与调试
-
物理沙盒:动手构建一个沙盒是理解和测试Pymunk所有功能的最佳方式。
-
调试可视化:实时绘制碰撞法线、速度向量、边界框等信息,是开发和调整物理参数的必备工具。
12.2 核心资源导航
-
官方文档与源码:遇到问题时,Pymunk官方文档和 Chipmunk2D文档是终极参考。源码位于 GitHub。
-
扩展学习:
-
游戏框架集成:探索如何将Pymunk与Pygame、Pyglet、Pyxel等渲染框架深度结合。
-
高级主题 :研究连续碰撞检测(CCD) 的详细原理、软体模拟 的近似实现方法,以及如何与图形骨骼动画驱动相结合。
-
-
社区与项目:
-
在Stack Overflow 、Reddit的r/pygame板块 或相关Discord频道提问和分享。
-
阅读开源游戏(如许多使用Pygame+Pymunk的游戏)的源码是绝佳的学习方式。
-
最后结语:
Pymunk的强大在于其在易用性与灵活性之间取得了精妙的平衡。它既能让初学者快速看到物理效果,获得成就感,又能为资深开发者提供足够的底层控制,以构建独特而稳定的物理交互。希望这篇指南能成为您探索2D物理模拟世界的可靠地图。