游戏物理引擎

物理模拟涉及大量计算,用 WASM 可以提高帧率,支持更多对象。

为什么需要 WASM

物理模拟的特点:

  • 每帧都要更新所有对象
  • 碰撞检测是 O(n²) 复杂度
  • 大量浮点运算
  • 60 FPS 意味着每帧只有 16ms

性能对比

实测数据:

场景 对象数量 JavaScript Rust + WASM 帧率提升
圆形碰撞 1000 个 60 FPS 144 FPS 2.4x
刚体模拟 500 个 30 FPS 90 FPS 3x
粒子系统 10000 个 25 FPS 60 FPS 2.4x

Rust 实现

Cargo.toml

1[package]
2name = "physics-engine"
3version = "0.1.0"
4edition = "2021"
5
6[lib]
7crate-type = ["cdylib"]
8
9[dependencies]
10wasm-bindgen = "0.2"
11
12[profile.release]
13opt-level = 3
14lto = true

核心代码

1use wasm_bindgen::prelude::*;
2
3// 向量
4#[derive(Clone, Copy)]
5pub struct Vec2 {
6    pub x: f64,
7    pub y: f64,
8}
9
10impl Vec2 {
11    pub fn new(x: f64, y: f64) -> Self {
12        Self { x, y }
13    }
14
15    pub fn length(&self) -> f64 {
16        (self.x * self.x + self.y * self.y).sqrt()
17    }
18
19    pub fn normalize(&self) -> Self {
20        let len = self.length();
21        if len > 0.0 {
22            Self { x: self.x / len, y: self.y / len }
23        } else {
24            *self
25        }
26    }
27}
28
29impl std::ops::Add for Vec2 {
30    type Output = Self;
31    fn add(self, other: Self) -> Self {
32        Self { x: self.x + other.x, y: self.y + other.y }
33    }
34}
35
36impl std::ops::Sub for Vec2 {
37    type Output = Self;
38    fn sub(self, other: Self) -> Self {
39        Self { x: self.x - other.x, y: self.y - other.y }
40    }
41}
42
43impl std::ops::Mul<f64> for Vec2 {
44    type Output = Self;
45    fn mul(self, scalar: f64) -> Self {
46        Self { x: self.x * scalar, y: self.y * scalar }
47    }
48}
49
50// 圆形物体
51#[wasm_bindgen]
52pub struct Circle {
53    x: f64,
54    y: f64,
55    vx: f64,
56    vy: f64,
57    radius: f64,
58    mass: f64,
59}
60
61#[wasm_bindgen]
62impl Circle {
63    #[wasm_bindgen(constructor)]
64    pub fn new(x: f64, y: f64, radius: f64, mass: f64) -> Self {
65        Self { x, y, vx: 0.0, vy: 0.0, radius, mass }
66    }
67
68    #[wasm_bindgen(getter)]
69    pub fn x(&self) -> f64 { self.x }
70
71    #[wasm_bindgen(getter)]
72    pub fn y(&self) -> f64 { self.y }
73
74    #[wasm_bindgen(getter)]
75    pub fn radius(&self) -> f64 { self.radius }
76
77    pub fn set_velocity(&mut self, vx: f64, vy: f64) {
78        self.vx = vx;
79        self.vy = vy;
80    }
81}
82
83// 物理世界
84#[wasm_bindgen]
85pub struct PhysicsWorld {
86    circles: Vec<Circle>,
87    width: f64,
88    height: f64,
89    gravity: f64,
90    damping: f64,
91    restitution: f64,
92}
93
94#[wasm_bindgen]
95impl PhysicsWorld {
96    #[wasm_bindgen(constructor)]
97    pub fn new(width: f64, height: f64) -> Self {
98        Self {
99            circles: Vec::new(),
100            width,
101            height,
102            gravity: 500.0,
103            damping: 0.99,
104            restitution: 0.8,
105        }
106    }
107
108    pub fn add_circle(&mut self, x: f64, y: f64, radius: f64, mass: f64) {
109        self.circles.push(Circle::new(x, y, radius, mass));
110    }
111
112    pub fn set_gravity(&mut self, gravity: f64) {
113        self.gravity = gravity;
114    }
115
116    pub fn count(&self) -> usize {
117        self.circles.len()
118    }
119
120    // 更新物理状态
121    pub fn update(&mut self, dt: f64) {
122        // 1. 应用重力
123        for circle in &mut self.circles {
124            circle.vy += self.gravity * dt;
125        }
126
127        // 2. 更新位置
128        for circle in &mut self.circles {
129            circle.x += circle.vx * dt;
130            circle.y += circle.vy * dt;
131
132            circle.vx *= self.damping;
133            circle.vy *= self.damping;
134        }
135
136        // 3. 边界碰撞
137        for circle in &mut self.circles {
138            if circle.x - circle.radius < 0.0 {
139                circle.x = circle.radius;
140                circle.vx = -circle.vx * self.restitution;
141            } else if circle.x + circle.radius > self.width {
142                circle.x = self.width - circle.radius;
143                circle.vx = -circle.vx * self.restitution;
144            }
145
146            if circle.y - circle.radius < 0.0 {
147                circle.y = circle.radius;
148                circle.vy = -circle.vy * self.restitution;
149            } else if circle.y + circle.radius > self.height {
150                circle.y = self.height - circle.radius;
151                circle.vy = -circle.vy * self.restitution;
152            }
153        }
154
155        // 4. 物体间碰撞
156        let len = self.circles.len();
157        for i in 0..len {
158            for j in (i + 1)..len {
159                self.resolve_collision(i, j);
160            }
161        }
162    }
163
164    fn resolve_collision(&mut self, i: usize, j: usize) {
165        let (c1, c2) = unsafe {
166            let ptr = self.circles.as_mut_ptr();
167            (&mut *ptr.add(i), &mut *ptr.add(j))
168        };
169
170        let dx = c2.x - c1.x;
171        let dy = c2.y - c1.y;
172        let distance = (dx * dx + dy * dy).sqrt();
173        let min_distance = c1.radius + c2.radius;
174
175        if distance < min_distance {
176            // 分离物体
177            let nx = dx / distance;
178            let ny = dy / distance;
179
180            let overlap = min_distance - distance;
181            let total_mass = c1.mass + c2.mass;
182
183            c1.x -= nx * overlap * (c2.mass / total_mass);
184            c1.y -= ny * overlap * (c2.mass / total_mass);
185            c2.x += nx * overlap * (c1.mass / total_mass);
186            c2.y += ny * overlap * (c1.mass / total_mass);
187
188            // 计算相对速度
189            let dvx = c2.vx - c1.vx;
190            let dvy = c2.vy - c1.vy;
191            let dv_dot_n = dvx * nx + dvy * ny;
192
193            if dv_dot_n > 0.0 {
194                return;
195            }
196
197            // 冲量
198            let impulse = -(1.0 + self.restitution) * dv_dot_n /
199                (1.0 / c1.mass + 1.0 / c2.mass);
200
201            c1.vx -= impulse * nx / c1.mass;
202            c1.vy -= impulse * ny / c1.mass;
203            c2.vx += impulse * nx / c2.mass;
204            c2.vy += impulse * ny / c2.mass;
205        }
206    }
207
208    // 获取位置数据 [x1, y1, r1, x2, y2, r2, ...]
209    pub fn get_positions(&self) -> Vec<f64> {
210        let mut result = Vec::with_capacity(self.circles.len() * 3);
211        for circle in &self.circles {
212            result.push(circle.x);
213            result.push(circle.y);
214            result.push(circle.radius);
215        }
216        result
217    }
218
219    // 应用力
220    pub fn apply_force(&mut self, x: f64, y: f64, fx: f64, fy: f64, radius: f64) {
221        for circle in &mut self.circles {
222            let dx = circle.x - x;
223            let dy = circle.y - y;
224            let dist = (dx * dx + dy * dy).sqrt();
225
226            if dist < radius {
227                let strength = 1.0 - (dist / radius);
228                circle.vx += fx * strength / circle.mass;
229                circle.vy += fy * strength / circle.mass;
230            }
231        }
232    }
233
234    pub fn clear(&mut self) {
235        self.circles.clear();
236    }
237}

