88、C++项目实战:游戏引擎(物理碰撞)

物理碰撞,说白了就是让游戏里的物体能「真实地」撞在一起。你想想看,如果没有碰撞检测,子弹会穿过墙壁,角色会掉进地板——那还玩什么?

我在做这个项目之前,一直觉得物理引擎是那些数学天才才能碰的东西。直到自己动手撸了一个简易版,才发现核心原理其实没那么玄乎。今天我就带你一步步拆解,从碰撞检测到碰撞响应,把整个流程走通。

物理碰撞的核心问题

游戏引擎里的碰撞,本质上就两个问题:

  1. 有没有撞上?——碰撞检测
  2. 撞上之后怎么办?——碰撞响应

嗯,听起来简单。但实际做起来,坑多得很。我曾经在一个项目里,因为碰撞检测的精度没处理好,导致角色卡在墙角里疯狂抖动——那画面,简直像在跳霹雳舞。

碰撞检测:从简单到复杂

我们先从最简单的开始。最常用的碰撞检测方法,就是包围盒(AABB)。说白了,就是用矩形把物体框起来,然后判断两个矩形有没有重叠。

核心思路:两个轴对齐矩形,如果它们在X轴和Y轴上的投影都有重叠,那它们就撞上了。

struct AABB {
    float x, y;      // 中心坐标
    float w, h;      // 宽高
    
    bool overlaps(const AABB& other) const {
        // 检查X轴是否有重叠
        bool xOverlap = abs(x - other.x) < (w/2 + other.w/2);
        // 检查Y轴是否有重叠
        bool yOverlap = abs(y - other.y) < (h/2 + other.h/2);
        return xOverlap && yOverlap;
    }
};

这段代码我写过不下十遍。你注意看,这里用的是「中心坐标+半宽高」的表示法。我个人习惯用这种,因为做碰撞响应时计算穿透深度特别方便。

小技巧:如果物体是圆形的,用圆-圆碰撞检测更简单——直接判断两个圆心距离是否小于半径之和。我在做弹幕游戏时就用这个,性能好得很。

碰撞响应:推出去,别卡住

检测到碰撞之后,下一步就是「怎么处理」。最简单的做法是:把物体沿着碰撞法线方向推出去

为什么会这样?你想想看,两个物体重叠了,说明它们「不该在同一个位置」。我们要做的就是计算一个最小的位移,让它们刚好分开。

struct CollisionResult {
    bool collided;
    float penetration;  // 穿透深度
    float normalX, normalY;  // 碰撞法线方向
};

CollisionResult resolveAABB(const AABB& a, const AABB& b) {
    CollisionResult result = {false, 0, 0, 0};
    
    // 计算各个方向上的重叠量
    float overlapX = (a.w/2 + b.w/2) - abs(a.x - b.x);
    float overlapY = (a.h/2 + b.h/2) - abs(a.y - b.y);
    
    if (overlapX > 0 && overlapY > 0) {
        result.collided = true;
        
        // 选择重叠量最小的方向作为分离方向
        if (overlapX < overlapY) {
            result.penetration = overlapX;
            result.normalX = (a.x < b.x) ? -1.0f : 1.0f;
            result.normalY = 0;
        } else {
            result.penetration = overlapY;
            result.normalX = 0;
            result.normalY = (a.y < b.y) ? -1.0f : 1.0f;
        }
    }
    
    return result;
}

这里有个关键点:选择重叠量最小的方向。我曾经踩过这个坑——如果随便选一个方向推,物体会「跳」到奇怪的位置。比如你从侧面撞墙,结果被推到天上去了,那体验就太糟糕了。

注意:碰撞响应之后,一定要更新物体的位置。否则下一帧检测时,它们还是重叠的,就会反复推来推去——这就是我之前说的「抖动」问题。

物理碰撞的完整流程

把上面这些串起来,一个完整的物理碰撞系统大概长这样:

物理碰撞系统流程图 输入:物体位置、速度 碰撞检测(AABB / 圆-圆) 是否碰撞? 碰撞响应:计算法线、穿透深度 输出:更新后的位置 否 → 跳过

