每日算法 — 使用java实现太空侵略者:有限状态机敌人AI与弹幕躲避策略

引言

太空侵略者(Space Invaders)是街机射击游戏的鼻祖。其核心算法包括:敌人编队的阵型移动、有限状态机驱动的行为决策、弹幕生成模式,以及玩家的自动躲避策略。本文用Java实现完整的敌人AI系统与智能躲避算法。

核心算法设计

1. 敌人有限状态机(FSM)

每个敌人有多个状态:巡逻、瞄准、射击、撤退。状态转移由游戏条件触发。

import java.util.*;

/**
 * 敌人状态枚举
 */
enum EnemyState {
    PATROL,    // 巡逻:随编队左右移动
    AIMING,    // 瞄准:暂停移动,锁定玩家
    SHOOTING,  // 射击:发射弹幕
    RETREAT,   // 撤退:向下移动并加速
    DIVE       // 俯冲:脱离编队冲向玩家
}

/**
 * 状态转移条件
 */
interface StateTransition {
    boolean canTransition(EnemyAI enemy, GameContext ctx);
    EnemyState targetState();
}

/**
 * 游戏上下文:提供状态机决策所需的全局信息
 */
class GameContext {
    double playerX, playerY;    // 玩家位置
    List<EnemyAI> allEnemies;   // 所有敌人
    int frameCount;             // 当前帧数
    double difficulty;          // 难度系数(随关卡提升)

    public GameContext(double px, double py, List<EnemyAI> enemies, int frame, double diff) {
        this.playerX = px; this.playerY = py;
        this.allEnemies = enemies;
        this.frameCount = frame;
        this.difficulty = diff;
    }
}

/**
 * 单个敌人的AI控制器
 */
class EnemyAI {
    double x, y;           // 位置
    double speed;          // 移动速度
    EnemyState state;      // 当前状态
    int stateTimer;        // 状态已持续帧数
    double fireCooldown;   // 射击冷却
    boolean isAlive;       // 是否存活
    Random rand;           // 随机数生成器

    // 状态转移表:每个状态对应一组可转移的条件
    Map<EnemyState, List<StateTransition>> transitions;

    public EnemyAI(double x, double y, double speed, long seed) {
        this.x = x; this.y = y;
        this.speed = speed;
        this.state = EnemyState.PATROL;
        this.stateTimer = 0;
        this.fireCooldown = 0;
        this.isAlive = true;
        this.rand = new Random(seed);
        initTransitions();
    }

    private void initTransitions() {
        transitions = new HashMap<>();

        // PATROL -> AIMING:随机概率触发,难度越高概率越大
        transitions.put(EnemyState.PATROL, Arrays.asList(
            new StateTransition() {
                public boolean canTransition(EnemyAI e, GameContext ctx) {
                    return e.stateTimer > 30 && e.rand.nextDouble() < 0.02 * ctx.difficulty;
                }
                public EnemyState targetState() { return EnemyState.AIMING; }
            },
            // PATROL -> DIVE:玩家靠近时俯冲
            new StateTransition() {
                public boolean canTransition(EnemyAI e, GameContext ctx) {
                    return Math.abs(e.x - ctx.playerX) < 30 && e.rand.nextDouble() < 0.1;
                }
                public EnemyState targetState() { return EnemyState.DIVE; }
            }
        ));

        // AIMING -> SHOOTING:瞄准足够时间后射击
        transitions.put(EnemyState.AIMING, Arrays.asList(
            new StateTransition() {
                public boolean canTransition(EnemyAI e, GameContext ctx) {
                    return e.stateTimer > 20 && Math.abs(e.x - ctx.playerX) < 50;
                }
                public EnemyState targetState() { return EnemyState.SHOOTING; }
            },
            // AIMING -> PATROL:玩家跑太远,放弃瞄准
            new StateTransition() {
                public boolean canTransition(EnemyAI e, GameContext ctx) {
                    return e.stateTimer > 60;
                }
                public EnemyState targetState() { return EnemyState.PATROL; }
            }
        ));

        // SHOOTING -> RETREAT:射击后撤退
        transitions.put(EnemyState.SHOOTING, Arrays.asList(
            new StateTransition() {
                public boolean canTransition(EnemyAI e, GameContext ctx) {
                    return e.stateTimer > 10;
                }
                public EnemyState targetState() { return EnemyState.RETREAT; }
            }
        ));

        // RETREAT -> PATROL:撤退一段时间后恢复巡逻
        transitions.put(EnemyState.RETREAT, Arrays.asList(
            new StateTransition() {
                public boolean canTransition(EnemyAI e, GameContext ctx) {
                    return e.stateTimer > 40;
                }
                public EnemyState targetState() { return EnemyState.PATROL; }
            }
        ));

        // DIVE -> PATROL:俯冲到底部后恢复
        transitions.put(EnemyState.DIVE, Arrays.asList(
            new StateTransition() {
                public boolean canTransition(EnemyAI e, GameContext ctx) {
                    return e.y < 100 || e.stateTimer > 100; // 到达底部或超时
                }
                public EnemyState targetState() { return EnemyState.PATROL; }
            }
        ));
    }