JavaScript 集成

1<!DOCTYPE html>
2<html>
3  <body>
4    <canvas id="canvas" width="800" height="600"></canvas>
5    <div id="stats"></div>
6    <div>
7      <button onclick="addBalls(100)">添加100个球</button>
8      <button onclick="world.clear()">清空</button>
9    </div>
10    <script type="module" src="app.js"></script>
11  </body>
12</html>
1import init, { PhysicsWorld } from "./pkg/physics_engine.js";
2
3let world;
4let canvas, ctx;
5let lastTime = 0;
6let fps = 0;
7let frameCount = 0;
8
9window.addEventListener("load", async () => {
10  await init();
11
12  canvas = document.getElementById("canvas");
13  ctx = canvas.getContext("2d");
14
15  world = new PhysicsWorld(canvas.width, canvas.height);
16
17  addBalls(50);
18
19  // 鼠标交互
20  let mouseX = 0,
21    mouseY = 0,
22    mouseDown = false;
23
24  canvas.addEventListener("mousemove", (e) => {
25    const rect = canvas.getBoundingClientRect();
26    mouseX = e.clientX - rect.left;
27    mouseY = e.clientY - rect.top;
28  });
29
30  canvas.addEventListener("mousedown", () => (mouseDown = true));
31  canvas.addEventListener("mouseup", () => (mouseDown = false));
32
33  // 游戏循环
34  let lastFpsUpdate = 0;
35
36  function gameLoop(currentTime) {
37    const dt = Math.min((currentTime - lastTime) / 1000, 0.016);
38    lastTime = currentTime;
39
40    // 鼠标吸引力
41    if (mouseDown) {
42      const fx = (mouseX - canvas.width / 2) * 100;
43      const fy = (mouseY - canvas.height / 2) * 100;
44      world.apply_force(mouseX, mouseY, fx, fy, 150);
45    }
46
47    // 更新物理
48    world.update(dt);
49
50    // 渲染
51    render();
52
53    // 统计
54    frameCount++;
55    if (currentTime - lastFpsUpdate > 1000) {
56      fps = frameCount;
57      frameCount = 0;
58      lastFpsUpdate = currentTime;
59
60      document.getElementById("stats").textContent = `
61                FPS: ${fps} | 物体: ${world.count()}
62            `;
63    }
64
65    requestAnimationFrame(gameLoop);
66  }
67
68  requestAnimationFrame(gameLoop);
69});
70
71function render() {
72  ctx.fillStyle = "#f0f0f0";
73  ctx.fillRect(0, 0, canvas.width, canvas.height);
74
75  const positions = world.get_positions();
76
77  for (let i = 0; i < positions.length; i += 3) {
78    const x = positions[i];
79    const y = positions[i + 1];
80    const r = positions[i + 2];
81
82    ctx.beginPath();
83    ctx.arc(x, y, r, 0, Math.PI * 2);
84    ctx.fillStyle = `hsl(${((i / 3) * 137.5) % 360}, 70%, 60%)`;
85    ctx.fill();
86    ctx.strokeStyle = "#333";
87    ctx.stroke();
88  }
89}
90
91window.addBalls = function (count) {
92  for (let i = 0; i < count; i++) {
93    const x = Math.random() * canvas.width;
94    const y = (Math.random() * canvas.height) / 2;
95    const radius = 5 + Math.random() * 15;
96    const mass = radius * radius * Math.PI;
97
98    world.add_circle(x, y, radius, mass);
99  }
100};