这个流程看起来简单,但实际项目中,你还要考虑性能优化。比如,如果场景里有1000个物体,两两检测就是50万次计算——这谁顶得住?

空间划分:别傻傻地全量检测

我建议你引入网格空间划分。把场景分成一个个格子,每个物体只跟同格子里的物体做碰撞检测。这样能把O(n²)降到接近O(n)。

class SpatialGrid {
    int cellSize;
    std::unordered_map<int, std::vector<Entity*>> cells;
    
    int getCellKey(float x, float y) const {
        int cx = static_cast<int>(x / cellSize);
        int cy = static_cast<int>(y / cellSize);
        return cx * 10000 + cy;  // 简单的哈希
    }
    
    void addEntity(Entity* e) {
        int key = getCellKey(e->x, e->y);
        cells[key].push_back(e);
    }
    
    std::vector<Entity*> getNearby(Entity* e) {
        int key = getCellKey(e->x, e->y);
        return cells[key];  // 只检测同格子的物体
    }
};

经验之谈:格子大小怎么选?我一般取物体平均大小的2-3倍。太小了格子太多,太大了又退化成全量检测。这个值需要根据你的场景调优。

碰撞响应的进阶:反弹与摩擦

如果你想让碰撞更「真实」,可以加入弹性系数摩擦力。比如,一个皮球撞到地面,它会弹起来;一个箱子推过去,它会滑行一段距离。

struct PhysicsMaterial {
    float restitution;  // 弹性系数,0~1
    float friction;     // 摩擦系数
};

void applyCollisionResponse(Entity* a, Entity* b, 
                            const CollisionResult& result) {
    // 分离物体
    a->x += result.normalX * result.penetration * 0.5f;
    a->y += result.normalY * result.penetration * 0.5f;
    b->x -= result.normalX * result.penetration * 0.5f;
    b->y -= result.normalY * result.penetration * 0.5f;
    
    // 计算相对速度
    float relVx = a->vx - b->vx;
    float relVy = a->vy - b->vy;
    
    // 沿法线方向的速度分量
    float normalV = relVx * result.normalX + relVy * result.normalY;
    
    // 如果正在远离,不处理
    if (normalV > 0) return;
    
    // 应用弹性系数
    float e = min(a->material.restitution, b->material.restitution);
    float impulse = -(1 + e) * normalV / 2;
    
    a->vx += impulse * result.normalX;
    a->vy += impulse * result.normalY;
    b->vx -= impulse * result.normalX;
    b->vy -= impulse * result.normalY;
}

这段代码里,restitution 是关键。值为1时是完美弹性碰撞(像台球),值为0时是完全非弹性碰撞(像两团泥巴粘在一起)。我在做物理小游戏时,一般把地面设为0.3,角色设为0.1——这样既不会弹得太夸张,也不会完全粘住。

常见坑点与避坑指南

坑点 现象 解决方案
物体穿透 高速物体直接穿过薄墙 使用连续碰撞检测(CCD),或限制最大速度
抖动 物体卡在墙角疯狂震动 加入「休眠」机制,速度很小时直接置零
堆叠不稳定 物体堆叠时慢慢滑落 使用迭代求解,多次处理碰撞
性能爆炸 物体一多就卡顿 空间划分 + 多线程并行检测

我曾经踩过的坑:有一次做平台跳跃游戏,角色从高处落下时,因为速度太快,直接穿过了地面。查了半天才发现,是碰撞检测只做了「当前帧的位置」,没有考虑「两帧之间的运动轨迹」。后来我改用连续碰撞检测,把运动路径分段检测,才彻底解决。

总结一下

物理碰撞系统,说白了就是三件事:

  • 检测——用AABB或圆-圆判断是否重叠
  • 响应——沿法线方向推开,并调整速度
  • 优化——用空间划分避免全量检测

嗯,其实核心代码加起来也就两三百行。但要把这些代码打磨得稳定、高效、不出bug,那才是真正考验功底的地方。我建议你从最简单的矩形碰撞开始,跑通了再慢慢加弹性、摩擦、空间划分这些特性。一口吃不成胖子,物理引擎也是一样。


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