每日算法 — 使用java实现大蜜蜂:敌人编队运动模式与最优射击策略

引言

大蜜蜂(Galaga)是 Namco 于1981年推出的经典射击游戏,以其独特的敌人编队系统和华丽的攻击动画闻名。游戏中的敌人以整齐的矩阵编队出现,定期脱离编队对玩家发起俯冲攻击。本文用 Java 实现大蜜蜂的核心 AI 系统,重点讲解敌人编队运动模式与最优射击策略,包括贝塞尔曲线控制的攻击轨迹和基于概率密度分布的射击时机选择。

游戏场景建模

游戏场景为一个固定大小的射击区域:
– 玩家战机(Player):屏幕底部水平移动,可发射子弹
– 敌人编队(Formation):5 行 × 10 列的敌人矩阵,在屏幕上半部左右摆动
– 攻击敌人(Attacker):脱离编队的敌人,沿曲线飞向玩家并发射子弹
– 子弹(Bullet):玩家和敌人都可以发射,碰撞即消灭目标

敌人的行为分为两个阶段:
1. 编队阶段:所有敌人按正弦波轨迹集体左右移动
2. 攻击阶段:选定敌人脱离编队,沿预设曲线俯冲攻击玩家

核心算法设计

编队运动模式

敌人编队采用正弦波+噪声的复合运动:
– 基础运动:编队中心沿水平正弦波移动 centerX = baseX + amplitude * sin(ω * t)
– 个体偏移:每个敌人在编队中有固定的相对位置 (row, col)
– 进入动画:游戏开始时敌人从屏幕外逐个飞入预定位置,使用缓动函数

编队更新时每帧只需更新中心位置,所有敌人的绝对位置 = 中心位置 + 相对偏移。这种设计使编队运动既整齐又流畅。

攻击轨迹:贝塞尔曲线

敌人俯冲攻击的轨迹使用三次贝塞尔曲线定义:
– P0:攻击起始点(脱离编队时的位置)
– P1:控制点1(远离玩家的方向,制造弧线)
– P2:控制点2(朝向玩家的方向,引导俯冲)
– P3:目标点(玩家当前位置或预测位置)

曲线方程:

