每日算法 — 使用java实现游戏物理引擎:2D刚体动力学模拟与碰撞响应系统

游戏物理引擎是现代电子游戏中不可或缺的组成部分,负责模拟现实世界的物理规律,包括重力、摩擦力、弹力、碰撞等。从《愤怒的小鸟》的抛物线弹道到《我的世界》的方块堆叠,物理引擎为游戏增添了真实感和可玩性。本文将用Java实现一个简化版的2D物理引擎,涵盖刚体运动学、AABB碰撞检测、冲量响应等核心机制。

物理引擎核心概念

一个基础的2D物理引擎通常包含以下核心组件:

  1. 刚体(RigidBody):具有质量、位置、速度、加速度的物理实体,假设物体形状在运动中不发生形变
  2. 碰撞检测(Collision Detection):识别两个物体是否发生空间重叠,常用AABB(轴对齐包围盒)或OBB(定向包围盒)
  3. 碰撞响应(Collision Response):处理碰撞后的物理效果,通过动量守恒和冲量计算改变物体速度
  4. 积分器(Integrator):根据牛顿第二定律 $F=ma$ 更新物体位置和速度,常用欧拉积分或Verlet积分

Java实现

import java.util.ArrayList;
import java.util.List;

/**
 * 2D物理引擎核心实现
 * 包含刚体、AABB碰撞检测、冲量响应和重力模拟
 */
public class PhysicsEngine {

    private static final double GRAVITY = 500.0;      // 重力加速度(像素/秒²)
    private static final double RESTITUTION = 0.7;    // 弹性系数(0-1)
    private static final double FRICTION = 0.98;      // 空气阻力系数

    /**
     * 二维向量类,用于表示位置、速度、力
     */
    public static class Vec2 {
        public double x, y;
        public Vec2(double x, double y) { this.x = x; this.y = y; }
        public Vec2 add(Vec2 v) { return new Vec2(x + v.x, y + v.y); }
        public Vec2 sub(Vec2 v) { return new Vec2(x - v.x, y - v.y); }
        public Vec2 mul(double s) { return new Vec2(x * s, y * s); }
        public double dot(Vec2 v) { return x * v.x + y * v.y; }
        public double length() { return Math.sqrt(x * x + y * y); }
        public Vec2 normalize() {
            double len = length();
            return len == 0 ? new Vec2(0, 0) : new Vec2(x / len, y / len);
        }
        public Vec2 negate() { return new Vec2(-x, -y); }
    }

    /**
     * AABB轴对齐包围盒
     */
    public static class AABB {
        public Vec2 min;  // 左下角
        public Vec2 max;  // 右上角

        public AABB(Vec2 min, Vec2 max) {
            this.min = min;
            this.max = max;
        }

        public double width() { return max.x - min.x; }
        public double height() { return max.y - min.y; }
        public Vec2 center() { return new Vec2((min.x + max.x) / 2, (min.y + max.y) / 2); }
    }

    /**
     * 刚体对象
     */
    public static class RigidBody {
        public Vec2 position;      // 当前位置
        public Vec2 velocity;      // 当前速度
        public Vec2 acceleration;  // 当前加速度
        public double mass;        // 质量
        public double invMass;     // 质量的倒数(0表示无限质量/静止物体)
        public AABB bounds;        // 包围盒(相对位置)
        public boolean isStatic;   // 是否为静止物体

        public RigidBody(Vec2 pos, Vec2 size, double mass) {
            this.position = pos;
            this.velocity = new Vec2(0, 0);
            this.acceleration = new Vec2(0, 0);
            this.mass = mass;
            this.invMass = mass == 0 ? 0 : 1.0 / mass;
            this.bounds = new AABB(new Vec2(-size.x / 2, -size.y / 2), 
                                   new Vec2(size.x / 2, size.y / 2));
            this.isStatic = mass == 0;
        }

        /** 获取世界空间的AABB */
        public AABB getWorldAABB() {
            return new AABB(
                new Vec2(position.x + bounds.min.x, position.y + bounds.min.y),
                new Vec2(position.x + bounds.max.x, position.y + bounds.max.y)
            );
        }

        /** 施加力 */
        public void applyForce(Vec2 force) {
            if (isStatic) return;
            // F = ma  =>  a = F/m
            acceleration = acceleration.add(force.mul(invMass));
        }
    }

    /**
     * 碰撞信息
     */
    public static class Collision {
        public RigidBody bodyA;
        public RigidBody bodyB;
        public Vec2 normal;      // 碰撞法线(从A指向B)
        public double penetration; // 穿透深度
    }

    private List<RigidBody> bodies = new ArrayList<>();

    public void addBody(RigidBody body) {
        bodies.add(body);
    }

    /**
     * AABB碰撞检测
     * @return 若发生碰撞返回碰撞信息,否则返回null
     */
    public Collision detectAABB(RigidBody a, RigidBody b) {
        AABB boxA = a.getWorldAABB();
        AABB boxB = b.getWorldAABB();

        // 分离轴定理的简化版本:AABB在x轴和y轴上的投影是否重叠
        double overlapX = Math.min(boxA.max.x - boxB.min.x, boxB.max.x - boxA.min.x);
        double overlapY = Math.min(boxA.max.y - boxB.min.y, boxB.max.y - boxA.min.y);

        // 任一轴无重叠则无碰撞
        if (overlapX <= 0 || overlapY <= 0) return null;

        Collision col = new Collision();
        col.bodyA = a;
        col.bodyB = b;
        col.penetration = Math.min(overlapX, overlapY);

        // 确定碰撞法线方向(穿透最小的轴)
        if (overlapX < overlapY) {
            col.normal = new Vec2(boxA.center().x < boxB.center().x ? -1 : 1, 0);
        } else {
            col.normal = new Vec2(0, boxA.center().y < boxB.center().y ? -1 : 1);
        }

        return col;
    }

