引言
小蜜蜂(Space Invaders)是街机游戏黄金时代的经典之作,玩家控制一艘飞船在屏幕底部移动,击落从上方成群结队压下来的外星敌人。游戏的核心算法挑战在于敌人阵型的波浪移动模式设计,以及在密集弹幕中寻找最优射击时机的决策策略。本文将用Java实现一个完整的小蜜蜂游戏,讲解基于正弦波的阵型运动算法、碰撞检测网格优化,以及基于威胁评估的贪心射击策略。
核心算法原理
1. 波浪阵型运动算法
敌人编队采用矩阵排列,整体运动可分解为水平移动和垂直下降两个阶段。为增加视觉趣味性,可引入正弦波叠加:
x(t) = baseX + amplitude * sin(frequency * t + phase)
y(t) = baseY + descentSpeed * t
其中 baseX 为编队基准位置,amplitude 控制左右摆动幅度,frequency 控制波动速度。
2. 空间哈希碰撞检测
当子弹和敌人数量增多时,逐对检测的O(N²)复杂度会成为性能瓶颈。空间哈希将屏幕划分为固定大小的网格单元,每个对象只与同一单元及相邻单元内的对象做碰撞检测:
cellX = object.x / CELL_SIZE
cellY = object.y / CELL_SIZE
理想情况下,每个单元内对象数趋近于常数,碰撞检测复杂度降为O(N)。
3. 威胁评估与贪心射击策略
AI需要选择射击哪个敌人以获得最高收益。贪心策略考虑以下因素:
- 距离优先:越近的敌人威胁越大
- 高度优先:越低的敌人越接近玩家
- 暴露度:左右无遮挡的敌人更容易击中
- 编队价值:击落后可能导致整行下坠的敌人价值更高
综合评分公式:
score = w1 * (screenHeight - enemy.y) + w2 * exposedBonus + w3 * formationValue
Java完整实现
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
/**
* 小蜜蜂模拟 - 敌人波浪阵型与最优射击策略
*
* 核心算法:
* 1. 正弦波叠加的波浪阵型运动
* 2. 空间哈希网格碰撞检测优化
* 3. 威胁评估贪心射击策略
* 4. 粒子爆炸与屏幕震动效果
*/
public class SpaceInvadersGame extends JPanel implements Runnable, KeyListener {
// 游戏常量
private static final int SCREEN_WIDTH = 800;
private static final int SCREEN_HEIGHT = 600;
private static final int CELL_SIZE = 40; // 空间哈希单元大小
private static final int ENEMY_ROWS = 5;
private static final int ENEMY_COLS = 11;
private static final double ENEMY_SPACING_X = 50;
private static final double ENEMY_SPACING_Y = 40;
private static final double PLAYER_SPEED = 5;
private static final double BULLET_SPEED = 8;
private static final int PLAYER_COOLDOWN = 15; // 射击冷却帧数
// 游戏对象
private final List<Enemy> enemies = new ArrayList<>();
private final List<Bullet> playerBullets = new ArrayList<>();
private final List<Bullet> enemyBullets = new ArrayList<>();
private final List<Particle> particles = new ArrayList<>();
private Player player;
// 游戏状态
private volatile boolean running = true;
private volatile boolean gameOver = false;
private volatile int score = 0;
private volatile int lives = 3;
private volatile int wave = 1;
private int frameCount = 0;
private boolean aiMode = true;
private int shakeFrames = 0; // 屏幕震动剩余帧数
// 敌人编队运动参数
private double formationX = 100;
private double formationY = 50;
private double formationSpeed = 0.8;
private double formationDirection = 1; // 1为右,-1为左
private double waveAmplitude = 15;
private double waveFrequency = 0.03;
private double descentAmount = 10;
/**
* 玩家飞船
*/
static class Player {
double x, y;
double width = 40, height = 30;
int cooldown = 0;
boolean leftPressed, rightPressed, shootPressed;
Player(double x, double y) {
this.x = x;
this.y = y;
}
Rectangle getBounds() {
return new Rectangle((int) (x - width / 2), (int) (y - height / 2),
(int) width, (int) height);
}
}
/**
* 敌人
*/
static class Enemy {
double baseX, baseY; // 在编队中的相对位置
double offsetX; // 正弦波偏移
int row, col;
int type; // 敌人类型(0-2,影响分值和外观)
boolean alive;
int animFrame; // 动画帧
double phase; // 正弦波相位
Enemy(double bx, double by, int r, int c, int type) {
this.baseX = bx;
this.baseY = by;
this.row = r;
this.col = c;
this.type = type;
this.alive = true;
this.animFrame = 0;
this.phase = c * 0.3; // 每列不同相位,形成波浪效果
}
double getX() {
return formationX + baseX + offsetX;
}
double getY() {
return formationY + baseY;
}
Rectangle getBounds() {
return new Rectangle((int) (getX() - 18), (int) (getY() - 14), 36, 28);
}
}
/**
* 子弹
*/
static class Bullet {
double x, y;
double vy;
boolean fromPlayer;
Color color;
Bullet(double x, double y, double vy, boolean player) {
this.x = x;
this.y = y;
this.vy = vy;
this.fromPlayer = player;
this.color = player ? Color.GREEN : Color.RED;
}
Rectangle getBounds() {
return new Rectangle((int) x - 2, (int) y - 6, 4, 12);
}
}
/**
* 爆炸粒子
*/
static class Particle {
double x, y, vx, vy;
int life, maxLife;
Color color;
double size;
Particle(double x, double y, double vx, double vy,
Color c, int life, double size) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.color = c;
this.life = life;
this.maxLife = life;
this.size = size;
}
}
public SpaceInvadersGame() {
setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
setBackground(Color.BLACK);
setFocusable(true);
addKeyListener(this);
initWave();
}
/**
* 初始化一波敌人
*/
private void initWave() {
enemies.clear();
playerBullets.clear();
enemyBullets.clear();
formationX = 100;
formationY = 50 + wave * 10;
formationSpeed = 0.5 + wave * 0.15;
for (int row = 0; row < ENEMY_ROWS; row++) {
for (int col = 0; col < ENEMY_COLS; col++) {
double bx = col * ENEMY_SPACING_X;
double by = row * ENEMY_SPACING_Y;
int type = row / 2; // 上方敌人分值更高
enemies.add(new Enemy(bx, by, row, col, type));
}
}
if (player == null) {
player = new Player(SCREEN_WIDTH / 2, SCREEN_HEIGHT - 50);
}
}
/**
* 更新游戏逻辑
*/
private void update() {
if (gameOver) return;
frameCount++;
if (shakeFrames > 0) shakeFrames--;
// 更新玩家位置
updatePlayer();
// 更新敌人编队运动
updateFormation();
// 更新子弹
updateBullets();
// 碰撞检测(空间哈希优化)
checkCollisions();
// 敌人随机射击
enemyShoot();
// AI射击决策
if (aiMode && player.cooldown <= 0) {
aiShootDecision();
}
// 更新粒子
updateParticles();
// 检查波次完成
if (enemies.stream().noneMatch(e -> e.alive)) {
wave++;
initWave();
}
// 检查敌人是否到达底部
for (Enemy e : enemies) {
if (e.alive && e.getY() > player.y - 20) {
gameOver = true;
return;
}
}
}
/**
* 更新玩家位置
*/
private void updatePlayer() {
if (player.leftPressed) player.x -= PLAYER_SPEED;
if (player.rightPressed) player.x += PLAYER_SPEED;
// 边界限制
player.x = Math.max(20, Math.min(SCREEN_WIDTH - 20, player.x));
if (player.cooldown > 0) player.cooldown--;
// 玩家射击
if (player.shootPressed && player.cooldown <= 0 && !aiMode) {
playerBullets.add(new Bullet(player.x, player.y - 15, -BULLET_SPEED, true));
player.cooldown = PLAYER_COOLDOWN;
}
}
/**
* 更新敌人编队运动
*
* 核心算法:
* 1. 整体左右移动
* 2. 到达边界后下移并反向
* 3. 每列叠加独立的正弦波,形成波浪效果
*/
private void updateFormation() {
formationX += formationSpeed * formationDirection;
// 检查是否到达边界
double maxX = 0, minX = Double.MAX_VALUE;
for (Enemy e : enemies) {
if (!e.alive) continue;
double x = e.getX();
maxX = Math.max(maxX, x);
minX = Math.min(minX, x);
}
if (maxX > SCREEN_WIDTH - 40 || minX < 40) {
formationDirection *= -1;
formationY += descentAmount;
}
// 正弦波偏移更新
for (Enemy e : enemies) {
if (!e.alive) continue;
e.offsetX = waveAmplitude * Math.sin(frameCount * waveFrequency + e.phase);
e.animFrame = (frameCount / 30) % 2;
}
}
/**
* 更新子弹位置
*/
private void updateBullets() {
Iterator<Bullet> it = playerBullets.iterator();
while (it.hasNext()) {
Bullet b = it.next();
b.y += b.vy;
if (b.y < 0) it.remove();
}
it = enemyBullets.iterator();
while (it.hasNext()) {
Bullet b = it.next();
b.y += b.vy;
if (b.y > SCREEN_HEIGHT) it.remove();
}
}
/**
* 空间哈希碰撞检测
*
* 算法步骤:
* 1. 将屏幕划分为 CELL_SIZE × CELL_SIZE 的网格
* 2. 将每个子弹放入对应的网格单元
* 3. 遍历每个敌人,只检测同单元及相邻单元内的子弹
* 4. 若AABB相交,判定碰撞
*/
private void checkCollisions() {
// 构建空间哈希:格子坐标 -> 子弹列表
Map<String, List<Bullet>> spatialHash = new HashMap<>();
for (Bullet b : playerBullets) {
int cx = (int) (b.x / CELL_SIZE);
int cy = (int) (b.y / CELL_SIZE);
String key = cx + "," + cy;
spatialHash.computeIfAbsent(key, k -> new ArrayList<>()).add(b);
}
// 检测玩家子弹与敌人的碰撞
Iterator<Enemy> eit = enemies.iterator();
while (eit.hasNext()) {
Enemy e = eit.next();
if (!e.alive) continue;
Rectangle enemyRect = e.getBounds();
int minCX = (int) (enemyRect.x / CELL_SIZE) - 1;
int maxCX = (int) ((enemyRect.x + enemyRect.width) / CELL_SIZE) + 1;
int minCY = (int) (enemyRect.y / CELL_SIZE) - 1;
int maxCY = (int) ((enemyRect.y + enemyRect.height) / CELL_SIZE) + 1;
for (int cx = minCX; cx <= maxCX; cx++) {
for (int cy = minCY; cy <= maxCY; cy++) {
String key = cx + "," + cy;
List<Bullet> bullets = spatialHash.get(key);
if (bullets == null) continue;
Iterator<Bullet> bit = bullets.iterator();
while (bit.hasNext()) {
Bullet b = bit.next();
if (enemyRect.intersects(b.getBounds())) {
e.alive = false;
bit.remove();
playerBullets.remove(b);
int points = (3 - e.type) * 10 * wave;
score += points;
createExplosion(e.getX(), e.getY(), getEnemyColor(e.type));
shakeFrames = 5;
break;
}
}
}
}
}
// 检测敌人子弹与玩家
for (Bullet b : enemyBullets) {
if (b.getBounds().intersects(player.getBounds())) {
lives--;
createExplosion(player.x, player.y, Color.CYAN);
shakeFrames = 10;
enemyBullets.clear();
if (lives <= 0) {
gameOver = true;
}
break;
}
}
}
/**
* 敌人随机射击
*/
private void enemyShoot() {
if (frameCount % (45 - Math.min(30, wave * 2)) != 0) return;
// 收集最下方的存活敌人(每列)
Map<Integer, Enemy> bottomEnemies = new HashMap<>();
for (Enemy e : enemies) {
if (!e.alive) continue;
Enemy current = bottomEnemies.get(e.col);
if (current == null || e.row > current.row) {
bottomEnemies.put(e.col, e);
}
}
// 随机选择一列射击
List<Enemy> shooters = new ArrayList<>(bottomEnemies.values());
if (!shooters.isEmpty()) {
Enemy shooter = shooters.get((int) (Math.random() * shooters.size()));
enemyBullets.add(new Bullet(shooter.getX(), shooter.getY() + 15,
BULLET_SPEED * 0.6, false));
}
}
/**
* AI贪心射击决策
*
* 核心算法:
* 1. 收集所有存活的敌人
* 2. 对每个敌人计算威胁评分
* 3. 评分因素:高度(越低越危险)、暴露度(左右无遮挡加分)、类型价值
* 4. 选择评分最高的敌人,将玩家移动到其正下方射击
*/
private void aiShootDecision() {
List<Enemy> aliveEnemies = new ArrayList<>();
for (Enemy e : enemies) {
if (e.alive) aliveEnemies.add(e);
}
if (aliveEnemies.isEmpty()) return;
Enemy bestTarget = null;
double bestScore = -1;
for (Enemy e : aliveEnemies) {
double ex = e.getX();
double ey = e.getY();
// 因素1:高度评分(越低越危险,分数越高)
double heightScore = (SCREEN_HEIGHT - ey) / SCREEN_HEIGHT * 100;
// 因素2:暴露度评分(左右是否有遮挡)
double exposedScore = 0;
boolean leftBlocked = false, rightBlocked = false;
for (Enemy other : aliveEnemies) {
if (other == e) continue;
if (Math.abs(other.getY() - ey) < 30) {
if (other.getX() < ex - 20) leftBlocked = true;
if (other.getX() > ex + 20) rightBlocked = true;
}
}
if (!leftBlocked || !rightBlocked) exposedScore = 30;
if (!leftBlocked && !rightBlocked) exposedScore = 50;
// 因素3:类型价值
double typeScore = (3 - e.type) * 10;
// 因素4:距离玩家水平距离(越近越容易命中)
double distScore = 50 - Math.abs(ex - player.x) / SCREEN_WIDTH * 50;
double totalScore = heightScore * 0.4 + exposedScore * 0.3 +
typeScore * 0.2 + distScore * 0.1;
if (totalScore > bestScore) {
bestScore = totalScore;
bestTarget = e;
}
}
if (bestTarget != null) {
double targetX = bestTarget.getX();
double diff = targetX - player.x;
// 移动到目标正下方
if (Math.abs(diff) > 5) {
player.leftPressed = diff < 0;
player.rightPressed = diff > 0;
} else {
player.leftPressed = false;
player.rightPressed = false;
// 到位后射击
playerBullets.add(new Bullet(player.x, player.y - 15,
-BULLET_SPEED, true));
player.cooldown = PLAYER_COOLDOWN;
}
}
}
/**
* 创建爆炸粒子效果
*/
private void createExplosion(double x, double y, Color color) {
for (int i = 0; i < 12; i++) {
double angle = Math.random() * Math.PI * 2;
double speed = 1 + Math.random() * 4;
particles.add(new Particle(x, y,
Math.cos(angle) * speed, Math.sin(angle) * speed,
color, 20 + (int) (Math.random() * 15), 2 + Math.random() * 3));
}
}
/**
* 更新粒子
*/
private void updateParticles() {
Iterator<Particle> it = particles.iterator();
while (it.hasNext()) {
Particle p = it.next();
p.x += p.vx;
p.y += p.vy;
p.vy += 0.1; // 重力
p.life--;
if (p.life <= 0) it.remove();
}
}
/**
* 获取敌人类型对应颜色
*/
private Color getEnemyColor(int type) {
switch (type) {
case 0: return Color.MAGENTA;
case 1: return Color.GREEN;
case 2: return Color.CYAN;
default: return Color.WHITE;
}
}
@Override
public void run() {
while (running) {
update();
repaint();
try {
Thread.sleep(16);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// 屏幕震动效果
int shakeX = 0, shakeY = 0;
if (shakeFrames > 0) {
shakeX = (int) ((Math.random() - 0.5) * 6);
shakeY = (int) ((Math.random() - 0.5) * 6);
}
g2d.translate(shakeX, shakeY);
// 绘制星星背景
g2d.setColor(Color.WHITE);
for (int i = 0; i < 50; i++) {
int sx = (i * 137 + frameCount / 10) % SCREEN_WIDTH;
int sy = (i * 89) % (SCREEN_HEIGHT - 100);
g2d.fillRect(sx, sy, 1 + (i % 2), 1 + (i % 2));
}
// 绘制敌人
for (Enemy e : enemies) {
if (!e.alive) continue;
double x = e.getX();
double y = e.getY();
Color c = getEnemyColor(e.type);
g2d.setColor(c);
// 绘制外星人形状
int[] xPoints, yPoints;
if (e.animFrame == 0) {
xPoints = new int[]{(int) x - 15, (int) x - 10, (int) x,
(int) x + 10, (int) x + 15,
(int) x + 8, (int) x - 8};
yPoints = new int[]{(int) y + 10, (int) y - 5, (int) y - 12,
(int) y - 5, (int) y + 10,
(int) y + 5, (int) y + 5};
} else {
xPoints = new int[]{(int) x - 12, (int) x - 8, (int) x,
(int) x + 8, (int) x + 12,
(int) x + 6, (int) x - 6};
yPoints = new int[]{(int) y + 8, (int) y - 8, (int) y - 10,
(int) y - 8, (int) y + 8,
(int) y + 3, (int) y + 3};
}
g2d.fillPolygon(xPoints, yPoints, 7);
// 眼睛
g2d.setColor(Color.WHITE);
g2d.fillRect((int) x - 6, (int) y - 4, 4, 4);
g2d.fillRect((int) x + 2, (int) y - 4, 4, 4);
}
// 绘制玩家
if (lives > 0) {
g2d.setColor(Color.GREEN);
int px = (int) player.x;
int py = (int) player.y;
int[] pxPoints = {px, px - 20, px - 15, px + 15, px + 20};
int[] pyPoints = {py - 15, py + 15, py + 10, py + 10, py + 15};
g2d.fillPolygon(pxPoints, pyPoints, 5);
// 引擎火焰
g2d.setColor(Color.ORANGE);
int flameH = 5 + (int) (Math.random() * 8);
g2d.fillRect(px - 5, py + 15, 3, flameH);
g2d.fillRect(px + 2, py + 15, 3, flameH);
}
// 绘制子弹
for (Bullet b : playerBullets) {
g2d.setColor(b.color);
g2d.fillRect((int) b.x - 2, (int) b.y - 6, 4, 12);
}
for (Bullet b : enemyBullets) {
g2d.setColor(b.color);
g2d.fillOval((int) b.x - 3, (int) b.y - 3, 6, 6);
}
// 绘制粒子
for (Particle p : particles) {
float alpha = p.life / (float) p.maxLife;
g2d.setColor(new Color(
p.color.getRed() / 255.0f,
p.color.getGreen() / 255.0f,
p.color.getBlue() / 255.0f,
alpha));
int s = (int) (p.size * alpha);
g2d.fillOval((int) (p.x - s / 2), (int) (p.y - s / 2), s, s);
}
// 绘制底部地面
g2d.setColor(new Color(50, 50, 50));
g2d.fillRect(0, SCREEN_HEIGHT - 20, SCREEN_WIDTH, 20);
// UI
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("Arial", Font.BOLD, 18));
g2d.drawString("分数: " + score, 20, 30);
g2d.drawString("波次: " + wave, 20, 55);
g2d.drawString("生命: " + lives, 20, 80);
if (aiMode) {
g2d.setColor(Color.CYAN);
g2d.drawString("AI 模式", 20, 105);
}
if (gameOver) {
g2d.setColor(new Color(0, 0, 0, 200));
g2d.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
g2d.setColor(Color.RED);
g2d.setFont(new Font("Arial", Font.BOLD, 48));
String msg = "GAME OVER";
FontMetrics fm = g2d.getFontMetrics();
g2d.drawString(msg, (SCREEN_WIDTH - fm.stringWidth(msg)) / 2,
SCREEN_HEIGHT / 2 - 20);
g2d.setFont(new Font("Arial", Font.BOLD, 24));
g2d.setColor(Color.WHITE);
String scoreMsg = "最终分数: " + score;
g2d.drawString(scoreMsg, (SCREEN_WIDTH - fm.stringWidth(scoreMsg)) / 2 + 20,
SCREEN_HEIGHT / 2 + 30);
}
g2d.translate(-shakeX, -shakeY);
}
@Override
public void keyPressed(KeyEvent e) {
if (aiMode) return;
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT: player.leftPressed = true; break;
case KeyEvent.VK_RIGHT: player.rightPressed = true; break;
case KeyEvent.VK_SPACE: player.shootPressed = true; break;
}
}
@Override
public void keyReleased(KeyEvent e) {
if (aiMode) return;
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT: player.leftPressed = false; break;
case KeyEvent.VK_RIGHT: player.rightPressed = false; break;
case KeyEvent.VK_SPACE: player.shootPressed = false; break;
}
}
@Override
public void keyTyped(KeyEvent e) {}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("小蜜蜂 - 敌人波浪阵型与最优射击策略");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
SpaceInvadersGame game = new SpaceInvadersGame();
frame.add(game);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
new Thread(game).start();
});
}
}
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 编队运动更新 | O(N) | O(1) | N为敌人数量 |
| 空间哈希构建 | O(B) | O(B) | B为子弹数 |
| 碰撞检测 | O(N × K) | O(B) | K为每格平均子弹数,K≈常数 |
| AI射击决策 | O(N²) | O(1) | 遍历所有敌人对 |
| 粒子更新 | O(P) | O(1) | P为粒子数 |
核心算法优化点
- 正弦波叠加:每列敌人使用不同相位的正弦波偏移,形成视觉上的波浪蠕动效果,增强游戏表现力。
- 空间哈希碰撞检测:将O(N×B)的逐对检测优化为O(N×K),当K为常数时近似O(N),显著提升大规模战斗场景的性能。
- 多因素威胁评分:AI综合考虑高度、暴露度、类型价值和水平距离四个维度,使用加权评分选择最优射击目标。
- 底部敌人优先射击:只有每列最下方的敌人才有射击权限,符合原作设计且降低子弹密度。
扩展思考
- 阵型变换:引入多种预设阵型(V字形、菱形、螺旋),通过状态机切换,增加游戏多样性。
- A*路径规划:让AI控制的玩家飞船不仅考虑射击,还需同时规划躲避 enemy bullets 的移动路径。
- 群体智能:使用Boids算法让敌人编队具有分离、对齐、聚合行为,形成更自然的群体移动。
总结
本文通过Java实现了一个完整的小蜜蜂游戏模拟,涵盖波浪阵型运动、空间哈希碰撞检测和贪心威胁评估射击策略三大核心算法。该实现不仅还原了经典街机游戏的核心机制,也展示了如何用空间分割优化碰撞检测,以及如何用多因素评分指导AI决策,为理解实时游戏AI和性能优化提供了实用范例。