    /**
     * 每帧更新:状态转移 + 行为执行
     */
    public void update(GameContext ctx) {
        if (!isAlive) return;
        stateTimer++;
        fireCooldown = Math.max(0, fireCooldown - 1);

        // 检查状态转移
        List<StateTransition> possible = transitions.getOrDefault(state, Collections.emptyList());
        for (StateTransition t : possible) {
            if (t.canTransition(this, ctx)) {
                state = t.targetState();
                stateTimer = 0;
                break;
            }
        }

        // 执行当前状态的行为
        executeStateBehavior(ctx);
    }

    private void executeStateBehavior(GameContext ctx) {
        switch (state) {
            case PATROL:
                // 随编队左右移动,碰到边界向下
                x += speed * Math.sin(ctx.frameCount * 0.02);
                break;
            case AIMING:
                // 微调X位置对准玩家
                if (x < ctx.playerX) x += speed * 0.5;
                else if (x > ctx.playerX) x -= speed * 0.5;
                break;
            case SHOOTING:
                // 发射弹幕
                if (fireCooldown <= 0) {
                    fireBullet(ctx);
                    fireCooldown = 30 / ctx.difficulty;
                }
                break;
            case RETREAT:
                // 向上撤退并左右躲避
                y += speed * 0.3;
                x += speed * 0.5 * Math.sin(ctx.frameCount * 0.1);
                break;
            case DIVE:
                // 俯冲向玩家位置
                double dx = ctx.playerX - x;
                double dy = ctx.playerY - y;
                double dist = Math.sqrt(dx * dx + dy * dy);
                if (dist > 0) {
                    x += (dx / dist) * speed * 1.5;
                    y += (dy / dist) * speed * 1.5;
                }
                break;
        }
    }

    private void fireBullet(GameContext ctx) {
        // 通知游戏世界创建子弹
        Bullet bullet = new Bullet(x, y - 10, 0, -3 * ctx.difficulty);
        ctx.allEnemies.add(null); // 占位:实际应加入世界的子弹列表
        System.out.printf("敌人在(%.0f, %.0f)发射子弹!\n", x, y);
    }
}

2. 弹幕生成模式

敌人的射击不是随机的,而是遵循特定模式增加可玩性。

/**
 * 弹幕对象
 */
class Bullet {
    double x, y;
    double vx, vy; // 速度向量
    boolean active;

    public Bullet(double x, double y, double vx, double vy) {
        this.x = x; this.y = y;
        this.vx = vx; this.vy = vy;
        this.active = true;
    }

    public void update() {
        x += vx;
        y += vy;
        if (y < 0 || y > 600 || x < 0 || x > 800) active = false;
    }
}

