每日算法 — 使用java实现索尼克:环形跑道物理模拟与动量守恒平台跳跃

游戏简介

索尼克(Sonic)是世嘉的标志性高速平台动作游戏角色,以其极速奔跑和独特的环形跑道(loop-the-loop)关卡设计闻名。与传统平台游戏不同,索尼克的核心乐趣在于动量积累——通过加速坡道获得速度,利用惯性冲过环形和陡坡。本文将用Java实现一个简化版的索尼克物理引擎,重点讲解动量守恒与摩擦力建模、环形跑道向心力判定、以及AABB碰撞检测与平台响应算法。

核心物理模型

坐标系与状态变量

public class SonicPhysics {
    // 位置(像素坐标)
    public double x, y;
    // 速度(像素/帧)
    public double vx, vy;
    // 地面倾角(弧度,0表示水平向右)
    public double groundAngle;
    // 是否在地面
    public boolean onGround;

    // 物理常量
    private static final double GRAVITY = 0.5;        // 重力加速度
    private static final double FRICTION = 0.046875;  // 地面摩擦系数
    private static final double AIR_DRAG = 0.001875;  // 空气阻力
    private static final double ACCEL = 0.046875;     // 地面加速度
    private static final double JUMP_FORCE = 6.5;     // 跳跃初速度
    private static final double MAX_SPEED = 12.0;     // 最大奔跑速度
    private static final double RADIUS_X = 9;         // 水平碰撞半径
    private static final double RADIUS_Y = 19;        // 垂直碰撞半径

    public SonicPhysics(double startX, double startY) {
        this.x = startX;
        this.y = startY;
        this.vx = 0;
        this.vy = 0;
        this.groundAngle = 0;
        this.onGround = false;
    }
}

地面运动与动量守恒

索尼克在地面上的运动受到地面倾角的显著影响。我们将速度分解为沿地面切线方向的分量,重力沿斜面的分量会自然地加速或减速角色。

public class SonicPhysics {
    // ... 状态变量

    /**
     * 地面帧更新:处理地面运动、摩擦力和坡度影响
     */
    public void updateOnGround(boolean inputLeft, boolean inputRight, boolean inputJump, 
                               TerrainMap terrain) {
        // 1. 应用玩家输入加速度
        if (inputRight) {
            vx += ACCEL * Math.cos(groundAngle);
            vy += ACCEL * Math.sin(groundAngle);
        } else if (inputLeft) {
            vx -= ACCEL * Math.cos(groundAngle);
            vy -= ACCEL * Math.sin(groundAngle);
        } else {
            // 无输入时应用摩擦力
            double speed = Math.sqrt(vx * vx + vy * vy);
            if (speed > 0) {
                double frictionFactor = Math.max(0, speed - FRICTION) / speed;
                vx *= frictionFactor;
                vy *= frictionFactor;
            }
        }

        // 2. 应用重力沿斜面的分量
        // 重力在斜面上的分量 = g * sin(angle),方向沿斜面向下
        double gravityAlongSlope = GRAVITY * Math.sin(groundAngle);
        vx += gravityAlongSlope * Math.cos(groundAngle);
        vy += gravityAlongSlope * Math.sin(groundAngle);

        // 3. 限制最大速度
        double speed = Math.sqrt(vx * vx + vy * vy);
        if (speed > MAX_SPEED) {
            double scale = MAX_SPEED / speed;
            vx *= scale;
            vy *= scale;
        }

        // 4. 处理跳跃
        if (inputJump && onGround) {
            // 跳跃速度垂直于地面
            vx -= JUMP_FORCE * Math.sin(groundAngle);
            vy += JUMP_FORCE * Math.cos(groundAngle);
            onGround = false;
        }

        // 5. 应用速度更新位置
        x += vx;
        y += vy;

        // 6. 碰撞检测与地形贴合
        handleGroundCollision(terrain);
    }

