每日算法 — 使用java实现小蜜蜂:阵型路径编排与弹幕轨迹预测

引言

小蜜蜂(Galaga)是经典的街机射击游戏,成群的外星敌机以特定编队从屏幕上方降临,向玩家发射弹幕。本文用Java实现小蜜蜂风格的敌人阵型编排AI与弹幕轨迹预测系统,核心算法包括贝塞尔曲线路径规划、阵型变换插值与抛物线弹幕轨迹计算,让敌机能够编排出动感的入场动画和具有威胁性的射击模式。

游戏场景与坐标系统

游戏场景为800×600像素。敌人从屏幕上方或两侧进入,以编队形式飞行。弹幕沿直线或曲线轨迹运动,玩家需要躲避弹幕并击毁敌机。

核心算法设计

1. 贝塞尔曲线路径规划

敌机的入场和离场路径使用二次或三次贝塞尔曲线定义:

B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂      (二次贝塞尔)
B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃  (三次贝塞尔)

通过调整控制点,可以生成圆弧、S形、螺旋等各种飞行轨迹。

2. 阵型变换插值

敌机编队可以在飞行过程中变换阵型(如从一字长蛇阵变为V字形)。使用线性插值(Lerp)或球面线性插值(Slerp)平滑过渡各敌机的目标位置。

3. 弹幕轨迹计算

弹幕轨迹分为几种模式:
– 直线弹:固定方向匀速运动
– 追踪弹:每帧微调方向指向玩家当前位置
– 抛物线弹:受虚拟重力影响,形成抛物线轨迹
– 散射弹:多个弹幕以扇形角度发射

Java完整实现

import java.util.*;

/**
 * 小蜜蜂敌人阵型与弹幕AI系统
 * 实现贝塞尔路径规划、阵型变换与弹幕轨迹计算
 */
public class GalagaAI {
    public static final int WIDTH = 800;
    public static final int HEIGHT = 600;

    private List<Enemy> enemies;
    private List<Bullet> bullets;
    private Player player;
    private int frame;
    private Random random;

    public GalagaAI() {
        this.enemies = new ArrayList<>();
        this.bullets = new ArrayList<>();
        this.player = new Player(WIDTH / 2, HEIGHT - 50);
        this.random = new Random();
        this.frame = 0;
        initFormation();
    }

    static class Player {
        double x, y;
        Player(double x, double y) { this.x = x; this.y = y; }
    }

    /**
     * 敌机类,包含路径跟随与状态管理
     */
    static class Enemy {
        double x, y;            // 当前位置
        double hp;              // 生命值
        Path path;              // 当前跟随的路径
        double pathProgress;    // 路径进度 0~1
        Formation formation;    // 所属阵型
        int formationIndex;     // 在阵型中的索引
        int state;              // 0=路径跟随 1=阵型悬停 2=俯冲攻击 3=返回
        int cooldown;           // 射击冷却
        int stateTimer;

        public Enemy(double x, double y) {
            this.x = x; this.y = y;
            this.hp = 10;
            this.pathProgress = 0;
            this.state = 0;
            this.cooldown = random.nextInt(60);
        }
    }

    /**
     * 贝塞尔路径类
     */
    static class Path {
        List<double[]> points;  // 控制点列表
        int degree;             // 贝塞尔曲线阶数

        public Path(List<double[]> points) {
            this.points = points;
            this.degree = points.size() - 1;
        }

        /**
         * 计算贝塞尔曲线上t时刻的位置
         */
        public double[] getPoint(double t) {
            if (degree == 2) {
                return quadraticBezier(points.get(0), points.get(1), points.get(2), t);
            } else if (degree == 3) {
                return cubicBezier(points.get(0), points.get(1), points.get(2), points.get(3), t);
            }
            return points.get(0).clone();
        }

        private double[] quadraticBezier(double[] p0, double[] p1, double[] p2, double t) {
            double u = 1 - t;
            double x = u * u * p0[0] + 2 * u * t * p1[0] + t * t * p2[0];
            double y = u * u * p0[1] + 2 * u * t * p1[1] + t * t * p2[1];
            return new double[]{x, y};
        }

        private double[] cubicBezier(double[] p0, double[] p1, double[] p2, double[] p3, double t) {
            double u = 1 - t;
            double u2 = u * u, u3 = u2 * u;
            double t2 = t * t, t3 = t2 * t;
            double x = u3 * p0[0] + 3 * u2 * t * p1[0] + 3 * u * t2 * p2[0] + t3 * p3[0];
            double y = u3 * p0[1] + 3 * u2 * t * p1[1] + 3 * u * t2 * p2[1] + t3 * p3[1];
            return new double[]{x, y};
        }
    }

    /**
     * 阵型类,定义各敌机的相对位置
     */
    static class Formation {
        String name;
        List<double[]> offsets; // 各位置相对中心点的偏移

        public Formation(String name, List<double[]> offsets) {
            this.name = name;
            this.offsets = offsets;
        }
    }