/**
 * 弹幕模式生成器
 */
class BulletPatternGenerator {
    Random rand = new Random();

    /**
     * 生成扇形散射弹幕
     */
    public List<Bullet> generateFan(double x, double y, int count, double baseAngle, double spread) {
        List<Bullet> bullets = new ArrayList<>();
        double speed = 4.0;
        for (int i = 0; i < count; i++) {
            double angle = baseAngle - spread / 2 + (spread / (count - 1)) * i;
            bullets.add(new Bullet(x, y,
                speed * Math.cos(angle),
                speed * Math.sin(angle)));
        }
        return bullets;
    }

    /**
     * 生成追踪弹幕(带轻微追踪效果)
     */
    public List<Bullet> generateHoming(double x, double y, double playerX, double playerY) {
        List<Bullet> bullets = new ArrayList<>();
        double dx = playerX - x;
        double dy = playerY - y;
        double dist = Math.sqrt(dx * dx + dy * dy);
        double speed = 3.5;
        if (dist > 0) {
            // 添加随机偏移,使追踪不是100%准确
            double noiseX = rand.nextDouble() * 2 - 1;
            double noiseY = rand.nextDouble() * 2 - 1;
            bullets.add(new Bullet(x, y,
                (dx / dist) * speed + noiseX,
                (dy / dist) * speed + noiseY));
        }
        return bullets;
    }

    /**
     * 生成环形弹幕
     */
    public List<Bullet> generateRing(double x, double y, int count) {
        List<Bullet> bullets = new ArrayList<>();
        double speed = 3.0;
        for (int i = 0; i < count; i++) {
            double angle = 2 * Math.PI * i / count;
            bullets.add(new Bullet(x, y,
                speed * Math.cos(angle),
                speed * Math.sin(angle)));
        }
        return bullets;
    }
}

3. 玩家自动躲避算法

AI玩家需要分析弹幕轨迹,找到安全位置。

/**
 * 玩家自动躲避AI
 */
class DodgeAI {
    double playerX, playerY;
    double playerWidth = 30;
    double moveSpeed = 5.0;

    /**
     * 计算最佳躲避方向
     * @param bullets 所有活跃弹幕
     * @return 建议移动方向(-1左, 0停, 1右)
     */
    public int calculateDodgeDirection(List<Bullet> bullets) {
        // 方法:对每个候选位置(左/停/右)评估危险度,选择最安全的
        double[] dangerScores = new double[3]; // 左, 停, 右
        double[] candidateX = {playerX - moveSpeed, playerX, playerX + moveSpeed};

        for (int i = 0; i < 3; i++) {
            dangerScores[i] = evaluateDanger(candidateX[i], playerY, bullets);
        }

        // 选择危险度最低的方向
        int bestDir = 1; // 默认不动
        double minDanger = dangerScores[1];
        if (dangerScores[0] < minDanger) { minDanger = dangerScores[0]; bestDir = -1; }
        if (dangerScores[2] < minDanger) { bestDir = 1; }

        return bestDir;
    }

    /**
     * 评估某位置的未来危险度
     * 核心思想:模拟未来几帧,统计会进入玩家碰撞盒的弹幕数量
     */
    private double evaluateDanger(double testX, double testY, List<Bullet> bullets) {
        double danger = 0.0;
        int predictionFrames = 30; // 预测未来30帧

        for (Bullet b : bullets) {
            if (!b.active) continue;
            Bullet sim = new Bullet(b.x, b.y, b.vx, b.vy);
            for (int f = 0; f < predictionFrames; f++) {
                sim.update();
                // 检查是否进入玩家碰撞区域
                if (Math.abs(sim.x - testX) < playerWidth &&
                    Math.abs(sim.y - testY) < playerWidth) {
                    // 越近的弹幕危险度越高
                    danger += 10.0 / (f + 1);
                    break;
                }
            }
        }
        return danger;
    }

