游戏简介
索尼克(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碰撞检测:使用不同半径的包围盒近似椭圆角色,高效检测与平台、环形、斜坡的碰撞
- 平台贴合响应:根据碰撞对象类型(平面/环形/斜坡)更新角色的地面角度和速度投影
读者可以进一步扩展,例如添加弹簧、尖刺、加速带等交互元素,或实现更精确的旋转碰撞体检测,打造更完整的平台动作体验。