每日算法 — 使用java实现滚动的天空:球体运动学与3D平台碰撞检测

引言

滚动的天空(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) 每帧遍历所有平台

扩展方向

  1. 四叉树/八叉树空间分割:大量平台时使用空间索引加速碰撞检测
  2. 连续碰撞检测(CCD):高速运动时防止穿透(tunneling)
  3. 布料模拟:添加飘扬的旗帜或飘带效果增强视觉
  4. 神经网络AI:使用深度强化学习训练能长期生存的滚动小球

总结

本文通过Java实现了滚动的天空的核心物理与算法系统:
– 球体刚体动力学:精确模拟无滑动滚动,平动与转动耦合关系
– AABB/OBB碰撞检测:支持轴对齐和任意旋转的平台碰撞体
– 程序化关卡生成:基于音乐节拍自动生成有节奏感的平台布局
– 预测性AI决策:通过前瞻轨迹模拟评估最优路径,自动躲避障碍

这些技术是3D跑酷游戏、物理引擎和实时碰撞检测系统的基础,广泛应用于游戏开发、机器人仿真和虚拟现实领域。