    /**
     * 高级躲避:同时考虑射击窗口(在安全前提下尽量对准敌人)
     */
    public int calculateDodgeAndAim(List<Bullet> bullets, List<EnemyAI> enemies) {
        double[] scores = new double[3];
        double[] candidateX = {playerX - moveSpeed, playerX, playerX + moveSpeed};

        for (int i = 0; i < 3; i++) {
            double danger = evaluateDanger(candidateX[i], playerY, bullets);
            double aimBonus = calculateAimBonus(candidateX[i], enemies);
            scores[i] = aimBonus - danger * 2; // 安全优先,危险权重更高
        }

        int bestDir = 1;
        double bestScore = scores[1];
        if (scores[0] > bestScore) { bestScore = scores[0]; bestDir = -1; }
        if (scores[2] > bestScore) { bestDir = 1; }
        return bestDir;
    }

    private double calculateAimBonus(double x, List<EnemyAI> enemies) {
        double bonus = 0;
        for (EnemyAI e : enemies) {
            if (!e.isAlive) continue;
            double dist = Math.abs(e.x - x);
            if (dist < 20) bonus += 5; // 正对敌人,射击加分
            else bonus += 1.0 / (dist + 1);
        }
        return bonus;
    }
}

演示程序

public class SpaceInvadersDemo {
    public static void main(String[] args) {
        // 创建5个敌人
        List<EnemyAI> enemies = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            enemies.add(new EnemyAI(100 + i * 80, 400, 2.0, i));
        }

        BulletPatternGenerator patternGen = new BulletPatternGenerator();
        List<Bullet> bullets = new ArrayList<>();
        DodgeAI dodgeAI = new DodgeAI();
        dodgeAI.playerX = 250;
        dodgeAI.playerY = 50;

        System.out.println("=== 太空侵略者AI模拟 ===");
        for (int frame = 0; frame < 100; frame++) {
            GameContext ctx = new GameContext(dodgeAI.playerX, dodgeAI.playerY, enemies, frame, 1.0);

            // 更新所有敌人
            for (EnemyAI e : enemies) {
                e.update(ctx);
            }

            // 随机生成弹幕(模拟敌人射击)
            if (frame % 20 == 0 && !enemies.isEmpty()) {
                EnemyAI shooter = enemies.get(frame / 20 % enemies.size());
                if (shooter.state == EnemyState.SHOOTING) {
                    bullets.addAll(patternGen.generateFan(shooter.x, shooter.y, 3, -Math.PI/2, 0.5));
                }
            }

            // 更新弹幕位置
            for (Bullet b : bullets) b.update();
            bullets.removeIf(b -> !b.active);

            // AI躲避决策
            int dir = dodgeAI.calculateDodgeDirection(bullets);
            dodgeAI.playerX += dir * dodgeAI.moveSpeed;
            dodgeAI.playerX = Math.max(20, Math.min(480, dodgeAI.playerX));

            if (frame % 10 == 0) {
                System.out.printf("帧%3d: 玩家(%.0f, %.0f), 弹幕数:%d, 敌人状态:",
                    frame, dodgeAI.playerX, dodgeAI.playerY, bullets.size());
                for (EnemyAI e : enemies) {
                    System.out.print(e.state + " ");
                }
                System.out.println();
            }
        }
    }
}

复杂度分析

  • FSM状态转移:O(t)每敌人每帧,t为转移条件数
  • 弹幕更新:O(b)每帧,b为弹幕数量
  • 躲避评估:O(b × f)每帧,b为弹幕数,f为预测帧数
  • 整体:可控制在O(b × f + n × t),n为敌人数量

总结

本文从有限状态机、弹幕模式、躲避策略三个角度构建太空侵略者AI。FSM赋予敌人多样化的战术行为;模式化弹幕增加游戏节奏感;预测性躲避算法让AI玩家展现出类人的反应。读者可在此基础上扩展更复杂的编队行为、Boss多阶段状态机与弹幕编织算法。