游戏物理引擎是现代电子游戏中不可或缺的组成部分,负责模拟现实世界的物理规律,包括重力、摩擦力、弹力、碰撞等。从《愤怒的小鸟》的抛物线弹道到《我的世界》的方块堆叠,物理引擎为游戏增添了真实感和可玩性。本文将用Java实现一个简化版的2D物理引擎,涵盖刚体运动学、AABB碰撞检测、冲量响应等核心机制。
物理引擎核心概念
一个基础的2D物理引擎通常包含以下核心组件:
- 刚体(RigidBody):具有质量、位置、速度、加速度的物理实体,假设物体形状在运动中不发生形变
- 碰撞检测(Collision Detection):识别两个物体是否发生空间重叠,常用AABB(轴对齐包围盒)或OBB(定向包围盒)
- 碰撞响应(Collision Response):处理碰撞后的物理效果,通过动量守恒和冲量计算改变物体速度
- 积分器(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,防止高速物体穿透)、休眠机制(静止物体暂停模拟以节省计算资源)、以及更复杂的接触流形处理。本文实现的简化版引擎已涵盖最核心的物理模拟机制,适合作为学习物理引擎原理的入门项目。