25、C++项目实战:物理引擎模拟(小球碰撞)
物理引擎,听起来很高大上对吧?其实说白了,就是让计算机里的物体遵循现实世界的物理规律。今天我们要做的,是一个经典中的经典——小球碰撞模拟。你想想看,台球桌上的碰撞、分子运动、甚至游戏里的弹跳,背后都是这套逻辑。
我个人觉得,这个项目是理解面向对象、碰撞检测、以及简单物理模拟的绝佳切入点。代码量不大,但知识点很密集。嗯,咱们直接开干。
项目目标与核心逻辑
我们要实现一个2D平面内,多个小球自由运动并相互碰撞的模拟器。核心就两件事:移动和碰撞。
- 移动:每个小球有位置 (x, y) 和速度 (vx, vy)。每帧更新位置。
- 碰撞:检测小球之间、小球与墙壁之间是否发生碰撞,并更新速度。
这里有个关键点:碰撞检测的时机。我曾经在项目里犯过一个低级错误——在更新位置之后才检测碰撞。结果小球直接穿模了。正确的做法是:先检测,再更新位置,或者检测到碰撞后立即修正位置和速度。
核心公式(弹性碰撞):
两球质量相等时,交换速度分量。
质量不等时,需要用到动量守恒和能量守恒公式。
两球质量相等时,交换速度分量。
质量不等时,需要用到动量守恒和能量守恒公式。
SVG 结构图:物理引擎核心流程
代码实现:Ball 类设计
我习惯把每个小球封装成一个类。这样代码清晰,也方便扩展。来看看核心成员:
class Ball {
public:
float x, y; // 位置
float vx, vy; // 速度
float radius; // 半径
float mass; // 质量
Color color; // 颜色(用于渲染)
Ball(float x, float y, float vx, float vy, float r, float m)
: x(x), y(y), vx(vx), vy(vy), radius(r), mass(m) {}
// 更新位置
void update(float dt) {
x += vx * dt;
y += vy * dt;
}
// 墙壁碰撞(边界反弹)
void checkWallCollision(float width, float height) {
if (x - radius < 0) { x = radius; vx = -vx; }
if (x + radius > width) { x = width - radius; vx = -vx; }
if (y - radius < 0) { y = radius; vy = -vy; }
if (y + radius > height) { y = height - radius; vy = -vy; }
}
};
小技巧: 墙壁碰撞时,我习惯把小球位置拉回边界内,防止它卡在墙里。这个细节在帧率不稳定时特别重要。
核心算法:两球碰撞处理
两球碰撞是物理引擎的灵魂。这里我用的是弹性碰撞公式。说白了,就是动量守恒和能量守恒联立求解。
我记得第一次实现时,直接套公式结果小球乱飞。后来发现是法向量方向搞反了。嗯,方向很重要。
void resolveBallCollision(Ball& a, Ball& b) {
// 计算两球中心距离向量
float dx = b.x - a.x;
float dy = b.y - a.y;
float dist = sqrt(dx*dx + dy*dy);
// 如果距离小于半径和,说明发生了碰撞
if (dist < a.radius + b.radius) {
// 法向量单位化
float nx = dx / dist;
float ny = dy / dist;
// 相对速度在法向量上的投影
float dvx = a.vx - b.vx;
float dvy = a.vy - b.vy;
float dvn = dvx * nx + dvy * ny;
// 如果相对速度是远离方向,不处理(防止重复碰撞)
if (dvn > 0) return;
// 质量相关
float totalMass = a.mass + b.mass;
float impulse = (2.0f * dvn) / totalMass;
// 更新速度
a.vx -= impulse * b.mass * nx;
a.vy -= impulse * b.mass * ny;
b.vx += impulse * a.mass * nx;
b.vy += impulse * a.mass * ny;
// 位置修正:把小球分开,防止重叠
float overlap = (a.radius + b.radius) - dist;
a.x -= overlap * nx * 0.5f;
a.y -= overlap * ny * 0.5f;
b.x += overlap * nx * 0.5f;
b.y += overlap * ny * 0.5f;
}
}
避坑指南: 我曾经在循环里同时更新了位置和速度,导致碰撞处理时位置已经变了,结果出现「抖动」现象。正确的做法是:先统一检测所有碰撞,再统一处理速度更新,最后统一更新位置。或者每检测到一对碰撞就立即处理,但要注意不要重复处理同一对。
主循环与性能考量
主循环其实很简单:
while (running) {
float dt = getDeltaTime(); // 获取帧时间
// 1. 更新所有小球位置
for (auto& ball : balls) {
ball.update(dt);
}
// 2. 墙壁碰撞
for (auto& ball : balls) {
ball.checkWallCollision(SCREEN_WIDTH, SCREEN_HEIGHT);
}
// 3. 两两碰撞检测(O(n²) 复杂度)
for (size_t i = 0; i < balls.size(); ++i) {
for (size_t j = i + 1; j < balls.size(); ++j) {
resolveBallCollision(balls[i], balls[j]);
}
}
// 4. 渲染
render(balls);
}
这里有个性能问题:当小球数量超过100个时,O(n²)的碰撞检测会变得很慢。你想想看,100个小球就要检测4950对。我建议引入空间分区(比如网格或四叉树)来优化。不过作为入门项目,这个版本已经够用了。
表格:常见问题与解决方案
| 问题现象 | 可能原因 | 我的解决方案 |
|---|---|---|
| 小球重叠在一起 | 碰撞后没有做位置修正 | 在碰撞处理中加入 overlap 修正代码 |
| 小球越弹越快 | 帧时间 dt 不稳定,导致能量不守恒 | 固定时间步长(fixed timestep) |
| 小球穿过墙壁 | 速度太快,一帧内穿过了边界 | 使用 swept 碰撞检测,或限制最大速度 |
| 碰撞时小球抖动 | 重复碰撞检测,或位置更新顺序错误 | 确保每对碰撞只处理一次,先处理再更新 |
扩展思路:让模拟更真实
做完基础版本后,你可以试试这些方向:
- 摩擦力与空气阻力:给速度加一个衰减系数,让小球慢慢停下来。
- 非弹性碰撞:引入恢复系数(restitution),让碰撞损失一部分能量。
- 重力场:在 update 里给 vy 加上重力加速度。
- 多线程优化:把碰撞检测分到多个线程里,提升性能。
我的建议: 先把这个基础版本跑通,再逐步加功能。一口吃不成胖子,物理引擎的坑很多,慢慢踩才有意思。
好了,这个项目就讲到这里。代码量不大,但每一步都有值得琢磨的地方。动手试试吧,你会发现物理模拟其实没那么神秘。