引言
滚动的天空(Rolling Sky)是一款快节奏的3D音乐节奏跑酷游戏,玩家操控一个小球在悬浮于天空中的复杂轨道上滚动前进,需要精准躲避障碍物并在平台间跳跃。游戏的核心算法挑战在于球体刚体运动学与3D平台碰撞检测。本文将用Java实现一个简化版的滚动天空物理引擎,涵盖球体滚动动力学、AABB与OBB碰撞检测、以及基于预测的最优路径决策算法,帮助读者理解3D游戏物理的核心原理。
球体刚体运动学
1. 滚动动力学模型
当球体在斜面上无滑动滚动时,平动与转动满足约束关系:
$$
v = \omega \times r
$$
其中 $v$ 为球心线速度,$\omega$ 为角速度,$r$ 为球半径。
球体在重力作用下的加速度为:
$$
a = \frac{g \sin(\theta)}{1 + \frac{I}{mr^2}}
$$
对于实心球,转动惯量 $I = \frac{2}{5}mr^2$,因此:
$$
a = \frac{5}{7} g \sin(\theta)
$$
2. Java实现
/**
* 三维向量
*/
public class Vec3 {
public double x, y, z;
public Vec3(double x, double y, double z) {
this.x = x; this.y = y; this.z = z;
}
public Vec3 add(Vec3 v) { return new Vec3(x + v.x, y + v.y, z + v.z); }
public Vec3 subtract(Vec3 v) { return new Vec3(x - v.x, y - v.y, z - v.z); }
public Vec3 multiply(double s) { return new Vec3(x * s, y * s, z * s); }
public double dot(Vec3 v) { return x * v.x + y * v.y + z * v.z; }
public Vec3 cross(Vec3 v) {
return new Vec3(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x);
}
public double length() { return Math.sqrt(x * x + y * y + z * z); }
public Vec3 normalize() {
double len = length();
if (len < 1e-10) return new Vec3(0, 0, 0);
return multiply(1.0 / len);
}
@Override
public String toString() {
return String.format("(%.3f, %.3f, %.3f)", x, y, z);
}
}
/**
* 球体物理对象
* 包含平动、转动和碰撞响应
*/
public class BallPhysics {
// 物理常量
private static final double GRAVITY = 9.8;
private static final double INERTIA_FACTOR = 2.0 / 5.0; // 实心球的 I/(mr^2)
private static final double ROLLING_FACTOR = 1.0 / (1.0 + INERTIA_FACTOR); // 5/7
private static final double TIME_STEP = 0.016; // 60fps
private static final double RESTITUTION = 0.3; // 弹性系数
private static final double FRICTION = 0.98; // 滚动摩擦衰减
private Vec3 position; // 球心位置
private Vec3 velocity; // 线速度
private Vec3 angularVelocity; // 角速度
private double radius;
private double mass;
public BallPhysics(Vec3 pos, double radius, double mass) {
this.position = pos;
this.velocity = new Vec3(0, 0, 0);
this.angularVelocity = new Vec3(0, 0, 0);
this.radius = radius;
this.mass = mass;
}
/**
* 物理更新
* @param platforms 当前场景中的所有平台
* @param playerInput 玩家输入:x轴偏移 [-1, 1]
*/
public void update(Platform[] platforms, double playerInput) {
// 找到球体下方的平台
Platform ground = findGroundPlatform(platforms);
if (ground != null) {
updateOnPlatform(ground, playerInput);
} else {
updateInAir();
}
// 与障碍物的碰撞检测
for (Platform p : platforms) {
if (p.isObstacle() && checkSphereAABB(position, radius, p)) {
resolveCollision(p);
}
}
// 边界检查
if (position.y < -10) {
// 掉落深渊,重置
position = new Vec3(0, 2, 0);
velocity = new Vec3(0, 0, 0);
}
}
private void updateOnPlatform(Platform ground, double playerInput) {
// 平台法线和切线方向
Vec3 normal = ground.getNormal();
Vec3 forward = ground.getForwardDirection();
Vec3 right = forward.cross(normal).normalize();
// 重力沿斜面分量
Vec3 gravity = new Vec3(0, -GRAVITY, 0);
double slopeAccel = gravity.dot(forward) * ROLLING_FACTOR;
// 自动向前加速(游戏机制)
double autoForwardAccel = 5.0 * ROLLING_FACTOR;
// 玩家侧向输入
double sideAccel = playerInput * 8.0;
// 总加速度
Vec3 acceleration = forward.multiply(slopeAccel + autoForwardAccel)
.add(right.multiply(sideAccel));
// 更新速度
velocity = velocity.add(acceleration.multiply(TIME_STEP));
// 滚动摩擦
velocity = velocity.multiply(FRICTION);
// 限制最大速度
double maxSpeed = 15.0;
if (velocity.length() > maxSpeed) {
velocity = velocity.normalize().multiply(maxSpeed);
}
// 更新位置
position = position.add(velocity.multiply(TIME_STEP));
// 更新角速度(滚动约束)
angularVelocity = new Vec3(
-velocity.z / radius,
0,
velocity.x / radius
);
// 贴地:将球心保持在平台表面上方 radius 处
double distToPlane = ground.signedDistance(position);
if (Math.abs(distToPlane - radius) > 0.01) {
position = position.add(normal.multiply(radius - distToPlane));
}
}
private void updateInAir() {
// 空中仅受重力
Vec3 gravity = new Vec3(0, -GRAVITY, 0);
velocity = velocity.add(gravity.multiply(TIME_STEP));
position = position.add(velocity.multiply(TIME_STEP));
}
/**
* 跳跃
*/
public void jump() {
if (findGroundPlatform(new Platform[0]) != null) {
velocity = velocity.add(new Vec3(0, 8, 0));
}
}
private Platform findGroundPlatform(Platform[] platforms) {
Platform best = null;
double bestDist = Double.MAX_VALUE;
for (Platform p : platforms) {
if (p.isObstacle()) continue;
if (checkSphereAABB(position, radius, p)) {
double dist = Math.abs(p.signedDistance(position) - radius);
if (dist < bestDist && p.signedDistance(position) >= 0) {
bestDist = dist;
best = p;
}
}
}
return best;
}
private boolean checkSphereAABB(Vec3 center, double r, Platform p) {
Vec3 closest = p.closestPoint(center);
double dist = center.subtract(closest).length();
return dist < r;
}
private void resolveCollision(Platform obstacle) {
Vec3 closest = obstacle.closestPoint(position);
Vec3 normal = position.subtract(closest).normalize();
// 将球推出碰撞体
double penetration = radius - position.subtract(closest).length();
position = position.add(normal.multiply(penetration + 0.01));
// 反射速度
double vn = velocity.dot(normal);
if (vn < 0) {
Vec3 vNormal = normal.multiply(vn);
Vec3 vTangent = velocity.subtract(vNormal);
velocity = vTangent.subtract(vNormal.multiply(RESTITUTION));
}
}
public Vec3 getPosition() { return position; }
public Vec3 getVelocity() { return velocity; }
public double getRadius() { return radius; }
}
3D平台碰撞检测
AABB与OBB碰撞体
/**
* 3D平台基类,支持AABB和OBB两种碰撞体
*/
public abstract class Platform {
protected Vec3 center;
protected boolean isObstacle;
public abstract boolean checkSphereCollision(Vec3 sphereCenter, double radius);
public abstract Vec3 closestPoint(Vec3 p);
public abstract double signedDistance(Vec3 p);
public abstract Vec3 getNormal();
public abstract Vec3 getForwardDirection();
public boolean isObstacle() { return isObstacle; }
}
/**
* 轴对齐包围盒(AABB)平台
* 简化处理,法线始终朝上
*/
public class AABBPlatform extends Platform {
private Vec3 min, max; // 边界
private Vec3 normal;
private Vec3 forward;
public AABBPlatform(Vec3 center, Vec3 size, boolean isObstacle) {
this.center = center;
this.isObstacle = isObstacle;
Vec3 half = size.multiply(0.5);
this.min = center.subtract(half);
this.max = center.add(half);
this.normal = new Vec3(0, 1, 0);
this.forward = new Vec3(0, 0, 1);
}
@Override
public boolean checkSphereCollision(Vec3 sphereCenter, double radius) {
Vec3 closest = closestPoint(sphereCenter);
return sphereCenter.subtract(closest).length() < radius;
}
@Override
public Vec3 closestPoint(Vec3 p) {
return new Vec3(
clamp(p.x, min.x, max.x),
clamp(p.y, min.y, max.y),
clamp(p.z, min.z, max.z)
);
}
@Override
public double signedDistance(Vec3 p) {
return p.y - max.y; // 简化:仅考虑Y方向距离
}
@Override
public Vec3 getNormal() { return normal; }
@Override
public Vec3 getForwardDirection() { return forward; }
private double clamp(double v, double min, double max) {
return Math.max(min, Math.min(max, v));
}
}
/**
* 定向包围盒(OBB)平台
* 支持任意旋转,用于斜坡和旋转平台
*/
public class OBBPlatform extends Platform {
private Vec3 halfExtents; // 半边长
private Vec3[] axes; // 局部坐标轴(归一化)
private Vec3 normal;
private Vec3 forward;
/**
* 构造OBB平台
* @param center 中心点
* @param size 完整尺寸 (width, height, depth)
* @param rotationY Y轴旋转角度(弧度)
* @param isObstacle 是否为障碍物
*/
public OBBPlatform(Vec3 center, Vec3 size, double rotationY, boolean isObstacle) {
this.center = center;
this.isObstacle = isObstacle;
this.halfExtents = size.multiply(0.5);
// 构建旋转矩阵的列向量(局部坐标轴)
double cos = Math.cos(rotationY);
double sin = Math.sin(rotationY);
this.axes = new Vec3[3];
axes[0] = new Vec3(cos, 0, sin); // 局部X(右)
axes[1] = new Vec3(0, 1, 0); // 局部Y(上)
axes[2] = new Vec3(-sin, 0, cos); // 局部Z(前)
this.normal = axes[1];
this.forward = axes[2];
}
@Override
public boolean checkSphereCollision(Vec3 sphereCenter, double radius) {
Vec3 closest = closestPoint(sphereCenter);
return sphereCenter.subtract(closest).length() < radius;
}
/**
* OBB上最近点:将点变换到OBB局部坐标,clamp到包围盒,再变换回来
*/
@Override
public Vec3 closestPoint(Vec3 p) {
Vec3 d = p.subtract(center);
Vec3 closest = center;
// 在OBB的每个轴上投影并clamp
for (int i = 0; i < 3; i++) {
double dist = d.dot(axes[i]);
dist = Math.max(-halfExtents.x, Math.min(halfExtents.x, dist));
closest = closest.add(axes[i].multiply(dist));
}
return closest;
}
@Override
public double signedDistance(Vec3 p) {
// 简化的有符号距离:点在法线方向的投影
return p.subtract(center).dot(normal);
}
@Override
public Vec3 getNormal() { return normal; }
@Override
public Vec3 getForwardDirection() { return forward; }
}
场景生成与路径规划
/**
* 程序化场景生成器
* 生成随音乐节奏变化的平台布局
*/
public class LevelGenerator {
private final Random random;
private double totalLength = 0;
public LevelGenerator(long seed) {
this.random = new Random(seed);
}
/**
* 生成一段关卡
* @param beatTimes 音乐节拍时间
* @param bpm 每分钟节拍数
* @return 平台数组
*/
public Platform[] generateLevel(List<Double> beatTimes, double bpm) {
List<Platform> platforms = new ArrayList<>();
double beatInterval = 60.0 / bpm;
double platformSpeed = 5.0; // 球体平均前进速度
double z = 0;
double lastPlatformEnd = 0;
for (int i = 0; i < beatTimes.size(); i++) {
double beatTime = beatTimes.get(i);
// 平台长度与BPM相关
double platformLength = beatInterval * platformSpeed * 0.8;
double gap = beatInterval * platformSpeed * 0.2; // 间隙
// 每4拍一个变奏
boolean isVariation = (i % 4 == 3);
boolean isObstacle = random.nextDouble() < 0.15 && i > 2;
// 平台类型
if (isVariation && !isObstacle) {
// 斜坡平台
double slopeAngle = (random.nextBoolean() ? 1 : -1) * Math.PI / 12;
Vec3 center = new Vec3(
random.nextDouble() * 4 - 2, // X偏移 [-2, 2]
Math.sin(slopeAngle) * platformLength / 2,
z + platformLength / 2
);
Vec3 size = new Vec3(4, 0.2, platformLength);
platforms.add(new OBBPlatform(center, size, slopeAngle, false));
} else {
// 普通平台或障碍物
Vec3 center = new Vec3(
random.nextDouble() * 4 - 2,
0,
z + platformLength / 2
);
Vec3 size = new Vec3(4, 0.2, platformLength);
if (isObstacle) {
// 障碍物:竖直方块
Vec3 obsCenter = new Vec3(center.x, 0.5, center.z);
Vec3 obsSize = new Vec3(1, 1, 1);
platforms.add(new AABBPlatform(obsCenter, obsSize, true));
// 障碍物前后的连接平台
platforms.add(new AABBPlatform(
new Vec3(center.x, 0, z + platformLength * 0.25),
new Vec3(4, 0.2, platformLength * 0.4), false));
platforms.add(new AABBPlatform(
new Vec3(center.x, 0, z + platformLength * 0.75),
new Vec3(4, 0.2, platformLength * 0.4), false));
} else {
platforms.add(new AABBPlatform(center, size, false));
}
}
z += platformLength + gap;
lastPlatformEnd = z;
}
totalLength = lastPlatformEnd;
return platforms.toArray(new Platform[0]);
}
public double getTotalLength() { return totalLength; }
}
AI路径决策系统
/**
* 滚动天空AI路径规划器
* 使用预测性碰撞检测寻找最优横向位置
*/
public class RollingSkyAI {
private static final double LOOK_AHEAD_TIME = 1.0; // 向前预测1秒
private static final int SAMPLE_POINTS = 7; // 采样位置数
/**
* 评估给定横向偏移的安全性
* @param ball 当前球体
* @param platforms 场景平台
* @param targetX 目标横向位置
* @return 安全得分(越高越安全)
*/
public double evaluatePath(BallPhysics ball, Platform[] platforms, double targetX) {
Vec3 pos = ball.getPosition();
Vec3 vel = ball.getVelocity();
// 模拟未来轨迹
double score = 0;
double dt = 0.05;
for (double t = 0; t < LOOK_AHEAD_TIME; t += dt) {
// 线性预测位置(简化)
Vec3 futurePos = new Vec3(
pos.x + (targetX - pos.x) * (t / LOOK_AHEAD_TIME),
pos.y + vel.y * t - 0.5 * 9.8 * t * t,
pos.z + vel.z * t
);
// 检查是否有平台支撑
boolean hasSupport = false;
for (Platform p : platforms) {
if (!p.isObstacle() && p.checkSphereCollision(futurePos, ball.getRadius())) {
hasSupport = true;
break;
}
}
if (!hasSupport && futurePos.y > 0.5) {
score -= 100; // 坠落风险
}
// 检查障碍物碰撞
for (Platform p : platforms) {
if (p.isObstacle() && p.checkSphereCollision(futurePos, ball.getRadius())) {
score -= 200;
}
}
// 居中奖励(避免边缘)
double centerBonus = 1.0 - Math.abs(futurePos.x) / 3.0;
score += centerBonus * dt;
}
return score;
}
/**
* 寻找最优横向位置
*/
public double findOptimalX(BallPhysics ball, Platform[] platforms) {
double bestX = ball.getPosition().x;
double bestScore = Double.NEGATIVE_INFINITY;
for (int i = 0; i < SAMPLE_POINTS; i++) {
double x = -3 + i * (6.0 / (SAMPLE_POINTS - 1));
double score = evaluatePath(ball, platforms, x);
if (score > bestScore) {
bestScore = score;
bestX = x;
}
}
return bestX;
}
/**
* 生成玩家输入值 [-1, 1]
*/
public double getPlayerInput(BallPhysics ball, Platform[] platforms) {
double optimalX = findOptimalX(ball, platforms);
double currentX = ball.getPosition().x;
double diff = optimalX - currentX;
// 映射到 [-1, 1]
return Math.max(-1, Math.min(1, diff * 2));
}
}
主游戏引擎与演示
public class RollingSkyDemo {
public static void main(String[] args) {
System.out.println("=== 滚动的天空物理引擎启动 ===\n");
// 1. 生成音乐节拍
List<Double> beatTimes = new ArrayList<>();
double bpm = 128.0;
double beatInterval = 60.0 / bpm;
for (int i = 0; i < 60; i++) {
beatTimes.add(i * beatInterval);
}
// 2. 生成关卡
LevelGenerator levelGen = new LevelGenerator(12345L);
Platform[] platforms = levelGen.generateLevel(beatTimes, bpm);
System.out.println("生成平台数: " + platforms.length);
System.out.println("关卡总长度: " + String.format("%.1f", levelGen.getTotalLength()) + " 米");
// 3. 创建球体
BallPhysics ball = new BallPhysics(new Vec3(0, 2, 0), 0.3, 1.0);
// 4. 创建AI
RollingSkyAI ai = new RollingSkyAI();
// 5. 游戏循环
System.out.println("\n=== 游戏模拟 ===");
double totalTime = 0;
int frame = 0;
boolean gameOver = false;
while (!gameOver && totalTime < 30 && ball.getPosition().z < levelGen.getTotalLength()) {
double playerInput = ai.getPlayerInput(ball, platforms);
ball.update(platforms, playerInput);
totalTime += 0.016;
frame++;
if (frame % 60 == 0) {
Vec3 pos = ball.getPosition();
System.out.println("t=" + String.format("%.2f", totalTime)
+ "s | 位置=" + pos
+ " | 速度=" + String.format("%.2f", ball.getVelocity().length())
+ " m/s | AI输入=" + String.format("%.2f", playerInput));
}
// 掉落检测
if (ball.getPosition().y < -5) {
System.out.println("\n球体掉落!游戏结束。");
gameOver = true;
}
}
if (!gameOver) {
System.out.println("\n恭喜通关!最终位置: " + ball.getPosition());
}
// 6. 碰撞检测测试
System.out.println("\n=== 碰撞检测测试 ===");
AABBPlatform platform = new AABBPlatform(
new Vec3(0, 0, 5), new Vec3(4, 0.2, 4), false);
Vec3 testPos = new Vec3(0, 0.5, 5);
System.out.println("球体在 " + testPos + " 与平台碰撞? "
+ platform.checkSphereCollision(testPos, 0.3));
Vec3 testPos2 = new Vec3(3, 0.5, 5);
System.out.println("球体在 " + testPos2 + " 与平台碰撞? "
+ platform.checkSphereCollision(testPos2, 0.3));
// OBB测试
OBBPlatform slope = new OBBPlatform(
new Vec3(0, 0.5, 10), new Vec3(4, 0.2, 4), Math.PI / 6, false);
System.out.println("OBB法线: " + slope.getNormal());
System.out.println("OBB前向: " + slope.getForwardDirection());
}
}
复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 球体物理更新 | O(P) | O(1) | P为平台数 |
| AABB碰撞检测 | O(1) | O(1) | 坐标比较 |
| OBB最近点 | O(1) | O(1) | 3轴投影 |
| 场景生成 | O(B) | O(B) | B为节拍数 |
| AI路径评估 | O(P × T/dt) | O(1) | T为预测时长 |
| 游戏循环 | O(P) | O(P) | 每帧遍历所有平台 |
扩展方向
- 四叉树/八叉树空间分割:大量平台时使用空间索引加速碰撞检测
- 连续碰撞检测(CCD):高速运动时防止穿透(tunneling)
- 布料模拟:添加飘扬的旗帜或飘带效果增强视觉
- 神经网络AI:使用深度强化学习训练能长期生存的滚动小球
总结
本文通过Java实现了滚动的天空的核心物理与算法系统:
– 球体刚体动力学:精确模拟无滑动滚动,平动与转动耦合关系
– AABB/OBB碰撞检测:支持轴对齐和任意旋转的平台碰撞体
– 程序化关卡生成:基于音乐节拍自动生成有节奏感的平台布局
– 预测性AI决策:通过前瞻轨迹模拟评估最优路径,自动躲避障碍
这些技术是3D跑酷游戏、物理引擎和实时碰撞检测系统的基础,广泛应用于游戏开发、机器人仿真和虚拟现实领域。