    /**
     * 碰撞响应:基于冲量的分离与反弹
     */
    public void resolveCollision(Collision col) {
        RigidBody a = col.bodyA;
        RigidBody b = col.bodyB;
        Vec2 normal = col.normal;

        // 相对速度
        Vec2 relativeVel = b.velocity.sub(a.velocity);
        double velAlongNormal = relativeVel.dot(normal);

        // 若物体正在分离,无需处理
        if (velAlongNormal > 0) return;

        // 计算冲量标量:j = -(1+e) * (v·n) / (1/mA + 1/mB)
        double j = -(1 + RESTITUTION) * velAlongNormal;
        j /= a.invMass + b.invMass;

        // 应用冲量
        Vec2 impulse = normal.mul(j);
        a.velocity = a.velocity.sub(impulse.mul(a.invMass));
        b.velocity = b.velocity.add(impulse.mul(b.invMass));

        // 位置修正:防止物体持续穿透
        double percent = 0.2;  // 修正比例
        Vec2 correction = normal.mul(col.penetration * percent / (a.invMass + b.invMass));
        a.position = a.position.sub(correction.mul(a.invMass));
        b.position = b.position.add(correction.mul(b.invMass));
    }

    /**
     * 物理步进更新
     * @param dt 时间步长(秒)
     */
    public void step(double dt) {
        // 1. 施加重力并积分速度
        for (RigidBody body : bodies) {
            if (body.isStatic) continue;

            // 施加重力
            body.applyForce(new Vec2(0, body.mass * GRAVITY));

            // 欧拉积分更新速度:v = v + a * dt
            body.velocity = body.velocity.add(body.acceleration.mul(dt));

            // 空气阻力
            body.velocity = body.velocity.mul(FRICTION);

            // 欧拉积分更新位置:p = p + v * dt
            body.position = body.position.add(body.velocity.mul(dt));

            // 重置加速度
            body.acceleration = new Vec2(0, 0);
        }

        // 2. 碰撞检测与响应
        for (int i = 0; i < bodies.size(); i++) {
            for (int j = i + 1; j < bodies.size(); j++) {
                Collision col = detectAABB(bodies.get(i), bodies.get(j));
                if (col != null) {
                    resolveCollision(col);
                }
            }
        }

        // 3. 边界约束(简单地面和墙壁)
        for (RigidBody body : bodies) {
            AABB worldBox = body.getWorldAABB();

            // 地面约束(y=600)
            if (worldBox.max.y > 600) {
                body.position.y = 600 - body.bounds.max.y;
                body.velocity.y *= -RESTITUTION;
            }
            // 左墙
            if (worldBox.min.x < 0) {
                body.position.x = -body.bounds.min.x;
                body.velocity.x *= -RESTITUTION;
            }
            // 右墙
            if (worldBox.max.x > 800) {
                body.position.x = 800 - body.bounds.max.x;
                body.velocity.x *= -RESTITUTION;
            }
        }
    }

    public static void main(String[] args) {
        PhysicsEngine engine = new PhysicsEngine();

        // 创建地面(静止物体)
        RigidBody ground = new RigidBody(new Vec2(400, 620), new Vec2(800, 40), 0);
        engine.addBody(ground);

        // 创建动态物体
        for (int i = 0; i < 5; i++) {
            RigidBody box = new RigidBody(
                new Vec2(200 + i * 80, 50 + i * 30),
                new Vec2(40, 40),
                10.0
            );
            box.velocity = new Vec2(Math.random() * 200 - 100, 0);
            engine.addBody(box);
        }

        // 模拟物理世界
        double dt = 1.0 / 60.0;
        for (int step = 0; step < 600; step++) {
            engine.step(dt);
            if (step % 60 == 0) {
                RigidBody b = engine.bodies.get(2);
                System.out.printf("Step %d: pos=(%.1f, %.1f), vel=(%.1f, %.1f)%n",
                    step, b.position.x, b.position.y, b.velocity.x, b.velocity.y);
            }
        }
    }
}

核心算法解析

碰撞检测优化

本文使用AABB碰撞检测,适用于轴对齐的矩形物体。对于圆形或旋转物体,可使用圆形-圆形检测或SAT(分离轴定理)。在大量物体场景下,应采用空间划分算法(如四叉树或网格)将碰撞检测复杂度从 $O(n^2)$ 降低到近似 $O(n)$。

积分器选择

本文使用显式欧拉积分,实现简单但存在能量漂移问题。对于更精确的模拟,可采用:
– Verlet积分:位置Verlet算法,能量守恒更好
– RK4(四阶龙格-库塔):高精度但计算量较大
– 半隐式欧拉:先更新速度再更新位置,稳定性优于显式欧拉

冲量计算

碰撞响应基于动量守恒定律。设两物体质量分别为 $m_A$ 和 $m_B$,碰撞前相对速度为 $v_{rel}$,弹性系数为 $e$,则冲量大小为:

$$j = \frac{-(1+e)(v_{rel} \cdot n)}{\frac{1}{m_A} + \frac{1}{m_B}}$$

其中 $n$ 为碰撞法线。通过将冲量沿法线方向施加到两个物体上,实现真实的反弹效果。

扩展方向

一个完整的商业级物理引擎(如Box2D)还包含:关节约束(RevoluteJoint、DistanceJoint)、连续碰撞检测(CCD,防止高速物体穿透)、休眠机制(静止物体暂停模拟以节省计算资源)、以及更复杂的接触流形处理。本文实现的简化版引擎已涵盖最核心的物理模拟机制,适合作为学习物理引擎原理的入门项目。