    /**
     * 空中帧更新:处理抛物线运动与空气阻力
     */
    public void updateInAir(boolean inputLeft, boolean inputRight, TerrainMap terrain) {
        // 1. 空中仍可进行微小的方向调整
        if (inputRight) vx += ACCEL;
        if (inputLeft) vx -= ACCEL;

        // 2. 应用空气阻力
        vx *= (1 - AIR_DRAG);

        // 3. 应用重力
        vy += GRAVITY;

        // 4. 更新位置
        x += vx;
        y += vy;

        // 5. 碰撞检测
        handleAirCollision(terrain);
    }
}

环形跑道物理判定

环形跑道是索尼克最具标志性的关卡元素。角色能否成功冲过环形,关键在于速度是否足以克服重力产生的向心力需求。

物理原理

对于半径为R的竖直圆环,在最高点不脱离轨道的最小速度为:

v_min = sqrt(g * R)

在简化模型中,我们将环形跑道离散为一系列带有法向量的地面段。当角色在环上运动时,地面法向量持续变化,我们只需确保角色始终被”压向”地面(即地面法向量与角色到环中心的向量方向一致)。

public class LoopSegment {
    public double centerX, centerY;  // 环中心
    public double radius;            // 环半径
    public double startAngle;        // 起始角度
    public double endAngle;          // 结束角度

    /**
     * 获取环上某点的地面角度(切线方向)
     */
    public double getGroundAngle(double angle) {
        // 切线方向 = 角度 + 90度
        return angle + Math.PI / 2;
    }

    /**
     * 获取环上某点的法向量(指向环外侧)
     */
    public double[] getNormal(double angle) {
        return new double[]{Math.cos(angle), Math.sin(angle)};
    }

    /**
     * 判定角色是否在环内且速度足够
     */
    public boolean canStayOnLoop(SonicPhysics sonic) {
        double dx = sonic.x - centerX;
        double dy = sonic.y - centerY;
        double dist = Math.sqrt(dx * dx + dy * dy);

        // 判定角色是否在环的径向范围内
        if (Math.abs(dist - radius) > 5) return false;

        // 计算当前角度
        double angle = Math.atan2(dy, dx);
        if (angle < 0) angle += 2 * Math.PI;
        if (angle < startAngle || angle > endAngle) return false;

        // 计算所需向心力:F = m*v²/R
        double speedSq = sonic.vx * sonic.vx + sonic.vy * sonic.vy;
        double requiredForce = speedSq / radius;
        double gravityForce = GRAVITY;

        // 在环的上半部分(90°-270°),重力帮助提供向心力
        // 在环的下半部分,需要速度本身提供全部向心力
        double angleDeg = Math.toDegrees(angle);
        if (angleDeg > 90 && angleDeg < 270) {
            // 上半环:重力分量辅助
            double gravityAssist = gravityForce * Math.abs(Math.cos(angle));
            return requiredForce + gravityAssist > gravityForce * 0.3;
        } else {
            // 下半环:需要纯速度
            return requiredForce > gravityForce * 0.5;
        }
    }
}

AABB碰撞检测与平台响应

索尼克使用轴对齐包围盒(AABB)进行碰撞检测。由于角色是椭圆形,我们使用不同的水平和垂直半径。

public class CollisionDetector {

    /**
     * AABB碰撞检测
     */
    public static boolean checkAABB(double x1, double y1, double w1, double h1,
                                     double x2, double y2, double w2, double h2) {
        return x1 < x2 + w2 && x1 + w1 > x2 &&
               y1 < y2 + h2 && y1 + h1 > y2;
    }

    /**
     * 扩展的椭圆-AABB检测(简化版)
     * 使用不同半径的AABB近似椭圆
     */
    public static boolean checkEllipseAABB(SonicPhysics sonic, Platform platform) {
        double left = sonic.x - sonic.RADIUS_X;
        double right = sonic.x + sonic.RADIUS_X;
        double top = sonic.y - sonic.RADIUS_Y;
        double bottom = sonic.y + sonic.RADIUS_Y;

        return checkAABB(left, top, right - left, bottom - top,
                        platform.x, platform.y, platform.width, platform.height);
    }
}

