引言
小蜜蜂(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)存储敌机和弹幕状态。
扩展方向
- 螺旋弹幕:使用参数方程生成复杂的弹幕图案(如东方Project风格的符卡)
- 群体智能:引入Boids算法,让敌机在保持阵型同时实现局部避碰
- 学习玩家模式:记录玩家的移动偏好,预判其未来位置调整弹幕发射角度
- 动态难度调整:根据玩家表现实时调整敌机数量、弹幕密度和飞行速度
总结
小蜜蜂AI展示了经典射击游戏中敌人行为的设计模式。贝塞尔曲线路径规划提供了优雅的入场动画,阵型变换插值保持了编队的美感与秩序,而多样化的弹幕轨迹则为玩家创造了丰富的挑战。这套算法框架可扩展至各类弹幕射击游戏的敌人AI设计中。