引言
超级玛丽(Super Mario Bros.)是平台跳跃游戏的鼻祖,其敌人AI虽然看似简单,却蕴含经典的状态机设计智慧。栗宝宝(Goomba)的来回巡逻、乌龟(Koopa)的缩壳反弹、食人花的定时伸缩——这些行为都可以通过有限状态机(FSM)精确描述。本文用Java实现超级玛丽的敌人AI系统,结合视线追踪(Line of Sight)检测和行为树(Behavior Tree)组合逻辑,让敌人具备感知玩家、协同追击的智能。
敌人AI架构设计
我们将敌人抽象为三层:
1. 感知层:通过视线追踪和距离检测获取玩家位置
2. 决策层:行为树根据感知信息选择行动(巡逻、追击、攻击、逃跑)
3. 执行层:有限状态机驱动具体动画与物理行为
import java.util.*;
/**
* 超级玛丽敌人AI系统:FSM + 行为树 + 视线追踪
*/
public class MarioEnemyAI {
// ========== 基础数据结构 ==========
static class Vec2 {
double x, y;
Vec2(double x, double y) { this.x = x; this.y = y; }
double distanceTo(Vec2 other) {
return Math.hypot(x - other.x, y - other.y);
}
}
// 地图瓦片:0=空气,1=地面,2=砖块,3=管道
static class TileMap {
int[][] grid;
double tileSize;
int width, height;
TileMap(int w, int h, double tileSize) {
this.width = w; this.height = h; this.tileSize = tileSize;
this.grid = new int[h][w];
}
boolean isSolid(int tx, int ty) {
if (tx < 0 || tx >= width || ty < 0 || ty >= height) return true; // 边界视为 solid
return grid[ty][tx] == 1 || grid[ty][tx] == 2 || grid[ty][tx] == 3;
}
boolean isSolidAt(double x, double y) {
int tx = (int) (x / tileSize);
int ty = (int) (y / tileSize);
return isSolid(tx, ty);
}
/**
* Bresenham视线检测:从(start)到(end)是否有实体阻挡
*/
boolean hasLineOfSight(Vec2 start, Vec2 end) {
double dx = Math.abs(end.x - start.x);
double dy = Math.abs(end.y - start.y);
int steps = (int) Math.max(dx, dy);
if (steps == 0) return true;
double xInc = (end.x - start.x) / steps;
double yInc = (end.y - start.y) / steps;
double x = start.x, y = start.y;
for (int i = 0; i <= steps; i++) {
if (isSolidAt(x, y)) return false;
x += xInc;
y += yInc;
}
return true;
}
}
// 玩家实体
static class Player {
Vec2 pos = new Vec2(0, 0);
Vec2 vel = new Vec2(0, 0);
boolean alive = true;
double width = 0.8, height = 1.6; // 碰撞盒
}
// ========== 有限状态机 ==========
enum EnemyState {
PATROL, // 巡逻:左右来回移动
CHASE, // 追击:向玩家方向移动
ATTACK, // 攻击:近战或投掷
RETREAT, // 撤退:远离玩家
STUNNED, // 受击:缩壳/眩晕
DEAD // 死亡
}
// ========== 敌人基类 ==========
static abstract class Enemy {
Vec2 pos = new Vec2(0, 0);
Vec2 vel = new Vec2(0, 0);
EnemyState state = EnemyState.PATROL;
double speed = 1.0;
double viewRange = 8.0; // 视野范围
double attackRange = 1.5; // 攻击范围
double width = 0.8, height = 0.8;
boolean facingRight = true;
int patrolDir = 1; // 巡逻方向
double stateTimer = 0; // 状态持续时间
boolean onGround = false;
// 行为树节点引用
BehaviorNode behaviorTree;
Enemy(double x, double y) {
this.pos = new Vec2(x, y);
}
/**
* 每帧更新:感知 -> 行为树决策 -> FSM执行
*/
void update(double dt, Player player, TileMap map) {
if (state == EnemyState.DEAD) return;
// 1. 感知层:视线检测
Perception perception = sense(player, map);
// 2. 行为树决策
if (behaviorTree != null) {
behaviorTree.tick(this, perception, dt);
}
// 3. FSM状态执行
executeState(dt, player, map);
// 4. 物理更新
updatePhysics(dt, map);
stateTimer += dt;
}
/**
* 感知:视线追踪 + 距离检测
*/
Perception sense(Player player, TileMap map) {
Perception p = new Perception();
p.playerVisible = false;
p.playerInRange = false;
p.playerInAttackRange = false;
if (!player.alive) return p;
double dist = pos.distanceTo(player.pos);
p.playerDistance = dist;
p.playerDirection = (player.pos.x > pos.x) ? 1 : -1;
if (dist <= viewRange) {
// 视线检测:从敌人中心到玩家中心
Vec2 eyePos = new Vec2(pos.x, pos.y + height * 0.5);
Vec2 playerCenter = new Vec2(player.pos.x, player.pos.y + player.height * 0.5);
p.playerVisible = map.hasLineOfSight(eyePos, playerCenter);
}
p.playerInRange = dist <= viewRange && p.playerVisible;
p.playerInAttackRange = dist <= attackRange;
return p;
}
abstract void executeState(double dt, Player player, TileMap map);
void updatePhysics(double dt, TileMap map) {
// 重力
vel.y -= 20.0 * dt;
// 水平移动
pos.x += vel.x * dt;
// 水平碰撞检测
if (map.isSolidAt(pos.x + width * 0.5, pos.y + height * 0.5)
|| map.isSolidAt(pos.x - width * 0.5, pos.y + height * 0.5)) {
pos.x -= vel.x * dt;
vel.x = 0;
// 碰到墙翻转巡逻方向
if (state == EnemyState.PATROL) {
patrolDir *= -1;
}
}
// 垂直移动
pos.y += vel.y * dt;
onGround = false;
// 脚下碰撞
if (map.isSolidAt(pos.x, pos.y)) {
pos.y -= vel.y * dt;
vel.y = 0;
onGround = true;
}
// 头顶碰撞
if (map.isSolidAt(pos.x, pos.y + height)) {
pos.y -= vel.y * dt;
vel.y = 0;
}
// 更新朝向
if (vel.x > 0.01) facingRight = true;
else if (vel.x < -0.01) facingRight = false;
}
void changeState(EnemyState newState) {
if (state == newState) return;
state = newState;
stateTimer = 0;
onEnterState(newState);
}
abstract void onEnterState(EnemyState state);
}
/**
* 感知数据包
*/
static class Perception {
boolean playerVisible; // 玩家是否在视野内且可见
boolean playerInRange; // 玩家在追击范围内
boolean playerInAttackRange; // 玩家在攻击范围内
double playerDistance; // 与玩家距离
int playerDirection; // 玩家相对方向:1=右,-1=左
}
// ========== 行为树节点 ==========
static abstract class BehaviorNode {
abstract void tick(Enemy enemy, Perception perception, double dt);
}
/**
* 选择器:顺序执行子节点,直到有一个成功
*/
static class Selector extends BehaviorNode {
List<BehaviorNode> children = new ArrayList<>();
Selector(BehaviorNode... nodes) { children.addAll(Arrays.asList(nodes)); }
void tick(Enemy enemy, Perception perception, double dt) {
for (BehaviorNode child : children) {
child.tick(enemy, perception, dt);
if (enemy.state != EnemyState.PATROL) return; // 简化:非巡逻视为"成功"
}
}
}
/**
* 序列:顺序执行所有子节点
*/
static class Sequence extends BehaviorNode {
List<BehaviorNode> children = new ArrayList<>();
Sequence(BehaviorNode... nodes) { children.addAll(Arrays.asList(nodes)); }
void tick(Enemy enemy, Perception perception, double dt) {
for (BehaviorNode child : children) {
child.tick(enemy, perception, dt);
}
}
}
/**
* 条件:玩家在追击范围 -> 切CHASE
*/
static class CanSeePlayer extends BehaviorNode {
void tick(Enemy enemy, Perception p, double dt) {
if (p.playerInRange) {
enemy.changeState(EnemyState.CHASE);
}
}
}
/**
* 条件:玩家在攻击范围 -> 切ATTACK
*/
static class CanAttackPlayer extends BehaviorNode {
void tick(Enemy enemy, Perception p, double dt) {
if (p.playerInAttackRange) {
enemy.changeState(EnemyState.ATTACK);
}
}
}
/**
* 条件:失去玩家视野 -> 回PATROL
*/
static class LostPlayer extends BehaviorNode {
void tick(Enemy enemy, Perception p, double dt) {
if (!p.playerVisible && enemy.state == EnemyState.CHASE) {
enemy.changeState(EnemyState.PATROL);
}
}
}
// ========== 具体敌人:Goomba(栗宝宝) ==========
static class Goomba extends Enemy {
Goomba(double x, double y) {
super(x, y);
this.speed = 1.5;
this.viewRange = 6.0;
this.attackRange = 0.8;
// 行为树:能看到玩家 -> 追击 -> 能攻击 -> 攻击;否则巡逻
this.behaviorTree = new Selector(
new Sequence(new CanAttackPlayer(), new CanSeePlayer()),
new CanSeePlayer(),
new LostPlayer()
);
}
void onEnterState(EnemyState s) {
switch (s) {
case PATROL -> vel.x = patrolDir * speed;
case CHASE -> {} // 在executeState中设置速度
case ATTACK -> vel.x = 0;
case DEAD -> vel.x = 0;
}
}
void executeState(double dt, Player player, TileMap map) {
switch (state) {
case PATROL -> {
vel.x = patrolDir * speed;
// 巡逻到悬崖边翻转(前方下方没有地面)
double checkX = pos.x + patrolDir * (width * 0.5 + 0.1);
if (!map.isSolidAt(checkX, pos.y - 0.1)) {
patrolDir *= -1;
vel.x = patrolDir * speed;
}
}
case CHASE -> {
if (player.pos.x > pos.x + 0.2) {
vel.x = speed * 1.5;
} else if (player.pos.x < pos.x - 0.2) {
vel.x = -speed * 1.5;
} else {
vel.x = 0;
}
}
case ATTACK -> {
// 近战伤害判定(简化)
vel.x = 0;
if (stateTimer > 0.3) {
changeState(EnemyState.CHASE);
}
}
case STUNNED -> {
vel.x = 0;
if (stateTimer > 2.0) {
changeState(EnemyState.PATROL);
}
}
}
}
}
// ========== 具体敌人:Koopa(乌龟) ==========
static class Koopa extends Enemy {
boolean shellMode = false;
Koopa(double x, double y) {
super(x, y);
this.speed = 1.0;
this.viewRange = 10.0;
this.attackRange = 1.0;
this.behaviorTree = new Selector(
new Sequence(new CanAttackPlayer(), new CanSeePlayer()),
new CanSeePlayer(),
new LostPlayer()
);
}
void onEnterState(EnemyState s) {
switch (s) {
case PATROL -> { if (!shellMode) vel.x = patrolDir * speed; }
case STUNNED -> { shellMode = true; vel.x = 0; }
case CHASE -> { if (shellMode) vel.x = (playerDirection() * speed * 3); }
}
}
int playerDirection() {
return 1; // 简化
}
void executeState(double dt, Player player, TileMap map) {
switch (state) {
case PATROL -> {
if (!shellMode) {
vel.x = patrolDir * speed;
double checkX = pos.x + patrolDir * (width * 0.5 + 0.1);
if (!map.isSolidAt(checkX, pos.y - 0.1)) {
patrolDir *= -1;
vel.x = patrolDir * speed;
}
} else {
vel.x = 0;
if (stateTimer > 5.0) {
shellMode = false;
changeState(EnemyState.PATROL);
}
}
}
case CHASE -> {
if (shellMode) {
// 缩壳后高速滑行追击
vel.x = (player.pos.x > pos.x ? 1 : -1) * speed * 3;
} else {
vel.x = (player.pos.x > pos.x ? 1 : -1) * speed * 1.2;
}
}
case STUNNED -> {
vel.x = 0;
if (stateTimer > 3.0) {
shellMode = false;
changeState(EnemyState.PATROL);
}
}
}
}
}
// ========== 碰撞与交互 ==========
static boolean checkAABB(Enemy e, Player p) {
return Math.abs(e.pos.x - p.pos.x) < (e.width + p.width) * 0.5
&& Math.abs(e.pos.y - p.pos.y) < (e.height + p.height) * 0.5;
}
/**
* 处理玩家踩踏敌人
*/
static void handlePlayerStomp(Enemy e, Player p) {
if (e.state == EnemyState.DEAD) return;
// 玩家下落且位于敌人上方
if (p.vel.y < 0 && p.pos.y > e.pos.y + e.height * 0.3) {
if (e instanceof Goomba) {
e.changeState(EnemyState.DEAD);
} else if (e instanceof Koopa) {
if (!((Koopa) e).shellMode) {
e.changeState(EnemyState.STUNNED);
} else {
// 再次踩踏:踢飞缩壳乌龟
e.changeState(EnemyState.CHASE);
}
}
p.vel.y = 8.0; // 玩家弹跳
}
}
// ========== 主程序演示 ==========
public static void main(String[] args) {
// 创建简单地图:地面在y=0,中间有平台
TileMap map = new TileMap(40, 10, 1.0);
for (int x = 0; x < 40; x++) map.grid[0][x] = 1; // 地面
for (int x = 10; x < 15; x++) map.grid[3][x] = 2; // 平台
for (int x = 25; x < 30; x++) map.grid[5][x] = 2; // 平台
map.grid[0][20] = 3; // 管道
map.grid[1][20] = 3;
Player player = new Player();
player.pos = new Vec2(5, 2);
List<Enemy> enemies = new ArrayList<>();
enemies.add(new Goomba(15, 2));
enemies.add(new Goomba(28, 6));
enemies.add(new Koopa(8, 2));
System.out.println("=== 超级玛丽敌人AI模拟 ===\n");
double dt = 0.05;
for (double t = 0; t < 10.0; t += dt) {
// 模拟玩家移动:向右走然后停下
if (t < 4.0) player.pos.x += 1.5 * dt;
else if (t > 6.0 && t < 8.0) player.pos.x -= 1.0 * dt;
// 玩家重力
player.vel.y -= 20.0 * dt;
player.pos.y += player.vel.y * dt;
if (map.isSolidAt(player.pos.x, player.pos.y)) {
player.pos.y -= player.vel.y * dt;
player.vel.y = 0;
}
for (Enemy e : enemies) {
EnemyState oldState = e.state;
e.update(dt, player, map);
if (e.state != oldState) {
String name = (e instanceof Goomba) ? "Goomba" : "Koopa";
System.out.printf("[t=%.2f] %s@%s 状态: %s -> %s%n",
t, name, fmt(e.pos), oldState, e.state);
}
if (checkAABB(e, player)) {
handlePlayerStomp(e, player);
}
}
// 每2秒输出一次位置快照
if (Math.abs(t % 2.0) < dt) {
System.out.printf("\n--- t=%.1f 快照 ---%n", t);
System.out.printf("Player@%s%n", fmt(player.pos));
for (Enemy e : enemies) {
String name = (e instanceof Goomba) ? "Goomba" : "Koopa";
System.out.printf(" %s@%s state=%s vel=%.1f%n",
name, fmt(e.pos), e.state, e.vel.x);
}
}
}
// 视线检测测试
System.out.println("\n=== 视线检测测试 ===");
Vec2 a = new Vec2(5, 2);
Vec2 b = new Vec2(18, 2);
System.out.println("A(5,2) -> B(18,2) 视线通畅? " + map.hasLineOfSight(a, b));
Vec2 c = new Vec2(22, 2);
System.out.println("A(5,2) -> C(22,2) 视线通畅? " + map.hasLineOfSight(a, c) + " (管道阻挡)");
// 性能测试
System.out.println("\n=== 性能测试 ===");
List<Enemy> perfEnemies = new ArrayList<>();
for (int i = 0; i < 100; i++) {
perfEnemies.add(new Goomba(i % 40, 2));
}
long start = System.nanoTime();
for (int frame = 0; frame < 10000; frame++) {
for (Enemy e : perfEnemies) {
e.update(0.016, player, map);
}
}
long ns = System.nanoTime() - start;
System.out.println("100敌人×10000帧 平均: " + (ns / 10000.0 / 1000) + " μs/帧");
}
static String fmt(Vec2 v) {
return String.format("(%.1f,%.1f)", v.x, v.y);
}
}
有限状态机与行为树的协作
本文将FSM与行为树分层使用:
– 行为树负责高层策略选择(追击?攻击?撤退?),它的优势在于模块化组合
– FSM负责底层动画与物理状态转换(巡逻时左右移动、追击时加速、受击时缩壳),保证状态切换的严格时序
两者通过changeState桥接:行为树的条件节点调用changeState设置目标状态,FSM的executeState负责具体执行。
视线追踪与Bresenham算法
敌人只有”看到”玩家才会追击。本文使用Bresenham风格的离散射线检测:将起点到终点的线段等分为max(dx,dy)步,每步检测所在瓦片是否为实体。这比连续射线计算更快,且天然适配瓦片地图。
若射线被管道、砖块等阻挡,即使玩家在视野半径内,敌人也保持巡逻状态——这创造了”躲猫猫”的战术空间。
悬崖检测与智能巡逻
Goomba的经典行为是走到悬崖边自动转身。本文通过检测”前方水平位置的正下方是否有地面”实现:
if (!map.isSolidAt(checkX, pos.y - 0.1)) patrolDir *= -1;
这赋予了敌人基本的”求生本能”,不会主动跳崖(除非被缩壳乌龟撞击)。
复杂度分析
| 模块 | 时间复杂度 | 说明 |
|---|---|---|
| 视线检测 | O(d) | d为像素距离,最多遍历地图宽度 |
| 行为树遍历 | O(n) | n为树节点数,通常<10 |
| FSM执行 | O(1) | 状态数固定 |
| AABB碰撞 | O(1) | 每对实体 |
| 100敌人一帧 | O(100·d) | 约0.1ms |
总结
本文用Java实现了超级玛丽的分层AI架构:行为树处理策略决策,有限状态机驱动行为执行,Bresenham视线追踪提供感知能力。Goomba的悬崖感知、Koopa的缩壳状态机都忠实还原了原版游戏的设计智慧。这套架构可扩展至更复杂的平台游戏,支持跳跃追击、投掷道具、群体协同等高级行为。