概述
太空侵略者(Space Invaders)是1978年推出的经典街机射击游戏。玩家操控飞船击落从屏幕上方逐步逼近的外星入侵者。本文将用Java实现该游戏的核心算法,重点讲解外星敌人的波次生成算法、基于网格系统的移动策略,以及AABB碰撞检测在子弹与敌人之间的应用。
核心算法设计
1. 外星入侵者网格布局
游戏中的敌人按行列排成整齐的网格,每波敌人有固定的行列数。我们使用二维数组存储敌人状态,并通过统一的步长控制整体移动。
public class InvaderGrid {
private Invader[][] grid;
private int rows, cols;
private float x, y; // 网格左上角坐标
private float stepX = 15f; // 水平步长
private float stepY = 20f; // 垂直步长(碰到边界后下沉)
private int direction = 1; // 1=右移, -1=左移
private float speed = 1.0f; // 速度倍率(随数量减少而加快)
public InvaderGrid(int rows, int cols, float startX, float startY) {
this.rows = rows;
this.cols = cols;
this.x = startX;
this.y = startY;
grid = new Invader[rows][cols];
initGrid();
}
private void initGrid() {
float spacingX = 40f;
float spacingY = 30f;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
// 不同行敌人分值不同(越靠上分值越高)
int type = (r < 1) ? 3 : (r < 3) ? 2 : 1;
grid[r][c] = new Invader(
x + c * spacingX,
y + r * spacingY,
type
);
}
}
}
}
2. 波次移动与边界检测
所有存活的外星人同步移动,当任意一列触及屏幕边界时,整批敌人下移并反向。速度随存活数量减少而递增,制造逐渐紧张的游戏节奏。
public void update(float screenWidth, float screenHeight) {
boolean hitEdge = false;
float spacingX = 40f;
float rightmost = 0, leftmost = screenWidth;
// 计算存活敌人的最左和最右位置
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
Invader inv = grid[r][c];
if (inv != null && inv.isAlive()) {
float ex = inv.getX();
if (ex < leftmost) leftmost = ex;
if (ex > rightmost) rightmost = ex;
}
}
}
// 检测是否触及边界
if (rightmost >= screenWidth - 30 || leftmost <= 10) {
hitEdge = true;
}
if (hitEdge) {
direction *= -1; // 反向
y += stepY; // 整体下移
// 速度递增:存活越少越快
speed = 1.0f + (1.0f - getAliveRatio()) * 2.5f;
}
// 统一更新所有存活敌人位置
float dx = direction * stepX * speed;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
Invader inv = grid[r][c];
if (inv != null && inv.isAlive()) {
inv.move(dx, hitEdge ? stepY : 0);
}
}
}
}
private float getAliveRatio() {
int alive = 0, total = rows * cols;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] != null && grid[r][c].isAlive()) alive++;
}
}
return (float) alive / total;
}
3. AABB碰撞检测
子弹与敌人、敌人子弹与玩家飞船之间均使用轴对齐包围盒(AABB)碰撞检测。该方法仅需比较四个边界值,计算复杂度为O(1)。
public class AABB {
public float x, y, width, height;
public AABB(float x, float y, float width, float height) {
this.x = x; this.y = y;
this.width = width; this.height = height;
}
/**
* AABB碰撞检测核心算法
* 两个矩形相交当且仅当在X轴和Y轴上的投影均重叠
*/
public boolean intersects(AABB other) {
return this.x < other.x + other.width &&
this.x + this.width > other.x &&
this.y < other.y + other.height &&
this.y + this.height > other.y;
}
}
4. 敌人随机射击算法
存活敌人中随机选择若干名进行射击,射击概率与波次进度和存活数量相关。
public List<Bullet> tryFire(Random rand) {
List<Bullet> bullets = new ArrayList<>();
List<Invader> alive = new ArrayList<>();
// 收集所有存活敌人
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] != null && grid[r][c].isAlive()) {
alive.add(grid[r][c]);
}
}
}
// 射击概率:基础概率 + 波次加成
double baseChance = 0.02;
double waveBonus = waveNumber * 0.005;
double chance = Math.min(baseChance + waveBonus, 0.08);
for (Invader inv : alive) {
if (rand.nextDouble() < chance) {
// 只有底部敌人才射击(避免子弹穿过上方敌人)
if (isBottommost(inv)) {
bullets.add(new Bullet(inv.getX() + 15, inv.getY() + 20, 3f));
}
}
}
return bullets;
}
private boolean isBottommost(Invader target) {
int col = target.getCol();
for (int r = rows - 1; r >= 0; r--) {
if (grid[r][col] != null && grid[r][col].isAlive()) {
return grid[r][col] == target;
}
}
return false;
}
5. 完整运行示例
public class SpaceInvadersGame {
private InvaderGrid grid;
private PlayerShip player;
private List<Bullet> playerBullets = new ArrayList<>();
private List<Bullet> enemyBullets = new ArrayList<>();
private Random rand = new Random();
private int score = 0;
private int waveNumber = 1;
public SpaceInvadersGame() {
grid = new InvaderGrid(5, 11, 50, 50);
player = new PlayerShip(400, 550);
}
public void update(float screenWidth, float screenHeight) {
// 更新敌人网格
grid.update(screenWidth, screenHeight);
// 敌人随机射击
enemyBullets.addAll(grid.tryFire(rand));
// 更新子弹位置
for (Bullet b : playerBullets) b.move(0, -6f);
for (Bullet b : enemyBullets) b.move(0, 4f);
// 碰撞检测:玩家子弹 vs 敌人
Iterator<Bullet> pit = playerBullets.iterator();
while (pit.hasNext()) {
Bullet pb = pit.next();
Invader hit = grid.findCollision(pb.getBounds());
if (hit != null) {
hit.setAlive(false);
score += hit.getScore();
pit.remove();
}
}
// 碰撞检测:敌人子弹 vs 玩家
for (Bullet eb : enemyBullets) {
if (eb.getBounds().intersects(player.getBounds())) {
player.hit();
}
}
// 清理越界子弹
playerBullets.removeIf(b -> b.getY() < 0);
enemyBullets.removeIf(b -> b.getY() > screenHeight);
// 检查波次结束
if (grid.allDestroyed()) {
waveNumber++;
grid = new InvaderGrid(5, 11, 50, 50 - waveNumber * 5);
}
}
public void playerShoot() {
playerBullets.add(new Bullet(player.getX() + 18, player.getY(), -8f));
}
}
复杂度分析
| 算法模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 网格移动更新 | O(R×C) | O(R×C) | R=行数, C=列数,遍历全部格子 |
| AABB碰撞检测 | O(1) | O(1) | 单次碰撞仅需4次比较 |
| 子弹-敌人碰撞 | O(B×A) | O(1) | B=子弹数, A=存活敌人数 |
| 底部敌人筛选 | O(R) | O(1) | 固定列的从上至下扫描 |
总结
太空侵略者的核心算法围绕网格同步移动、边界触发反向和AABB碰撞检测三大机制展开。速度随存活数量递减而递增的设计,是典型的难度曲线控制策略。将敌人按行列组织成二维网格,不仅便于统一管理移动,也为后续的波次生成和阵型变化提供了良好的数据结构基础。