public class TerrainMap {
    private List<Platform> platforms;
    private List<LoopSegment> loops;
    private List<Slope> slopes;

    /**
     * 地面碰撞处理:找到角色脚下的地面并贴合
     */
    public void handleGroundCollision(SonicPhysics sonic) {
        sonic.onGround = false;

        // 向下扫描检测地面
        double footX = sonic.x;
        double footY = sonic.y + sonic.RADIUS_Y + 1;

        // 检测平台
        for (Platform p : platforms) {
            if (footX >= p.x && footX <= p.x + p.width &&
                footY >= p.y && sonic.y + sonic.RADIUS_Y <= p.y + p.height) {
                // 贴合到平台顶部
                sonic.y = p.y - sonic.RADIUS_Y;
                sonic.vy = 0;
                sonic.groundAngle = p.angle;
                sonic.onGround = true;
                return;
            }
        }

        // 检测环形跑道
        for (LoopSegment loop : loops) {
            if (loop.canStayOnLoop(sonic)) {
                double dx = sonic.x - loop.centerX;
                double dy = sonic.y - loop.centerY;
                double angle = Math.atan2(dy, dx);
                // 将角色固定到环表面
                sonic.x = loop.centerX + (loop.radius - sonic.RADIUS_Y) * Math.cos(angle);
                sonic.y = loop.centerY + (loop.radius - sonic.RADIUS_Y) * Math.sin(angle);
                sonic.groundAngle = loop.getGroundAngle(angle);
                sonic.onGround = true;

                // 将速度投影到切线方向
                double tangentX = -Math.sin(angle);
                double tangentY = Math.cos(angle);
                double speed = sonic.vx * tangentX + sonic.vy * tangentY;
                sonic.vx = speed * tangentX;
                sonic.vy = speed * tangentY;
                return;
            }
        }

        // 检测斜坡
        for (Slope s : slopes) {
            if (s.contains(footX, footY)) {
                double groundY = s.getYAt(footX);
                sonic.y = groundY - sonic.RADIUS_Y;
                sonic.vy = 0;
                sonic.groundAngle = s.angle;
                sonic.onGround = true;
                return;
            }
        }
    }
}

class Platform {
    double x, y, width, height, angle;

    Platform(double x, double y, double w, double h, double angle) {
        this.x = x; this.y = y;
        this.width = w; this.height = h;
        this.angle = angle;
    }
}

class Slope {
    double x1, y1, x2, y2, angle;

    Slope(double x1, double y1, double x2, double y2) {
        this.x1 = x1; this.y1 = y1;
        this.x2 = x2; this.y2 = y2;
        this.angle = Math.atan2(y2 - y1, x2 - x1);
    }

    boolean contains(double px, double py) {
        return px >= Math.min(x1, x2) && px <= Math.max(x1, x2) &&
               Math.abs(py - getYAt(px)) < 5;
    }

    double getYAt(double px) {
        return y1 + (px - x1) * (y2 - y1) / (x2 - x1);
    }
}

