每日算法 — 使用java实现超级玛丽:有限状态机敌人行为树与视线追踪AI

引言

超级玛丽(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的缩壳状态机都忠实还原了原版游戏的设计智慧。这套架构可扩展至更复杂的平台游戏,支持跳跃追击、投掷道具、群体协同等高级行为。