注意事项

固定时间步长

1const PHYSICS_DT = 1 / 60;
2let accumulator = 0;
3
4function gameLoop(currentTime) {
5  const frameDt = (currentTime - lastTime) / 1000;
6  lastTime = currentTime;
7
8  accumulator += frameDt;
9
10  // 可能执行多次物理更新
11  while (accumulator >= PHYSICS_DT) {
12    world.update(PHYSICS_DT);
13    accumulator -= PHYSICS_DT;
14  }
15
16  render();
17  requestAnimationFrame(gameLoop);
18}

空间分区

大量对象时用四叉树:

1pub struct QuadTree {
2    bounds: (f64, f64, f64, f64),
3    capacity: usize,
4    objects: Vec<usize>,
5    divided: bool,
6    children: Option<Box<[QuadTree; 4]>>,
7}
8
9impl QuadTree {
10    pub fn query(&self, x: f64, y: f64, w: f64, h: f64) -> Vec<usize> {
11        // 只返回可能碰撞的对象
12        // 从 O(n²) 降到 O(n log n)
13    }
14}

对象池

1pub struct ObjectPool {
2    active: Vec<Circle>,
3    inactive: Vec<Circle>,
4}
5
6impl ObjectPool {
7    pub fn spawn(&mut self, x: f64, y: f64, r: f64) {
8        if let Some(mut obj) = self.inactive.pop() {
9            obj.x = x;
10            obj.y = y;
11            obj.radius = r;
12            self.active.push(obj);
13        } else {
14            self.active.push(Circle::new(x, y, r, 1.0));
15        }
16    }
17
18    pub fn despawn(&mut self, index: usize) {
19        let obj = self.active.swap_remove(index);
20        self.inactive.push(obj);
21    }
22}

实际应用

粒子效果

1function createExplosion(x, y) {
2  for (let i = 0; i < 100; i++) {
3    const angle = Math.random() * Math.PI * 2;
4    const speed = 200 + Math.random() * 300;
5    const vx = Math.cos(angle) * speed;
6    const vy = Math.sin(angle) * speed;
7
8    world.add_circle(x, y, 2, 0.1);
9    // 设置速度...
10  }
11}

游戏场景

1// 弹球游戏
2class BallGame {
3  constructor() {
4    this.world = new PhysicsWorld(800, 600);
5
6    // 添加边界
7    // 添加障碍物
8    // 添加玩家控制的板
9  }
10
11  update(dt) {
12    this.world.update(dt);
13
14    // 检查游戏逻辑
15    this.checkScore();
16    this.checkGameOver();
17  }
18}

什么时候用

适合:

  • 大量物体 (>100)
  • 复杂物理 (碰撞、约束)
  • 实时交互
  • 粒子系统

不适合:

  • 简单动画 (用 CSS)
  • 少量对象 (<10)
  • 静态场景