滚动的天空(Rolling Sky)是一款快节奏的三维滚球游戏,玩家控制小球在悬浮于空中的道路上滚动,通过左右滑动躲避障碍。本文用Java实现其核心机制:三维空间中的球体运动学、基于四元数的平滑旋转、AABB碰撞检测与动态障碍生成算法。
核心设计思路
游戏世界可抽象为三个层次:
- 物理层:小球受重力、离心力与法向力作用,在三维曲面上滚动;
- 渲染层:将三维坐标投影到屏幕,绘制道路、障碍与球体;
- 逻辑层:根据球体位置实时检测与障碍的碰撞,生成前方道路。
本文聚焦于物理运动学与碰撞检测的算法实现。
完整Java实现
import java.util.*;
/**
* 滚动的天空核心引擎
* 包含:三维球体运动、四元数旋转、AABB碰撞、障碍生成
*/
public class RollingSkyEngine {
// ==================== 三维向量工具 ====================
static class Vec3 {
double x, y, z;
Vec3(double x, double y, double z) { this.x = x; this.y = y; this.z = z; }
Vec3 add(Vec3 v) { return new Vec3(x + v.x, y + v.y, z + v.z); }
Vec3 sub(Vec3 v) { return new Vec3(x - v.x, y - v.y, z - v.z); }
Vec3 mul(double s) { return new Vec3(x * s, y * s, z * s); }
double dot(Vec3 v) { return x * v.x + y * v.y + z * v.z; }
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
);
}
double length() { return Math.sqrt(x * x + y * y + z * z); }
Vec3 normalize() {
double len = length();
if (len < 1e-9) return new Vec3(0, 0, 0);
return new Vec3(x / len, y / len, z / len);
}
@Override
public String toString() {
return String.format("(%.2f, %.2f, %.2f)", x, y, z);
}
}
// ==================== 四元数:平滑旋转 ====================
static class Quaternion {
double w, x, y, z;
Quaternion(double w, double x, double y, double z) {
this.w = w; this.x = x; this.y = y; this.z = z;
}
// 绕轴旋转angle弧度
static Quaternion fromAxisAngle(Vec3 axis, double angle) {
Vec3 a = axis.normalize();
double half = angle / 2;
double s = Math.sin(half);
return new Quaternion(Math.cos(half), a.x * s, a.y * s, a.z * s);
}
Quaternion multiply(Quaternion q) {
return new Quaternion(
w * q.w - x * q.x - y * q.y - z * q.z,
w * q.x + x * q.w + y * q.z - z * q.y,
w * q.y - x * q.z + y * q.w + z * q.x,
w * q.z + x * q.y - y * q.x + z * q.w
);
}
Quaternion conjugate() { return new Quaternion(w, -x, -y, -z); }
// 旋转向量
Vec3 rotate(Vec3 v) {
Quaternion qv = new Quaternion(0, v.x, v.y, v.z);
Quaternion result = this.multiply(qv).multiply(this.conjugate());
return new Vec3(result.x, result.y, result.z);
}
// 球面插值Slerp
static Quaternion slerp(Quaternion a, Quaternion b, double t) {
double dot = a.w * b.w + a.x * b.x + a.y * b.y + a.z * b.z;
if (dot < 0) { b = new Quaternion(-b.w, -b.x, -b.y, -b.z); dot = -dot; }
if (dot > 0.9995) {
return new Quaternion(
a.w + t * (b.w - a.w),
a.x + t * (b.x - a.x),
a.y + t * (b.y - a.y),
a.z + t * (b.z - a.z)
).normalize();
}
double theta0 = Math.acos(dot);
double theta = theta0 * t;
double sinTheta = Math.sin(theta);
double sinTheta0 = Math.sin(theta0);
double s0 = Math.cos(theta) - dot * sinTheta / sinTheta0;
double s1 = sinTheta / sinTheta0;
return new Quaternion(
a.w * s0 + b.w * s1,
a.x * s0 + b.x * s1,
a.y * s0 + b.y * s1,
a.z * s0 + b.z * s1
);
}
Quaternion normalize() {
double len = Math.sqrt(w * w + x * x + y * y + z * z);
return new Quaternion(w / len, x / len, y / len, z / len);
}
}
// ==================== AABB包围盒 ====================
static class AABB {
Vec3 min, max;
AABB(Vec3 min, Vec3 max) { this.min = min; this.max = max; }
boolean intersects(AABB other) {
return min.x <= other.max.x && max.x >= other.min.x &&
min.y <= other.max.y && max.y >= other.min.y &&
min.z <= other.max.z && max.z >= other.min.z;
}
// 球体与AABB的最短距离平方
double distanceSqToPoint(Vec3 p) {
double dx = Math.max(min.x - p.x, Math.max(0, p.x - max.x));
double dy = Math.max(min.y - p.y, Math.max(0, p.y - max.y));
double dz = Math.max(min.z - p.z, Math.max(0, p.z - max.z));
return dx * dx + dy * dy + dz * dz;
}
}
// ==================== 道路段 ====================
static class RoadSegment {
Vec3 center; // 道路中心点
Vec3 forward; // 前进方向(切线)
Vec3 up; // 法向(垂直路面向上)
Vec3 right; // 右方向(路面宽度方向)
double width; // 道路宽度
double length; // 段长度
RoadSegment(Vec3 center, Vec3 forward, Vec3 up, double width, double length) {
this.center = center;
this.forward = forward.normalize();
this.up = up.normalize();
this.right = forward.cross(up).normalize();
this.width = width;
this.length = length;
}
// 获取道路表面的世界坐标(横向偏移offset∈[-1,1])
Vec3 getSurfacePoint(double offset) {
return center.add(right.mul(offset * width / 2));
}
}
// ==================== 障碍物 ====================
static class Obstacle {
Vec3 position;
Vec3 size;
AABB bounds;
ObstacleType type;
enum ObstacleType { STATIC, MOVING, ROTATING }
Obstacle(Vec3 pos, Vec3 size, ObstacleType type) {
this.position = pos;
this.size = size;
this.type = type;
Vec3 half = size.mul(0.5);
this.bounds = new AABB(pos.sub(half), pos.add(half));
}
void update(double time) {
if (type == ObstacleType.MOVING) {
position.x += Math.sin(time * 2) * 0.1;
Vec3 half = size.mul(0.5);
bounds = new AABB(position.sub(half), position.add(half));
}
}
}
// ==================== 球体实体 ====================
static class Ball {
Vec3 position = new Vec3(0, 1, 0); // 球心位置(半径1,y=1在地面)
Vec3 velocity = new Vec3(0, 0, 5); // 初始向前速度
double radius = 1.0;
double mass = 1.0;
Quaternion rotation = new Quaternion(1, 0, 0, 0); // 朝向四元数
double rollAngle = 0; // 滚动累积角度(用于计算旋转)
// 碰撞后的无敌帧
double invincibleTimer = 0;
void update(double dt, RoadSegment road, List<Obstacle> obstacles, double time) {
// 1. 输入:左右移动(简化:直接修改横向速度)
// 实际游戏中通过触摸/键盘输入控制横向速度
// 2. 将速度投影到道路表面
Vec3 velOnSurface = projectToSurface(velocity, road.up);
// 3. 更新位置
position = position.add(velOnSurface.mul(dt));
// 4. 重力与法向约束:将y约束在道路表面高度(简化:假设道路在xz平面)
position.y = radius; // 保持在地面上
// 5. 横向边界约束
double lateral = road.right.dot(position.sub(road.center));
if (Math.abs(lateral) > road.width / 2 - radius) {
double clamped = Math.signum(lateral) * (road.width / 2 - radius);
position = road.center.add(road.right.mul(clamped)).add(road.up.mul(radius));
// 撞墙反弹
velocity = velocity.sub(road.right.mul(velocity.dot(road.right) * 2));
}
// 6. 滚动旋转:球体滚动的角速度 = 线速度 / 半径
double speed = velOnSurface.length();
rollAngle += speed * dt / radius;
Vec3 rollAxis = road.right.cross(road.forward).normalize();
Quaternion deltaRot = Quaternion.fromAxisAngle(rollAxis, speed * dt / radius);
rotation = deltaRot.multiply(rotation);
// 7. 碰撞检测
if (invincibleTimer > 0) {
invincibleTimer -= dt;
} else {
for (Obstacle obs : obstacles) {
obs.update(time);
double distSq = obs.bounds.distanceSqToPoint(position);
if (distSq < radius * radius) {
// 碰撞发生!
handleCollision(obs, road);
invincibleTimer = 0.5; // 0.5秒无敌
break;
}
}
}
}
// 将向量投影到平面(去掉法向分量)
Vec3 projectToSurface(Vec3 v, Vec3 normal) {
Vec3 n = normal.normalize();
return v.sub(n.mul(v.dot(n)));
}
void handleCollision(Obstacle obs, RoadSegment road) {
// 简化碰撞响应:向后弹并减速
velocity = velocity.mul(0.5);
velocity = velocity.sub(road.forward.mul(2)); // 向后推
System.out.println("[碰撞] 球体撞击障碍物!速度降至 " + velocity.length());
}
AABB getBounds() {
Vec3 r = new Vec3(radius, radius, radius);
return new AABB(position.sub(r), position.add(r));
}
}
// ==================== 关卡生成器 ====================
static class LevelGenerator {
List<RoadSegment> roadSegments = new ArrayList<>();
List<Obstacle> obstacles = new ArrayList<>();
Random rand = new Random(123);
double totalLength = 0;
void generate(int numSegments) {
Vec3 pos = new Vec3(0, 0, 0);
Vec3 forward = new Vec3(0, 0, 1);
Vec3 up = new Vec3(0, 1, 0);
for (int i = 0; i < numSegments; i++) {
double segLen = 10 + rand.nextDouble() * 10;
double width = 8 + rand.nextDouble() * 4;
// 随机转弯
double turn = (rand.nextDouble() - 0.5) * 0.3; // 小幅度左右弯
Vec3 newForward = new Vec3(
forward.x * Math.cos(turn) - forward.z * Math.sin(turn),
0,
forward.x * Math.sin(turn) + forward.z * Math.cos(turn)
).normalize();
RoadSegment seg = new RoadSegment(pos, newForward, up, width, segLen);
roadSegments.add(seg);
// 在段上生成障碍物
int obsCount = rand.nextInt(3);
for (int j = 0; j < obsCount; j++) {
double t = rand.nextDouble();
Vec3 obsPos = pos.add(newForward.mul(t * segLen));
obsPos = obsPos.add(seg.right.mul((rand.nextDouble() - 0.5) * width * 0.6));
obsPos.y = 1.5; // 障碍物高度
Vec3 obsSize = new Vec3(2 + rand.nextDouble() * 2, 3, 2 + rand.nextDouble() * 2);
Obstacle.ObstacleType type = Obstacle.ObstacleType.values()[rand.nextInt(3)];
obstacles.add(new Obstacle(obsPos, obsSize, type));
}
pos = pos.add(newForward.mul(segLen));
forward = newForward;
totalLength += segLen;
}
}
// 获取球体当前所在的道路段
RoadSegment getCurrentRoad(Vec3 ballPos) {
// 简化:返回最近的段中心
RoadSegment best = roadSegments.get(0);
double bestDist = Double.MAX_VALUE;
for (RoadSegment seg : roadSegments) {
double d = seg.center.sub(ballPos).length();
if (d < bestDist) {
bestDist = d;
best = seg;
}
}
return best;
}
}
// ==================== 主模拟循环 ====================
public static void main(String[] args) {
System.out.println("=== 滚动的天空引擎启动 ===");
LevelGenerator level = new LevelGenerator();
level.generate(50);
System.out.printf("生成 %d 段道路,%d 个障碍物%n", level.roadSegments.size(), level.obstacles.size());
Ball ball = new Ball();
double dt = 1.0 / 60.0;
double time = 0;
System.out.println("\n--- 模拟运行(每30帧输出一次)---");
for (int frame = 0; frame < 1800; frame++) {
RoadSegment currentRoad = level.getCurrentRoad(ball.position);
ball.update(dt, currentRoad, level.obstacles, time);
time += dt;
if (frame % 30 == 0) {
System.out.printf("t=%.2fs 位置=%s 速度=%.2f 旋转角=%.2f°%n",
time, ball.position, ball.velocity.length(), Math.toDegrees(ball.rollAngle));
}
// 终点判定
if (ball.position.z > level.totalLength) {
System.out.println("\n到达终点!");
break;
}
}
System.out.println("\n模拟结束。");
}
}
复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 四元数旋转 | O(1) | O(1) | 固定16次乘加运算 |
| AABB相交测试 | O(1) | O(1) | 三次区间比较 |
| 球-AABB距离 | O(1) | O(1) | 三轴clamp+平方和 |
| 每帧碰撞检测 | O(O) | O(1) | O为可见障碍物数量 |
| 道路段查找 | O(S) | O(S) | S为总段数,可用空间索引优化 |
对于50段道路、约100个障碍物的关卡,每帧处理量在微秒级,完全满足60FPS实时需求。
扩展方向
- 曲面道路:将道路段升级为参数化曲面(如NURBS),实现连续起伏的立体关卡;
- 物理引擎集成:引入刚体动力学,支持弹跳板、加速带等互动元素;
- 空间索引:用八叉树或BVH加速碰撞检测,支持千级障碍物同屏;
- 关卡程序化生成:结合波函数坍缩(WFC)算法,根据音乐节奏自动生成与旋律匹配的道路形状。
滚动的天空将三维几何、物理模拟与音乐节奏融为一体。通过本文的Java实现,读者可以掌握四元数旋转、AABB碰撞检测与动态关卡生成的核心技术,并在此基础上打造更具创意的立体跑酷体验。