完整游戏循环与渲染

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class SonicGame extends JPanel implements ActionListener, KeyListener {
    private SonicPhysics sonic;
    private TerrainMap terrain;
    private Timer timer;
    private boolean left, right, jump;

    public SonicGame() {
        setPreferredSize(new Dimension(800, 600));
        setBackground(Color.CYAN);

        sonic = new SonicPhysics(100, 300);
        terrain = createSampleTerrain();

        timer = new Timer(16, this); // ~60 FPS
        timer.start();

        addKeyListener(this);
        setFocusable(true);
    }

    private TerrainMap createSampleTerrain() {
        TerrainMap map = new TerrainMap();

        // 地面平台
        map.addPlatform(new Platform(0, 500, 800, 100, 0));

        // 加速斜坡
        map.addSlope(new Slope(200, 500, 350, 400));

        // 环形跑道(中心x=500, y=300, 半径=80)
        map.addLoop(new LoopSegment(500, 300, 80, 0, 2 * Math.PI));

        // 着陆平台
        map.addPlatform(new Platform(600, 400, 200, 20, 0));

        return map;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        // 更新物理
        if (sonic.onGround) {
            sonic.updateOnGround(left, right, jump, terrain);
        } else {
            sonic.updateInAir(left, right, terrain);
        }

        // 边界检查
        if (sonic.x < 0) sonic.x = 0;
        if (sonic.x > 800) sonic.x = 800;
        if (sonic.y > 600) {
            // 掉落重置
            sonic.x = 100;
            sonic.y = 300;
            sonic.vx = 0;
            sonic.vy = 0;
        }

        jump = false; // 跳跃按键只生效一帧
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;

        // 绘制地形
        g2d.setColor(Color.GREEN);
        for (Platform p : terrain.getPlatforms()) {
            g2d.fillRect((int)p.x, (int)p.y, (int)p.width, (int)p.height);
        }

        // 绘制环形跑道
        g2d.setColor(Color.GRAY);
        for (LoopSegment loop : terrain.getLoops()) {
            g2d.drawOval((int)(loop.centerX - loop.radius), 
                        (int)(loop.centerY - loop.radius),
                        (int)(loop.radius * 2), (int)(loop.radius * 2));
        }

        // 绘制索尼克(蓝色椭圆)
        g2d.setColor(Color.BLUE);
        g2d.fillOval((int)(sonic.x - sonic.RADIUS_X), 
                     (int)(sonic.y - sonic.RADIUS_Y),
                     (int)(sonic.RADIUS_X * 2), (int)(sonic.RADIUS_Y * 2));

        // 显示速度
        g2d.setColor(Color.BLACK);
        double speed = Math.sqrt(sonic.vx * sonic.vx + sonic.vy * sonic.vy);
        g2d.drawString(String.format("Speed: %.2f", speed), 10, 20);
        g2d.drawString("On Ground: " + sonic.onGround, 10, 40);
    }

    @Override public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_LEFT -> left = true;
            case KeyEvent.VK_RIGHT -> right = true;
            case KeyEvent.VK_SPACE -> jump = true;
        }
    }

    @Override public void keyReleased(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_LEFT -> left = false;
            case KeyEvent.VK_RIGHT -> right = false;
        }
    }

    @Override public void keyTyped(KeyEvent e) {}

    public static void main(String[] args) {
        JFrame frame = new JFrame("Sonic Physics Demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new SonicGame());
        frame.pack();
        frame.setVisible(true);
    }
}

复杂度分析

操作 时间复杂度 空间复杂度
地面运动更新 O(1) O(1)
空中运动更新 O(1) O(1)
环形判定 O(1) O(1)
AABB碰撞检测 O(P),P为平台数 O(1)
完整帧更新 O(P + L),L为环数 O(1)

总结

本文实现了一个简化版索尼克物理引擎,核心亮点包括:

  • 动量守恒与坡度物理:将速度分解为切线分量,重力沿斜面的自然加速/减速效果使奔跑富有手感
  • 环形跑道判定:基于向心力需求的物理判定,确保角色只有在速度足够时才能冲过环形
  • AABB碰撞检测:使用不同半径的包围盒近似椭圆角色,高效检测与平台、环形、斜坡的碰撞
  • 平台贴合响应:根据碰撞对象类型(平面/环形/斜坡)更新角色的地面角度和速度投影

读者可以进一步扩展,例如添加弹簧、尖刺、加速带等交互元素,或实现更精确的旋转碰撞体检测,打造更完整的平台动作体验。