    /**
     * 弹幕类,支持多种轨迹模式
     */
    static class Bullet {
        double x, y;
        double vx, vy;
        int type;       // 0=直线 1=追踪 2=抛物线
        int lifetime;
        double gravity; // 抛物线模式的重力加速度

        public Bullet(double x, double y, double vx, double vy, int type) {
            this.x = x; this.y = y;
            this.vx = vx; this.vy = vy;
            this.type = type;
            this.lifetime = 300;
            this.gravity = type == 2 ? 0.05 : 0;
        }

        public void update(Player player) {
            if (type == 1 && lifetime > 250) { // 追踪弹前50帧追踪
                double dx = player.x - x;
                double dy = player.y - y;
                double dist = Math.sqrt(dx * dx + dy * dy);
                if (dist > 0) {
                    vx += dx / dist * 0.1;
                    vy += dy / dist * 0.1;
                    double speed = Math.sqrt(vx * vx + vy * vy);
                    if (speed > 4) {
                        vx = vx / speed * 4;
                        vy = vy / speed * 4;
                    }
                }
            }
            x += vx;
            y += vy;
            vy += gravity; // 抛物线重力
            lifetime--;
        }
    }

    /**
     * 初始化敌机编队
     */
    private void initFormation() {
        // 创建V字形阵型
        List<double[]> vOffsets = new ArrayList<>();
        vOffsets.add(new double[]{0, 0});
        vOffsets.add(new double[]{-40, 40});
        vOffsets.add(new double[]{40, 40});
        vOffsets.add(new double[]{-80, 80});
        vOffsets.add(new double[]{80, 80});
        Formation vFormation = new Formation("V字形", vOffsets);

        // 创建弧形入场路径
        List<double[]> arcPoints = new ArrayList<>();
        arcPoints.add(new double[]{-50, 100});
        arcPoints.add(new double[]{200, -50});
        arcPoints.add(new double[]{400, 150});
        Path arcPath = new Path(arcPoints);

        // 生成敌机
        for (int i = 0; i < 5; i++) {
            Enemy e = new Enemy(-50 - i * 30, 100);
            e.path = arcPath;
            e.formation = vFormation;
            e.formationIndex = i;
            enemies.add(e);
        }
    }

    /**
     * 每帧更新所有敌机和弹幕
     */
    public void update() {
        frame++;

        // 更新敌机
        for (Enemy e : enemies) {
            if (e.hp <= 0) continue;
            updateEnemy(e);
        }

        // 更新弹幕
        Iterator<Bullet> it = bullets.iterator();
        while (it.hasNext()) {
            Bullet b = it.next();
            b.update(player);
            if (b.lifetime <= 0 || b.y > HEIGHT || b.x < 0 || b.x > WIDTH) {
                it.remove();
            }
        }

        // 每120帧变换一次阵型
        if (frame % 120 == 0 && frame > 0) {
            changeFormation();
        }
    }

    /**
     * 更新单个敌机的状态和行为
     */
    private void updateEnemy(Enemy e) {
        switch (e.state) {
            case 0: // 路径跟随阶段
                e.pathProgress += 0.008;
                if (e.pathProgress >= 1.0) {
                    e.pathProgress = 1.0;
                    e.state = 1; // 进入阵型悬停
                    e.stateTimer = 0;
                } else {
                    double[] pos = e.path.getPoint(e.pathProgress);
                    e.x = pos[0];
                    e.y = pos[1];
                }
                break;
            case 1: // 阵型悬停阶段
                e.stateTimer++;
                // 保持在阵型位置
                updateFormationPosition(e);
                // 随机俯冲攻击
                if (e.stateTimer > 60 && random.nextDouble() < 0.01) {
                    e.state = 2;
                    e.stateTimer = 0;
                }
                // 射击
                if (e.cooldown <= 0) {
                    fireBullet(e);
                    e.cooldown = 40 + random.nextInt(40);
                }
                e.cooldown--;
                break;
            case 2: // 俯冲攻击阶段
                e.stateTimer++;
                // 向玩家位置俯冲
                double dx = player.x - e.x;
                double dy = player.y - e.y;
                double dist = Math.sqrt(dx * dx + dy * dy);
                if (dist > 0) {
                    e.x += dx / dist * 3;
                    e.y += dy / dist * 3;
                }
                if (e.stateTimer > 100 || dist < 30) {
                    e.state = 3; // 返回
                    e.stateTimer = 0;
                }
                break;
            case 3: // 返回阵型
                e.stateTimer++;
                updateFormationPosition(e);
                // 慢慢回到阵型位置
                if (e.stateTimer > 60) {
                    e.state = 1;
                    e.stateTimer = 0;
                }
                break;
        }
    }

    /**
     * 将敌机移动到阵型中的目标位置
     */
    private void updateFormationPosition(Enemy e) {
        if (e.formation == null) return;
        double[] offset = e.formation.offsets.get(e.formationIndex);
        double targetX = WIDTH / 2.0 + offset[0];
        double targetY = 100 + offset[1];
        // 线性插值平滑移动
        e.x += (targetX - e.x) * 0.05;
        e.y += (targetY - e.y) * 0.05;
    }

