引言
《超级玛丽》(Super Mario Bros.)是电子游戏史上的里程碑之作,其精妙的设计不仅体现在关卡设计上,更在于底层物理引擎和敌人AI的优雅实现。本文将用Java实现一个简化版超级玛丽的核心算法框架,重点讲解平台物理模拟(重力、跳跃、碰撞)、AABB平台碰撞检测,以及有限状态机(FSM)驱动的敌人AI行为系统。
核心数据结构:游戏实体与物理属性
import java.util.*;
/**
* 二维向量,用于位置、速度、加速度
*/
public class Vector2 {
public double x, y;
public Vector2(double x, double y) {
this.x = x;
this.y = y;
}
public Vector2 add(Vector2 v) { return new Vector2(x + v.x, y + v.y); }
public Vector2 sub(Vector2 v) { return new Vector2(x - v.x, y - v.y); }
public Vector2 mul(double s) { return new Vector2(x * s, y * s); }
@Override
public String toString() {
return String.format("(%.2f, %.2f)", x, y);
}
}
/**
* AABB碰撞盒
*/
public class BoundingBox {
public double x, y, width, height;
public BoundingBox(double x, double y, double w, double h) {
this.x = x; this.y = y; this.width = w; this.height = h;
}
public double left() { return x; }
public double right() { return x + width; }
public double top() { return y; }
public double bottom() { return y + height; }
public double centerX(){ return x + width / 2; }
public double centerY(){ return y + height / 2; }
public boolean intersects(BoundingBox other) {
return this.left() < other.right() && this.right() > other.left()
&& this.top() < other.bottom() && this.bottom() > other.top();
}
/**
* 计算两个AABB的MTV(最小平移向量)
* 用于将重叠的物体分离
*/
public Vector2 getOverlap(BoundingBox other) {
double overlapX = Math.min(this.right(), other.right()) -
Math.max(this.left(), other.left());
double overlapY = Math.min(this.bottom(), other.bottom()) -
Math.max(this.top(), other.top());
return new Vector2(overlapX, overlapY);
}
}
核心算法一:平台物理引擎
超级玛丽的物理系统是其手感的灵魂所在。我们实现一个包含重力、跳跃、地面摩擦和平台碰撞的物理引擎。
/**
* 物理参数常量
*/
public class PhysicsConstants {
static final double GRAVITY = 0.6; // 重力加速度
static final double MAX_FALL_SPEED = 12.0; // 最大下落速度
static final double FRICTION = 0.85; // 地面摩擦系数
static final double AIR_RESISTANCE = 0.98; // 空气阻力
static final double JUMP_FORCE = -12.0; // 起跳初速度(向上为负)
static final double MOVE_ACCEL = 0.5; // 水平移动加速度
static final double MAX_RUN_SPEED = 5.0; // 最大奔跑速度
static final double MAX_SPRINT_SPEED = 8.0;// 最大冲刺速度
}
/**
* 玛丽角色控制器
*/
public class MarioController {
Vector2 position; // 位置
Vector2 velocity; // 速度
Vector2 acceleration; // 加速度
BoundingBox bounds; // 碰撞盒
boolean onGround; // 是否在地面上
boolean isSprinting; // 是否冲刺中
boolean isBig; // 是否大玛丽(吃了蘑菇)
int width, height; // 尺寸
public MarioController(double x, double y) {
this.position = new Vector2(x, y);
this.velocity = new Vector2(0, 0);
this.acceleration = new Vector2(0, 0);
this.width = 32; this.height = 32;
this.bounds = new BoundingBox(x, y, width, height);
this.onGround = false;
this.isBig = false;
}
/**
* 每帧物理更新
* @param input 输入状态 [left, right, jump]
* @param platforms 所有平台碰撞盒
* @param dt 时间步长(帧间隔)
*/
public void update(boolean[] input, List<BoundingBox> platforms, double dt) {
// 1. 处理水平输入
boolean left = input[0], right = input[1], jump = input[2];
double targetSpeed = isSprinting ? PhysicsConstants.MAX_SPRINT_SPEED
: PhysicsConstants.MAX_RUN_SPEED;
if (left) {
acceleration.x = -PhysicsConstants.MOVE_ACCEL;
} else if (right) {
acceleration.x = PhysicsConstants.MOVE_ACCEL;
} else {
acceleration.x = 0;
// 无输入时应用摩擦/阻力减速
if (onGround) {
velocity.x *= PhysicsConstants.FRICTION;
} else {
velocity.x *= PhysicsConstants.AIR_RESISTANCE;
}
}
// 2. 应用重力
acceleration.y = PhysicsConstants.GRAVITY;
// 3. 更新速度
velocity.x += acceleration.x * dt;
velocity.y += acceleration.y * dt;
// 限制最大速度
velocity.x = Math.max(-targetSpeed, Math.min(targetSpeed, velocity.x));
velocity.y = Math.min(velocity.y, PhysicsConstants.MAX_FALL_SPEED);
// 4. 跳跃处理:只有在地面时才能起跳
if (jump && onGround) {
velocity.y = PhysicsConstants.JUMP_FORCE;
onGround = false;
}
// 5. 根据速度更新位置
position.x += velocity.x * dt;
position.y += velocity.y * dt;
// 6. 更新碰撞盒
bounds.x = position.x;
bounds.y = position.y;
// 7. 平台碰撞检测与响应
onGround = false;
for (BoundingBox platform : platforms) {
resolvePlatformCollision(platform);
}
}
/**
* 玛丽与单个平台的碰撞解析
* 核心逻辑:根据碰撞方向和相对速度判断碰撞面
*/
private void resolvePlatformCollision(BoundingBox platform) {
if (!bounds.intersects(platform)) return;
Vector2 overlap = bounds.getOverlap(platform);
double prevY = position.y - velocity.y; // 上一帧的Y位置
double prevBottom = prevY + height;
// 判断碰撞方向:基于上一帧位置进行推断
if (prevBottom <= platform.top() + 2 && velocity.y >= 0) {
// 从上方落下:站在平台上
position.y = platform.top() - height;
velocity.y = 0;
onGround = true;
} else if (prevY >= platform.bottom() - 2 && velocity.y < 0) {
// 从下方跳上去撞到:头顶碰撞
position.y = platform.bottom();
velocity.y = 0;
} else {
// 水平碰撞
if (position.x + width / 2 < platform.centerX()) {
// 从左侧碰撞
position.x = platform.left() - width;
velocity.x = 0;
} else {
// 从右侧碰撞
position.x = platform.right();
velocity.x = 0;
}
}
// 更新碰撞盒
bounds.x = position.x;
bounds.y = position.y;
}
public void grow() {
if (!isBig) {
isBig = true;
height = 64; // 变大
bounds.height = height;
position.y -= 32; // 向上扩展
}
}
public void shrink() {
if (isBig) {
isBig = false;
height = 32;
bounds.height = height;
}
}
}
核心算法二:有限状态机敌人AI
超级玛丽中的敌人(如板栗仔Goomba、乌龟Koopa)行为简单但富有特色。我们使用有限状态机(FSM)来建模敌人的行为模式。
/**
* 敌人AI的有限状态机
*/
public class EnemyFSM {
/**
* 敌人状态枚举
*/
enum State {
PATROL, // 巡逻:在平台上左右移动
CHASE, // 追击:发现玩家后向玩家方向移动
SHELL, // 缩壳(仅Koopa):被踩后变为壳
SLIDE, // 滑壳:被踢后壳滑行
DYING, // 死亡:被击中后的动画
FALLING // 掉落:走出平台边缘
}
Vector2 position;
Vector2 velocity;
BoundingBox bounds;
State currentState;
State previousState;
double patrolSpeed;
double chaseSpeed;
double detectRange; // 发现玩家的距离
int direction; // 移动方向:1=右,-1=左
boolean isShell; // 是否为壳形态
double stateTimer; // 状态计时器
double shellTimer; // 缩壳持续时间
public EnemyFSM(double x, double y, boolean isKoopa) {
this.position = new Vector2(x, y);
this.velocity = new Vector2(0, 0);
this.bounds = new BoundingBox(x, y, 32, 32);
this.currentState = State.PATROL;
this.patrolSpeed = 1.0;
this.chaseSpeed = 2.5;
this.detectRange = 150.0;
this.direction = -1; // 初始向左
this.isShell = false;
this.stateTimer = 0;
this.shellTimer = 0;
}
/**
* FSM状态更新主函数
*/
public void update(BoundingBox playerBounds,
List<BoundingBox> platforms, double dt) {
stateTimer += dt;
// 执行当前状态的行为
switch (currentState) {
case PATROL:
updatePatrol(playerBounds, platforms, dt);
break;
case CHASE:
updateChase(playerBounds, platforms, dt);
break;
case SHELL:
updateShell(dt);
break;
case SLIDE:
updateSlide(platforms, dt);
break;
case FALLING:
updateFalling(platforms, dt);
break;
case DYING:
// 死亡动画持续一段时间后移除
if (stateTimer > 0.5) {
// markForRemoval = true;
}
break;
}
// 通用物理:重力
velocity.y += PhysicsConstants.GRAVITY * dt;
velocity.y = Math.min(velocity.y, PhysicsConstants.MAX_FALL_SPEED);
position.x += velocity.x * dt;
position.y += velocity.y * dt;
bounds.x = position.x;
bounds.y = position.y;
}
/**
* 巡逻状态:在平台上来回走动,走到边缘或遇到障碍时转向
*/
private void updatePatrol(BoundingBox player, List<BoundingBox> platforms, double dt) {
velocity.x = direction * patrolSpeed;
// 检测前方是否有平台(防止掉落)
boolean hasGroundAhead = checkGroundAhead(platforms);
if (!hasGroundAhead) {
direction *= -1; // 转向
velocity.x = direction * patrolSpeed;
}
// 检测是否遇到障碍物
for (BoundingBox platform : platforms) {
if (willHitWall(platform)) {
direction *= -1;
velocity.x = direction * patrolSpeed;
break;
}
}
// 状态转换:检测到玩家进入范围 → 追击
double distToPlayer = Math.abs(player.centerX() - bounds.centerX());
if (distToPlayer < detectRange && sameLevel(player)) {
transitionTo(State.CHASE);
}
}
/**
* 追击状态:向玩家水平方向移动
*/
private void updateChase(BoundingBox player, List<BoundingBox> platforms, double dt) {
double dx = player.centerX() - bounds.centerX();
direction = dx > 0 ? 1 : -1;
velocity.x = direction * chaseSpeed;
// 检测前方平台边缘,防止追击时掉落
boolean hasGroundAhead = checkGroundAhead(platforms);
if (!hasGroundAhead) {
// 到达边缘,退回巡逻状态
transitionTo(State.PATROL);
}
// 玩家远离后回到巡逻
double distToPlayer = Math.abs(dx);
if (distToPlayer > detectRange * 1.5) {
transitionTo(State.PATROL);
}
}
/**
* 缩壳状态:静止不动,一段时间后恢复
*/
private void updateShell(double dt) {
velocity.x = 0;
shellTimer += dt;
if (shellTimer > 5.0) {
// 5秒后恢复
isShell = false;
shellTimer = 0;
transitionTo(State.PATROL);
}
}
/**
* 滑壳状态:高速滑动,消灭路径上的敌人
*/
private void updateSlide(List<BoundingBox> platforms, double dt) {
velocity.x = direction * chaseSpeed * 2;
// 检测墙壁碰撞反弹
for (BoundingBox platform : platforms) {
if (willHitWall(platform)) {
direction *= -1;
velocity.x = direction * chaseSpeed * 2;
break;
}
}
// 检测前方平台边缘,壳会掉落
boolean hasGroundAhead = checkGroundAhead(platforms);
if (!hasGroundAhead) {
transitionTo(State.FALLING);
}
}
/**
* 掉落状态:走出平台后的自然下落
*/
private void updateFalling(List<BoundingBox> platforms, double dt) {
// 如果重新落到地面,回到巡逻
for (BoundingBox platform : platforms) {
if (bounds.intersects(platform)) {
if (velocity.y > 0 && bounds.bottom() <= platform.top() + 5) {
position.y = platform.top() - bounds.height;
velocity.y = 0;
transitionTo(State.PATROL);
return;
}
}
}
}
/**
* 检测前方是否有地面(防止掉落)
*/
private boolean checkGroundAhead(List<BoundingBox> platforms) {
// 在敌人前方下方投射一个检测盒
double checkX = position.x + (direction > 0 ? bounds.width + 2 : -2);
double checkY = position.y + bounds.height + 5;
BoundingBox checkBox = new BoundingBox(checkX, checkY, 2, 2);
for (BoundingBox platform : platforms) {
if (checkBox.intersects(platform)) return true;
}
return false;
}
/**
* 检测是否会撞到墙壁
*/
private boolean willHitWall(BoundingBox platform) {
double futureX = position.x + velocity.x + (direction > 0 ? bounds.width : 0);
return futureX >= platform.left() && futureX <= platform.right()
&& bounds.top() < platform.bottom() && bounds.bottom() > platform.top();
}
/**
* 判断是否与玩家在同一水平层
*/
private boolean sameLevel(BoundingBox player) {
return Math.abs(player.centerY() - bounds.centerY()) < 64;
}
/**
* 状态转换
*/
private void transitionTo(State newState) {
previousState = currentState;
currentState = newState;
stateTimer = 0;
// 状态进入时的初始化
if (newState == State.SHELL) {
shellTimer = 0;
isShell = true;
}
}
/**
* 被踩踏时的处理
*/
public void onStomped() {
if (currentState == State.SHELL) {
// 已经是壳,被踢后滑行
transitionTo(State.SLIDE);
} else {
// 变成壳
transitionTo(State.SHELL);
}
}
/**
* 被火球击中
*/
public void onFireballHit() {
velocity.y = -5; // 向上弹起
transitionTo(State.DYING);
}
}
核心算法三:碰撞检测与交互系统
/**
* 游戏主逻辑:处理所有实体间的碰撞和交互
*/
public class GameWorld {
MarioController mario;
List<EnemyFSM> enemies;
List<BoundingBox> platforms;
List<PowerUp> powerUps;
public GameWorld() {
this.mario = new MarioController(100, 300);
this.enemies = new ArrayList<>();
this.platforms = new ArrayList<>();
this.powerUps = new ArrayList<>();
}
/**
* 主更新循环
*/
public void update(boolean[] input, double dt) {
// 1. 更新玛丽
mario.update(input, platforms, dt);
// 2. 更新所有敌人
for (EnemyFSM enemy : enemies) {
enemy.update(mario.bounds, platforms, dt);
}
// 3. 检测玛丽与敌人的碰撞
for (EnemyFSM enemy : enemies) {
if (!mario.bounds.intersects(enemy.bounds)) continue;
// 判断是踩踏还是撞上去
double marioBottom = mario.bounds.bottom();
double enemyTop = enemy.bounds.top();
boolean isStomp = mario.velocity.y > 0 &&
marioBottom <= enemyTop + 10;
if (isStomp) {
enemy.onStomped();
mario.velocity.y = PhysicsConstants.JUMP_FORCE * 0.5; // 小跳
} else if (enemy.currentState != EnemyFSM.State.SHELL &&
enemy.currentState != EnemyFSM.State.DYING) {
// 被敌人撞到
if (mario.isBig) {
mario.shrink();
} else {
// 玛丽死亡
System.out.println("玛丽死亡!");
}
}
}
// 4. 检测与道具的碰撞
Iterator<PowerUp> it = powerUps.iterator();
while (it.hasNext()) {
PowerUp pu = it.next();
pu.update(platforms, dt);
if (mario.bounds.intersects(pu.bounds)) {
pu.apply(mario);
it.remove();
}
}
// 5. 掉落检测
if (mario.position.y > 600) {
System.out.println("玛丽掉落深渊!");
}
}
/**
* 道具基类
*/
static class PowerUp {
Vector2 position;
Vector2 velocity;
BoundingBox bounds;
void update(List<BoundingBox> platforms, double dt) {
velocity.y += PhysicsConstants.GRAVITY * dt;
position.x += velocity.x * dt;
position.y += velocity.y * dt;
bounds.x = position.x;
bounds.y = position.y;
// 平台碰撞
for (BoundingBox p : platforms) {
if (bounds.intersects(p) && velocity.y > 0) {
position.y = p.top() - bounds.height;
velocity.y = 0;
}
}
}
void apply(MarioController mario) {}
}
static class Mushroom extends PowerUp {
Mushroom(double x, double y) {
this.position = new Vector2(x, y);
this.velocity = new Vector2(1.0, 0);
this.bounds = new BoundingBox(x, y, 32, 32);
}
@Override
void apply(MarioController mario) { mario.grow(); }
}
static class Star extends PowerUp {
Star(double x, double y) {
this.position = new Vector2(x, y);
this.velocity = new Vector2(2.0, -5.0);
this.bounds = new BoundingBox(x, y, 32, 32);
}
@Override
void apply(MarioController mario) {
// 无敌状态
System.out.println("无敌星星!");
}
}
}
关卡数据表示与加载
/**
* 简单的关卡文本格式解析器
* 字符映射:#=砖块, ?=金币块, M=蘑菇, S=星星, G=板栗仔, K=乌龟
*/
public class LevelLoader {
public static void loadLevel(String[] levelData, GameWorld world) {
int tileSize = 32;
for (int row = 0; row < levelData.length; row++) {
String line = levelData[row];
for (int col = 0; col < line.length(); col++) {
char c = line.charAt(col);
double x = col * tileSize;
double y = row * tileSize;
switch (c) {
case '#':
world.platforms.add(new BoundingBox(x, y, tileSize, tileSize));
break;
case '?':
world.platforms.add(new BoundingBox(x, y, tileSize, tileSize));
// 标记为问号块,顶击后产生道具
break;
case 'M':
world.powerUps.add(new GameWorld.Mushroom(x, y));
break;
case 'S':
world.powerUps.add(new GameWorld.Star(x, y));
break;
case 'G':
world.enemies.add(new EnemyFSM(x, y, false));
break;
case 'K':
world.enemies.add(new EnemyFSM(x, y, true));
break;
}
}
}
}
}
复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 物理更新 | O(P) | O(1) | P=平台数量,每帧遍历检测 |
| 敌人AI | O(E × P) | O(E) | E=敌人数量 |
| AABB碰撞 | O(1) | O(1) | 单次碰撞检测为常数 |
| FSM状态机 | O(1) | O(1) | 状态数为常数 |
| 关卡加载 | O(W × H) | O(W × H) | W,H为关卡宽高 |
总结
本文实现了一个简化版超级玛丽的完整算法框架:
– 平台物理:重力、跳跃、摩擦、碰撞响应的核心逻辑
– AABB碰撞:基于前一帧位置推断碰撞方向的鲁棒方法
– 有限状态机:敌人从巡逻→追击→缩壳→滑行的完整行为树
– 碰撞交互:踩踏判定、道具收集、伤害系统的精确实现
读者可以在此基础上扩展更多敌人种类(如飞行敌人、Boss)、更复杂的关卡机制(如移动平台、水管传送),以及TileMap渲染系统,打造一个完整的平台跳跃游戏。