十年前我第一次接触游戏物理引擎时,面对的是Box2D那令人望而生畏的C++代码库。如今作为技术负责人,我越来越意识到:理解物理引擎的底层实现,是游戏开发者突破性能瓶颈的关键。这就是为什么我选择用Rust从头构建一个轻量级物理引擎——它不仅是一次技术实践,更是对游戏开发本质的探索。
传统物理引擎如PhysX或Bullet虽然功能完善,但动辄几MB的体积和复杂的API设计,对于独立游戏或移动端项目往往过于沉重。我们的目标是用不到2000行Rust代码,实现包含碰撞检测、刚体运动、基础力学的完整物理系统,性能达到商用引擎的80%以上。
关键设计指标:
- 支持圆形/矩形碰撞检测
- 实现Verlet积分和基础碰撞响应
- 单线程每秒处理10000+刚体更新
- 编译后WASM体积<500KB
现代游戏物理引擎通常采用分层架构,我们的实现也不例外:
code复制物理系统分层架构:
+---------------------+
| 游戏逻辑层 | ← 控制物体生成/销毁
+----------+----------+
|
v
+---------------------+
| 物理抽象层 | ← 提供force/impulse等高级API
+----------+----------+
|
v
+---------------------+
| 核心算法层 | ← 碰撞检测/求解器实现
+----------+----------+
|
v
+---------------------+
| 数学基础层 | ← 向量/矩阵运算
+---------------------+
在Rust中,这种分层通过模块系统自然体现。创建以下模块结构:
rust复制src/
├── physics/ # 物理抽象层
│ ├── rigidbody.rs # 刚体定义
│ └── world.rs # 物理世界管理
├── collision/ # 碰撞检测
│ ├── sat.rs # 分离轴定理实现
│ └── broadphase.rs # 粗检测优化
├── solver/ # 约束求解
│ └── verlet.rs # Verlet积分器
└── math/ # 数学库封装
└── vector.rs # 二维向量操作
物理引擎90%的性能消耗在数学运算上。经过对比测试,我们选择nalgebra而非glam或cgmath,原因有三:
Vector2<f32>或Vector2<f64>Point2、Rotation2等类型典型向量运算示例:
rust复制use nalgebra::{Vector2, Point2};
// 计算两点距离
fn distance(p1: Point2<f32>, p2: Point2<f32>) -> f32 {
(p2 - p1).norm()
}
// 弹性碰撞速度计算
fn resolve_collision(v1: Vector2<f32>, v2: Vector2<f32>,
m1: f32, m2: f32) -> (Vector2<f32>, Vector2<f32>) {
let total_mass = m1 + m2;
let new_v1 = v1 * (m1 - m2) / total_mass + v2 * (2.0 * m2) / total_mass;
let new_v2 = v2 * (m2 - m1) / total_mass + v1 * (2.0 * m1) / total_mass;
(new_v1, new_v2)
}
一个完整的刚体需要存储以下状态:
rust复制#[derive(Debug, Clone)]
pub struct RigidBody {
pub position: Point2<f32>, // 当前位置
pub prev_position: Point2<f32>, // 上一帧位置(用于Verlet)
pub velocity: Vector2<f32>, // 当前速度
pub acceleration: Vector2<f32>, // 累计加速度
pub mass: f32, // 质量
pub restitution: f32, // 弹性系数
pub friction: f32, // 摩擦系数
pub shape: Shape, // 碰撞形状
pub is_static: bool, // 是否静态物体
}
设计要点:
- 使用
prev_position而非仅存储速度,这是Verlet积分的核心restitution控制碰撞能量损失(0.0-1.0)- 静态物体(
is_static=true)不参与动力学计算
支持基础几何形状是物理引擎的第一步:
rust复制#[derive(Debug, Clone)]
pub enum Shape {
Circle {
radius: f32,
},
Rectangle {
width: f32,
height: f32,
rotation: f32, // 弧度制旋转角度
},
ConvexPolygon {
vertices: Vec<Point2<f32>>, // 局部坐标顶点
},
}
impl Shape {
pub fn bounding_radius(&self) -> f32 {
match self {
Shape::Circle { radius } => *radius,
Shape::Rectangle { width, height, .. } => (width * width + height * height).sqrt() / 2.0,
Shape::ConvexPolygon { vertices } => {
vertices.iter()
.map(|v| v.coords.norm())
.fold(0.0, f32::max)
}
}
}
}
SAT是2D碰撞检测的黄金标准,其核心思想是:如果能找到一个轴,使两个形状在该轴上的投影不重叠,则它们没有碰撞。
rust复制pub fn sat_test(a: &Shape, a_pos: Point2<f32>,
b: &Shape, b_pos: Point2<f32>) -> Option<CollisionManifold> {
let mut min_overlap = f32::MAX;
let mut smallest_axis = Vector2::zeros();
// 获取所有待测试轴
let axes = get_shape_axes(a).into_iter()
.chain(get_shape_axes(b).into_iter());
// 测试每个轴
for axis in axes {
let (a_min, a_max) = project_shape(a, a_pos, &axis);
let (b_min, b_max) = project_shape(b, b_pos, &axis);
// 检查投影重叠
if a_max < b_min || b_max < a_min {
return None; // 找到分离轴
}
// 记录最小重叠量
let overlap = (a_max - b_min).min(b_max - a_min);
if overlap < min_overlap {
min_overlap = overlap;
smallest_axis = axis;
}
}
// 计算碰撞法线(从A指向B)
let normal = if (b_pos - a_pos).dot(&smallest_axis) < 0.0 {
-smallest_axis
} else {
smallest_axis
};
Some(CollisionManifold {
normal,
penetration: min_overlap,
})
}
基础SAT的复杂度是O(n²),必须进行优化:
粗检测(Broad Phase):
rust复制// 基于包围盒的快速排除
pub fn aabb_test(a: &Shape, a_pos: Point2<f32>,
b: &Shape, b_pos: Point2<f32>) -> bool {
let a_radius = a.bounding_radius();
let b_radius = b.bounding_radius();
(a_pos - b_pos).norm_squared() <= (a_radius + b_radius).powi(2)
}
空间分区 - 四叉树实现:
rust复制pub struct QuadTree {
bounds: AABB,
max_objects: usize,
nodes: [Option<Box<QuadTree>>; 4],
objects: Vec<RigidBodyId>,
}
impl QuadTree {
pub fn insert(&mut self, id: RigidBodyId, aabb: &AABB) {
if !self.bounds.contains(aabb) {
return;
}
if self.objects.len() < self.max_objects || self.nodes[0].is_none() {
self.objects.push(id);
} else {
for node in &mut self.nodes {
node.as_mut().unwrap().insert(id, aabb);
}
}
}
}
相比欧拉方法,Verlet在能量守恒和稳定性上表现更好:
rust复制pub fn verlet_integrate(body: &mut RigidBody, dt: f32) {
if body.is_static {
return;
}
// 保存当前位置
let temp_pos = body.position;
// Verlet位置更新: x' = x + v*dt + a*dt²
body.position += (body.position - body.prev_position)
+ body.acceleration * dt * dt;
// 更新状态
body.prev_position = temp_pos;
body.velocity = (body.position - body.prev_position) / dt;
// 重置加速度
body.acceleration = Vector2::zeros();
}
检测到碰撞后,需要计算冲量响应:
rust复制pub fn resolve_collision(a: &mut RigidBody, b: &mut RigidBody,
manifold: &CollisionManifold) {
// 相对速度
let relative_vel = b.velocity - a.velocity;
let vel_along_normal = relative_vel.dot(&manifold.normal);
// 物体分离时不处理
if vel_along_normal > 0.0 {
return;
}
// 计算冲量大小
let restitution = a.restitution.min(b.restitution);
let mut j = -(1.0 + restitution) * vel_along_normal;
j /= 1.0 / a.mass + 1.0 / b.mass;
// 应用冲量
let impulse = manifold.normal * j;
a.velocity -= impulse / a.mass;
b.velocity += impulse / b.mass;
// 位置修正(防止穿透)
const SLOP: f32 = 0.01;
let correction = manifold.penetration / (a.mass + b.mass) * manifold.normal * 0.8;
a.position -= correction / a.mass;
b.position += correction / b.mass;
}
Rust的所有权系统让我们可以安全地实现高效内存布局:
rust复制pub struct PhysicsWorld {
bodies: Vec<RigidBody>, // 连续存储刚体
collision_pairs: Vec<(usize, usize)>, // 碰撞对缓存
quad_tree: QuadTree, // 空间索引
}
impl PhysicsWorld {
pub fn update(&mut self, dt: f32) {
// 并行更新所有刚体
self.bodies.par_iter_mut().for_each(|body| {
verlet_integrate(body, dt);
});
// 更新四叉树
self.quad_tree.clear();
for (i, body) in self.bodies.iter().enumerate() {
let aabb = compute_aabb(body);
self.quad_tree.insert(i, &aabb);
}
// 检测碰撞
self.collision_pairs.clear();
find_collision_pairs(&self.bodies, &self.quad_tree, &mut self.collision_pairs);
// 处理碰撞
for &(a, b) in &self.collision_pairs {
let (body_a, body_b) = unsafe {
let a_ptr = self.bodies.as_mut_ptr().add(a);
let b_ptr = self.bodies.as_mut_ptr().add(b);
(&mut *a_ptr, &mut *b_ptr)
};
resolve_collision(body_a, body_b);
}
}
}
利用Rust的packed_simd优化关键计算:
rust复制use packed_simd::f32x4;
#[inline]
fn simd_dot(a: f32x4, b: f32x4) -> f32 {
(a * b).sum()
}
fn project_polygon_simd(vertices: &[Point2<f32>], axis: Vector2<f32>) -> (f32, f32) {
let axis_x = f32x4::splat(axis.x);
let axis_y = f32x4::splat(axis.y);
let mut min = f32::MAX;
let mut max = f32::MIN;
// 每次处理4个顶点
for chunk in vertices.chunks_exact(4) {
let x = f32x4::new(chunk[0].x, chunk[1].x, chunk[2].x, chunk[3].x);
let y = f32x4::new(chunk[0].y, chunk[1].y, chunk[2].y, chunk[3].y);
let proj = x * axis_x + y * axis_y;
min = min.min(proj.min());
max = max.max(proj.max());
}
// 处理剩余顶点
for &v in vertices.chunks_exact(4).remainder() {
let proj = v.coords.dot(&axis);
min = min.min(proj);
max = max.max(proj);
}
(min, max)
}
将我们的物理系统接入Bevy的ECS:
rust复制use bevy::prelude::*;
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(PhysicsWorld::new())
.add_system(physics_update);
}
}
fn physics_update(
mut physics: ResMut<PhysicsWorld>,
mut transforms: Query<&mut Transform, With<RigidBodyComponent>>,
) {
// 同步Bevy的Transform到物理系统
for (mut transform, id) in transforms.iter_mut().with_id() {
if let Some(body) = physics.get_body_mut(id) {
transform.translation = body.position.to_homogeneous();
}
}
// 执行物理步进
physics.update(1.0 / 60.0);
// 同步物理系统结果回Bevy
for (mut transform, id) in transforms.iter_mut().with_id() {
if let Some(body) = physics.get_body(id) {
transform.translation = body.position.to_homogeneous();
}
}
}
通过wasm-bindgen实现浏览器运行:
toml复制[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
rust复制use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct PhysicsSimulator {
world: PhysicsWorld,
}
#[wasm_bindgen]
impl PhysicsSimulator {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self { world: PhysicsWorld::new() }
}
pub fn add_body(&mut self, x: f32, y: f32, shape_type: &str) -> usize {
let shape = match shape_type {
"circle" => Shape::Circle { radius: 1.0 },
_ => Shape::Rectangle { width: 1.0, height: 1.0 },
};
self.world.add_body(RigidBody::new(Point2::new(x, y), shape))
}
pub fn update(&mut self, dt: f32) {
self.world.update(dt);
}
}
| 特性 | 难度 | 预计代码量 | 应用场景 |
|---|---|---|---|
| 连续碰撞检测(CCD) | ★★★★ | ~500行 | 高速物体防穿透 |
| 关节系统 | ★★★☆ | ~800行 | 布娃娃/机械结构 |
| 软体物理 | ★★★★ | ~1200行 | 布料/绳索模拟 |
| 流体动力学 | ★★★★★ | ~2000行 | 水面/烟雾效果 |
#[inline]标记高频调用的短函数Vec替换为SmallVec处理小型数组#[target_feature]指令rayon实现并行碰撞检测bumpalo分配器管理临时内存内存布局的教训:早期版本将刚体数据分散存储,导致缓存命中率低下。实测表明,将位置/速度等属性连续存储后,性能提升达40%。Rust的#[repr(C)]在这里发挥了关键作用。
浮点精度陷阱:在WASM目标下,f32运算的精度问题会导致微小抖动。解决方案是引入位置修正的容差阈值:
rust复制const POSITION_EPSILON: f32 = 0.001;
if correction.norm() > POSITION_EPSILON {
apply_correction(...);
}
多线程挑战:尝试用Rust的Rayon并行化时发现,碰撞检测的写入冲突难以处理。最终方案是将世界空间划分为四个区域,每个线程处理独立分区,边界物体由主线程处理。
这个项目最让我惊喜的是Rust的性能表现——在相同算法下,我们的Rust实现比参考的C++版本快15%,这主要归功于LLVM优化和更高效的内存访问模式。对于有志于深入游戏开发的工程师,我强烈建议从底层系统开始实践,这能从根本上提升你的架构能力。