    /**
     * 敌机发射弹幕
     */
    private void fireBullet(Enemy e) {
        double dx = player.x - e.x;
        double dy = player.y - e.y;
        double dist = Math.sqrt(dx * dx + dy * dy);
        double speed = 3.0;

        int pattern = random.nextInt(3);
        switch (pattern) {
            case 0: // 直线弹
                bullets.add(new Bullet(e.x, e.y,
                        dx / dist * speed, dy / dist * speed, 0));
                break;
            case 1: // 追踪弹
                bullets.add(new Bullet(e.x, e.y,
                        dx / dist * speed * 0.8, dy / dist * speed * 0.8, 1));
                break;
            case 2: // 散射弹(3发扇形)
                double baseAngle = Math.atan2(dy, dx);
                for (int i = -1; i <= 1; i++) {
                    double angle = baseAngle + i * 0.3;
                    bullets.add(new Bullet(e.x, e.y,
                            Math.cos(angle) * speed, Math.sin(angle) * speed, 0));
                }
                break;
        }
    }

    /**
     * 变换阵型:将所有敌机切换到新的阵型
     */
    private void changeFormation() {
        // 创建一字长蛇阵阵型
        List<double[]> lineOffsets = new ArrayList<>();
        for (int i = 0; i < enemies.size(); i++) {
            lineOffsets.add(new double[]{(i - 2) * 60, 0});
        }
        Formation lineFormation = new Formation("一字长蛇阵", lineOffsets);

        // 创建圆形阵型
        List<double[]> circleOffsets = new ArrayList<>();
        for (int i = 0; i < enemies.size(); i++) {
            double angle = 2 * Math.PI * i / enemies.size();
            circleOffsets.add(new double[]{Math.cos(angle) * 80, Math.sin(angle) * 40});
        }
        Formation circleFormation = new Formation("圆形阵", circleOffsets);

        Formation newFormation = random.nextBoolean() ? lineFormation : circleFormation;
        for (Enemy e : enemies) {
            if (e.hp > 0 && e.state == 1) {
                e.formation = newFormation;
            }
        }
        System.out.println("阵型变换为: " + newFormation.name);
    }

    public void printStatus() {
        System.out.printf("帧:%d 敌机:%d 弹幕:%d%n", frame, enemies.size(), bullets.size());
        for (Enemy e : enemies) {
            if (e.hp > 0) {
                System.out.printf("  敌机(%.0f,%.0f) 状态:%d%n", e.x, e.y, e.state);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        GalagaAI game = new GalagaAI();
        System.out.println("=== 小蜜蜂AI阵型模拟 ===");
        for (int i = 0; i < 300; i++) {
            game.update();
            if (i % 30 == 0) game.printStatus();
            Thread.sleep(33); // 约30fps
        }
        System.out.println("模拟结束!");
    }
}

算法亮点解析

贝塞尔路径的优雅入场

敌机使用二次贝塞尔曲线从屏幕外飞入预定位置,控制点位于屏幕上方,使入场轨迹呈现自然的弧线。相比直线移动,曲线入场更具视觉冲击力,也为后续阵型变换预留了平滑过渡的空间。

三阶段状态机

每个敌机具有路径跟随、阵型悬停、俯冲攻击、返回四个状态。路径跟随阶段使用预计算贝塞尔曲线;阵型悬停阶段通过Lerp插值保持队形;俯冲阶段直接追踪玩家形成威胁;返回阶段再次插值归位。这种分层设计让整体行为既有规律又具变化。

多样化弹幕模式

三种弹幕模式形成互补威胁:直线弹速度快但可预测;追踪弹持续施压但可被引诱;散射弹覆盖面积大需要大范围躲避。随机切换模式让玩家无法形成固定应对策略。

复杂度分析

  • 时间复杂度:每帧O(E + B),E为敌机数量,B为弹幕数量。贝塞尔计算为O(1),阵型插值为O(E)。
  • 空间复杂度:O(E + B)存储敌机和弹幕状态。

扩展方向

  1. 螺旋弹幕:使用参数方程生成复杂的弹幕图案(如东方Project风格的符卡)
  2. 群体智能:引入Boids算法,让敌机在保持阵型同时实现局部避碰
  3. 学习玩家模式:记录玩家的移动偏好,预判其未来位置调整弹幕发射角度
  4. 动态难度调整:根据玩家表现实时调整敌机数量、弹幕密度和飞行速度

总结

小蜜蜂AI展示了经典射击游戏中敌人行为的设计模式。贝塞尔曲线路径规划提供了优雅的入场动画,阵型变换插值保持了编队的美感与秩序,而多样化的弹幕轨迹则为玩家创造了丰富的挑战。这套算法框架可扩展至各类弹幕射击游戏的敌人AI设计中。