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 加上重力加速度。
  • 多线程优化:把碰撞检测分到多个线程里,提升性能。
我的建议: 先把这个基础版本跑通,再逐步加功能。一口吃不成胖子,物理引擎的坑很多,慢慢踩才有意思。

好了,这个项目就讲到这里。代码量不大,但每一步都有值得琢磨的地方。动手试试吧,你会发现物理模拟其实没那么神秘。


公众号:蓝海资料掘金营,微信 deep3321