B(t) = (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3,  t ∈ [0,1]

通过调整控制点,可以创造出多种攻击模式:
– ** swoop(弧线俯冲):P1 在 P0 上方,P2 在玩家侧方
–
loop(环形攻击):P1 和 P2 在玩家后方,形成绕圈
–
dive(直线下冲)**:P1 和 P2 接近 P0 和 P3 的连线

最优射击策略

玩家面对整齐编队时,最优射击策略需要考虑:
1. 命中率最大化:选择敌人最密集的区域射击
2. 威胁消除:优先攻击即将脱离编队或正在俯冲的敌人
3. 连锁消除:利用敌人之间的间距,一发子弹能否穿过多个敌人
4. 弹药效率:避免向空区域射击

我们使用概率密度图来评估每个水平位置的射击价值:
– 对每个敌人,以其水平位置为中心生成一个高斯分布
– 将所有敌人的高斯分布叠加,得到水平方向的”敌人密度函数”
– 密度峰值处即为最优射击位置

Java 实现

import java.util.*;

/**
 * 大蜜蜂:敌人编队运动模式与最优射击策略
 */
public class GalagaAI {

    static final int WIDTH = 224;
    static final int HEIGHT = 288;
    static final int FORMATION_ROWS = 5;
    static final int FORMATION_COLS = 10;
    static final double ENEMY_SPACING_X = 16;
    static final double ENEMY_SPACING_Y = 16;
    static final double FORMATION_AMP = 30; // 编队摆动幅度
    static final double FORMATION_OMEGA = 0.02; // 编队摆动角速度

    /**
     * 玩家战机
     */
    static class Player {
        double x = WIDTH / 2;
        double y = HEIGHT - 30;
        double speed = 3.0;
        int lives = 3;

        void moveLeft() { x = Math.max(20, x - speed); }
        void moveRight() { x = Math.min(WIDTH - 20, x + speed); }
    }

    /**
     * 敌人
     */
    static class Enemy {
        double x, y;
        int row, col; // 在编队中的行列位置
        int type; // 0=普通, 1=精英, 2=蜂王
        boolean inFormation = true;
        boolean alive = true;

        // 攻击轨迹参数
        double attackProgress = 0;
        double[] p0, p1, p2, p3; // 贝塞尔曲线控制点

        // 更新编队中的位置
        void updateFormationPosition(double formationCenterX, double formationBaseY, int frame) {
            if (!inFormation) return;
            double offsetX = (col - FORMATION_COLS / 2.0) * ENEMY_SPACING_X;
            double offsetY = row * ENEMY_SPACING_Y;
            this.x = formationCenterX + offsetX;
            this.y = formationBaseY + offsetY + Math.sin(frame * 0.05 + col * 0.3) * 2;
        }

        /**
         * 开始攻击:生成贝塞尔曲线轨迹
         */
        void startAttack(double playerX, double playerY) {
            inFormation = false;
            attackProgress = 0;
            p0 = new double[]{x, y};

            // 控制点1:向上并远离玩家方向
            double dirX = Math.signum(x - playerX);
            p1 = new double[]{x + dirX * 40 + (Math.random() - 0.5) * 30, y - 60};

            // 控制点2:朝向玩家方向引导
            p2 = new double[]{playerX + (Math.random() - 0.5) * 40, playerY - 80};

            // 目标点:玩家位置(带预测)
            p3 = new double[]{playerX, playerY};
        }

        /**
         * 更新攻击位置(沿贝塞尔曲线移动)
         */
        void updateAttackPosition() {
            if (inFormation || !alive) return;

            attackProgress += 0.008 + type * 0.003; // 精英敌人更快
            if (attackProgress > 1.0) {
                // 攻击结束,返回编队或飞出屏幕
                alive = false;
                return;
            }

            double t = attackProgress;
            double u = 1 - t;

            // 三次贝塞尔曲线
            x = u*u*u*p0[0] + 3*u*u*t*p1[0] + 3*u*t*t*p2[0] + t*t*t*p3[0];
            y = u*u*u*p0[1] + 3*u*u*t*p1[1] + 3*u*t*t*p2[1] + t*t*t*p3[1];
        }

        /**
         * 判断是否到达最佳射击时机(在玩家正上方时)
         */
        boolean shouldShoot() {
            if (inFormation) return false;
            // 当敌人在玩家上方且正在俯冲时射击
            return y < p3[1] - 50 && Math.abs(x - p3[0]) < 30 && attackProgress > 0.3;
        }
    }

    /**
     * 子弹
     */
    static class Bullet {
        double x, y;
        double vy;
        boolean fromPlayer;

        Bullet(double x, double y, double vy, boolean fromPlayer) {
            this.x = x; this.y = y; this.vy = vy; this.fromPlayer = fromPlayer;
        }

        void update() {
            y += vy;
        }

        boolean isOffScreen() {
            return y < 0 || y > HEIGHT;
        }
    }

    /**
     * 编队管理器
     */
    static class FormationManager {
        double baseX = WIDTH / 2;
        double baseY = 60;
        int frame = 0;
        List<Enemy> enemies;
        Random rand = new Random();
        int nextAttackFrame = 120;

        FormationManager(List<Enemy> enemies) {
            this.enemies = enemies;
        }

        void update() {
            frame++;
            // 编队中心正弦摆动
            double centerX = baseX + FORMATION_AMP * Math.sin(FORMATION_OMEGA * frame);

            // 更新编队中敌人的位置
            for (Enemy e : enemies) {
                if (e.alive && e.inFormation) {
                    e.updateFormationPosition(centerX, baseY, frame);
                }
            }

            // 定期派遣攻击敌人
            if (frame >= nextAttackFrame) {
                sendAttacker();
                nextAttackFrame = frame + 60 + rand.nextInt(120);
            }

            // 更新攻击中的敌人
            for (Enemy e : enemies) {
                if (e.alive && !e.inFormation) {
                    e.updateAttackPosition();
                }
            }
        }

        void sendAttacker() {
            List<Enemy> available = new ArrayList<>();
            for (Enemy e : enemies) {
                if (e.alive && e.inFormation) available.add(e);
            }
            if (available.isEmpty()) return;

            // 优先选择底行和边缘的敌人
            available.sort((a, b) -> Integer.compare(b.row, a.row));
            Enemy attacker = available.get(rand.nextInt(Math.min(3, available.size())));

            // 目标位置带预测
            Player p = GalagaAI.player; // 通过静态引用获取玩家位置
            double targetX = p.x + p.speed * 20 * (rand.nextDouble() - 0.5);
            attacker.startAttack(targetX, p.y);
        }
    }

    /**
     * 玩家射击策略:基于概率密度分布选择最优射击位置
     */
    static class ShootingStrategy {

        /**
         * 计算最优射击目标
         */
        static double[] computeOptimalTarget(List<Enemy> enemies, Player player) {
            // 1. 构建水平方向的敌人密度函数(高斯叠加)
            double[] density = new double[WIDTH];
            double sigma = 15.0; // 高斯分布标准差

            for (Enemy e : enemies) {
                if (!e.alive) continue;
                int cx = (int)e.x;
                for (int dx = -30; dx <= 30; dx++) {
                    int px = cx + dx;
                    if (px < 0 || px >= WIDTH) continue;
                    double dist = dx / sigma;
                    density[px] += Math.exp(-0.5 * dist * dist);
                }
            }

            // 2. 找到密度峰值
            double maxDensity = 0;
            int bestX = (int)player.x;
            for (int x = 0; x < WIDTH; x++) {
                if (density[x] > maxDensity) {
                    maxDensity = density[x];
                    bestX = x;
                }
            }

            // 3. 如果当前玩家位置接近最优位置,建议射击
            boolean shouldShoot = Math.abs(player.x - bestX) < 20 || maxDensity > 2.0;

            return new double[]{bestX, shouldShoot ? 1.0 : 0.0, maxDensity};
        }

        /**
         * 评估射击价值:考虑命中概率和威胁消除
         */
        static double evaluateShotValue(int targetX, List<Enemy> enemies, List<Bullet> bullets) {
            double value = 0;

            for (Enemy e : enemies) {
                if (!e.alive) continue;
                double distX = Math.abs(e.x - targetX);

                // 命中概率(假设子弹垂直向上,考虑敌人水平移动)
                double hitProb = Math.exp(-distX * distX / (2 * 8 * 8));

                // 基础价值
                double baseValue = (e.type + 1) * 10; // 高级敌人价值更高

                // 威胁加成:正在攻击的敌人价值更高
                if (!e.inFormation) baseValue *= 2.5;

                // 距离加成:越近的敌人威胁越大,优先消灭
                double proximityBonus = Math.max(0, (HEIGHT - e.y) / HEIGHT * 20);

                value += hitProb * (baseValue + proximityBonus);
            }

            // 避免与已有子弹重复射击同一区域
            for (Bullet b : bullets) {
                if (b.fromPlayer && Math.abs(b.x - targetX) < 10) {
                    value *= 0.5; // 该区域已有子弹,降低价值
                }
            }

            return value;
        }
    }

    static Player player = new Player();

    // 主程序演示
    public static void main(String[] args) {
        List<Enemy> enemies = new ArrayList<>();

        // 初始化编队
        for (int row = 0; row < FORMATION_ROWS; row++) {
            for (int col = 0; col < FORMATION_COLS; col++) {
                Enemy e = new Enemy();
                e.row = row;
                e.col = col;
                e.type = (row < 2) ? 2 : (row < 4) ? 1 : 0; // 顶部是蜂王,中间精英,底部普通
                enemies.add(e);
            }
        }

        FormationManager fm = new FormationManager(enemies);
        List<Bullet> bullets = new ArrayList<>();

        System.out.println("=== 大蜜蜂 AI 系统模拟 ===");

        // 模拟 300 帧
        for (int frame = 0; frame < 300; frame++) {
            // 更新编队
            fm.update();

            // 玩家移动(模拟追踪最优射击位置)
            double[] target = ShootingStrategy.computeOptimalTarget(enemies, player);
            int bestX = (int)target[0];
            if (Math.abs(player.x - bestX) > 5) {
                if (player.x < bestX) player.moveRight();
                else player.moveLeft();
            }

            // AI 决定是否射击
            if (target[1] > 0.5 && frame % 12 == 0) {
                bullets.add(new Bullet(player.x, player.y - 10, -6, true));
            }

            // 更新子弹
            bullets.removeIf(b -> {
                b.update();
                return b.isOffScreen();
            });

            // 敌人射击
            for (Enemy e : enemies) {
                if (e.alive && !e.inFormation && e.shouldShoot() && frame % 20 == 0) {
                    bullets.add(new Bullet(e.x, e.y + 10, 4, false));
                }
            }

            // 输出关键帧状态
            if (frame % 60 == 0) {
                long formationCount = enemies.stream().filter(e -> e.alive && e.inFormation).count();
                long attackCount = enemies.stream().filter(e -> e.alive && !e.inFormation).count();
                System.out.printf("帧 %3d: 编队敌人=%d, 攻击敌人=%d, 玩家位置=%.1f, 最优射击X=%.0f%n",
                    frame, formationCount, attackCount, player.x, target[0]);
            }
        }
    }
}

算法解析

贝塞尔曲线的攻击多样性

贝塞尔曲线的魅力在于用少量控制点生成丰富多样的轨迹。在大蜜蜂中:
– P1 控制”起飞弧线”的方向和弯曲度
– P2 控制”俯冲角度”和攻击路径
– 通过随机化控制点,每个敌人的攻击轨迹都略有不同,避免机械感

三次贝塞尔曲线的计算复杂度为 O(1) 每帧,非常适合实时游戏。

编队摆动的数学

编队使用正弦波 centerX = baseX + amplitude * sin(ω * t) 实现左右摆动:
– amplitude = 30:摆动幅度约 60 像素,使编队有足够动感
– ω = 0.02:每 314 帧(约 5 秒)完成一个周期
– 个体叠加 sin(frame * 0.05 + col * 0.3) * 2:每列有微小的相位差,使编队看起来更有生气

概率密度射击策略

高斯叠加的射击策略模拟了人类玩家的”找最密集区域打”的直觉:
– 每个敌人贡献一个钟形分布,峰值在其水平位置
– 多个敌人靠近时,分布叠加产生更高的峰值
– 最优射击位置自动指向敌人最密集的区域

这种方法比”找最近的敌人”更智能,因为它能识别”一发子弹打多个”的机会。

复杂度分析

操作 时间复杂度 空间复杂度
编队位置更新 O(N),N 为敌人数 O(1)
贝塞尔曲线计算 O(1) 每敌人 O(1)
概率密度计算 O(N × W),W 为高斯窗口宽 O(WIDTH)
射击价值评估 O(N + B),B 为子弹数 O(1)
完整帧更新 O(N × W) O(WIDTH + B)

对于 50 个敌人的编队,每帧计算量在数万级别,现代 CPU 轻松满足 60fps 要求。

扩展思路

  1. 群体攻击模式:引入”螺旋阵型””波浪阵型”等预设攻击模式,用参数化贝塞尔曲线实现
  2. 自适应难度:根据玩家命中率动态调整敌人攻击频率和子弹速度
  3. 神经网络瞄准:用 NN 训练敌人的射击提前量,考虑玩家移动模式
  4. 遗传算法进化:让敌人攻击策略在世代中进化,找到最难躲避的轨迹

总结

本文通过 Java 实现了大蜜蜂的敌人 AI 与射击策略系统:
– 正弦波编队运动:用中心正弦波+个体微扰实现整齐的编队效果
– 贝塞尔曲线攻击:用三次贝塞尔曲线生成多样化的俯冲轨迹
– 概率密度射击策略:用高斯分布叠加找到最优射击位置

读者可以从中学习到:贝塞尔曲线在游戏轨迹中的应用、群体动画的数学建模、以及基于概率密度的目标选择策略。