每日算法 — 使用java实现超级玛丽:平台物理模拟与有限状态机敌人AI

引言

《超级玛丽》(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渲染系统,打造一个完整的平台跳跃游戏。