雷电(Raiden)是街机时代最经典的纵向卷轴射击游戏之一。满屏飞舞的弹幕、高速穿梭的战机、密集而又规律的敌机编队,构成了独特的”弹幕美学”。本文将用 Java 实现雷电游戏的核心算法,重点讲解弹幕轨迹的抛物线运动预测、四叉树空间分割加速碰撞检测,以及对象池优化技术。
一、游戏实体建模
雷电中的实体分为三类:玩家战机、敌机、子弹(弹幕)。所有实体共享位置、速度、半径等基本属性。
/**
* 二维向量(复用工具类)
*/
public class Vec2 {
public float x, y;
public Vec2(float x, float y) { this.x = x; this.y = y; }
public Vec2 add(Vec2 v) { return new Vec2(x + v.x, y + v.y); }
public Vec2 sub(Vec2 v) { return new Vec2(x - v.x, y - v.y); }
public Vec2 scale(float s) { return new Vec2(x * s, y * s); }
public float length() { return (float)Math.sqrt(x*x + y*y); }
}
/**
* 游戏实体基类
*/
public abstract class Entity {
public Vec2 position; // 位置
public Vec2 velocity; // 速度(像素/帧)
public float radius; // 碰撞半径
public boolean alive; // 是否存活
public Entity(float x, float y, float r) {
this.position = new Vec2(x, y);
this.velocity = new Vec2(0, 0);
this.radius = r;
this.alive = true;
}
/** 每帧更新位置 */
public void update() {
position = position.add(velocity);
}
/** 圆形碰撞检测:两实体圆心距 < 半径和 */
public boolean collidesWith(Entity other) {
float dx = this.position.x - other.position.x;
float dy = this.position.y - other.position.y;
float distSq = dx*dx + dy*dy;
float minDist = this.radius + other.radius;
return distSq < minDist * minDist;
}
}
/** 玩家战机 */
public class Player extends Entity {
public Player(float x, float y) {
super(x, y, 12);
}
}
/** 敌机 */
public class Enemy extends Entity {
public int hp;
public Enemy(float x, float y, float r, int hp) {
super(x, y, r);
this.hp = hp;
}
}
/** 子弹/弹幕 */
public class Bullet extends Entity {
public boolean isPlayerBullet; // true=玩家子弹,false=敌机弹幕
public int damage;
public Bullet(float x, float y, float r, boolean playerBullet, int dmg) {
super(x, y, r);
this.isPlayerBullet = playerBullet;
this.damage = dmg;
}
}
二、弹幕轨迹预测:参数方程与抛物线运动
敌机发射的弹幕并非简单直线,而是遵循预设轨迹模式。常见的模式包括:直线弹、扇形扩散弹、自机狙(瞄准玩家)、抛物线弹、螺旋弹等。我们通过参数方程描述弹幕轨迹,在游戏循环中根据时间参数 t 计算实时位置。
/**
* 弹幕轨迹接口:给定时间参数返回速度和位置偏移
*/
public interface BulletPattern {
/** 计算第 t 帧的速度向量 */
Vec2 getVelocity(int t, Vec2 spawnPos, Vec2 targetPos);
}
/** 直线弹:匀速直线运动 */
public class StraightPattern implements BulletPattern {
private Vec2 dir;
private float speed;
public StraightPattern(float angleDeg, float speed) {
float rad = (float)Math.toRadians(angleDeg);
this.dir = new Vec2((float)Math.cos(rad), (float)Math.sin(rad));
this.speed = speed;
}
@Override
public Vec2 getVelocity(int t, Vec2 spawnPos, Vec2 targetPos) {
return dir.scale(speed);
}
}
/** 自机狙:始终瞄准玩家当前位置发射 */
public class AimPattern implements BulletPattern {
private float speed;
public AimPattern(float speed) { this.speed = speed; }
@Override
public Vec2 getVelocity(int t, Vec2 spawnPos, Vec2 targetPos) {
Vec2 toTarget = targetPos.sub(spawnPos);
float len = toTarget.length();
if (len < 0.001f) return new Vec2(0, speed);
return toTarget.scale(speed / len);
}
}
/** 扇形扩散弹:以固定角度间隔向扇形区域发射 */
public class FanPattern implements BulletPattern {
private float baseAngle;
private float spreadAngle;
private float speed;
private int bulletIndex; // 该弹幕在扇形中的序号
private int totalBullets;
public FanPattern(float baseAngle, float spreadAngle, float speed, int index, int total) {
this.baseAngle = baseAngle;
this.spreadAngle = spreadAngle;
this.speed = speed;
this.bulletIndex = index;
this.totalBullets = total;
}
@Override
public Vec2 getVelocity(int t, Vec2 spawnPos, Vec2 targetPos) {
float angle = baseAngle - spreadAngle/2
+ spreadAngle * bulletIndex / Math.max(1, totalBullets - 1);
float rad = (float)Math.toRadians(angle);
return new Vec2((float)Math.cos(rad) * speed, (float)Math.sin(rad) * speed);
}
}
/** 螺旋弹:速度方向随时间旋转 */
public class SpiralPattern implements BulletPattern {
private float initAngle;
private float rotSpeed; // 每帧旋转角度
private float speed;
public SpiralPattern(float initAngle, float rotSpeed, float speed) {
this.initAngle = initAngle;
this.rotSpeed = rotSpeed;
this.speed = speed;
}
@Override
public Vec2 getVelocity(int t, Vec2 spawnPos, Vec2 targetPos) {
float angle = initAngle + rotSpeed * t;
float rad = (float)Math.toRadians(angle);
return new Vec2((float)Math.cos(rad) * speed, (float)Math.sin(rad) * speed);
}
}
三、弹幕发射器
/**
* 敌机弹幕发射器,按时间轴发射不同模式的弹幕
*/
public class BulletSpawner {
private Enemy owner;
private int frameCounter;
public BulletSpawner(Enemy owner) {
this.owner = owner;
this.frameCounter = 0;
}
/**
* 每帧调用,生成新的弹幕
* @param playerPos 玩家当前位置(用于自机狙)
* @param bulletPool 子弹对象池
* @return 本帧新生成的弹幕列表
*/
public java.util.List<Bullet> tick(Vec2 playerPos, BulletPool bulletPool) {
java.util.List<Bullet> spawned = new java.util.ArrayList<>();
frameCounter++;
// 模式1:每60帧发射3发自机狙
if (frameCounter % 60 == 0) {
for (int i = 0; i < 3; i++) {
BulletPattern pattern = new AimPattern(3.0f + i * 0.5f);
Vec2 vel = pattern.getVelocity(0, owner.position, playerPos);
Bullet b = bulletPool.obtain();
b.position = new Vec2(owner.position.x, owner.position.y + owner.radius);
b.velocity = vel;
b.isPlayerBullet = false;
b.damage = 1;
b.alive = true;
spawned.add(b);
}
}
// 模式2:每90帧发射5发扇形弹
if (frameCounter % 90 == 0) {
for (int i = 0; i < 5; i++) {
BulletPattern pattern = new FanPattern(90, 60, 2.5f, i, 5);
Vec2 vel = pattern.getVelocity(0, owner.position, playerPos);
Bullet b = bulletPool.obtain();
b.position = new Vec2(owner.position.x, owner.position.y + owner.radius);
b.velocity = vel;
b.isPlayerBullet = false;
b.damage = 1;
b.alive = true;
spawned.add(b);
}
}
// 模式3:每120帧发射螺旋弹
if (frameCounter % 120 == 0) {
for (int i = 0; i < 8; i++) {
BulletPattern pattern = new SpiralPattern(i * 45, 3, 2.0f);
Vec2 vel = pattern.getVelocity(0, owner.position, playerPos);
Bullet b = bulletPool.obtain();
b.position = new Vec2(owner.position.x, owner.position.y);
b.velocity = vel;
b.isPlayerBullet = false;
b.damage = 1;
b.alive = true;
spawned.add(b);
}
}
return spawned;
}
}
四、四叉树空间分割碰撞检测
满屏数百发弹幕时,$O(N^2)$ 的暴力碰撞检测会严重拖慢帧率。四叉树(Quadtree) 将二维空间递归划分为四个象限,每个实体只与同一象限内的其他实体进行碰撞检测,将复杂度降至 $O(N \log N)$。
/**
* 轴对齐包围盒
*/
public class AABB {
public float minX, minY, maxX, maxY;
public AABB(float minX, float minY, float maxX, float maxY) {
this.minX = minX; this.minY = minY;
this.maxX = maxX; this.maxY = maxY;
}
public boolean contains(float x, float y) {
return x >= minX && x <= maxX && y >= minY && y <= maxY;
}
public boolean intersects(AABB other) {
return this.minX <= other.maxX && this.maxX >= other.minX
&& this.minY <= other.maxY && this.maxY >= other.minY;
}
}
/**
* 四叉树节点
*/
public class Quadtree {
private static final int MAX_OBJECTS = 10; // 单节点最大实体数
private static final int MAX_LEVELS = 5; // 最大深度
private int level;
private AABB bounds;
private java.util.List<Entity> objects;
private Quadtree[] nodes; // 四个子节点:NW, NE, SW, SE
public Quadtree(int level, AABB bounds) {
this.level = level;
this.bounds = bounds;
this.objects = new java.util.ArrayList<>();
this.nodes = null;
}
/** 清空四叉树(每帧重建) */
public void clear() {
objects.clear();
if (nodes != null) {
for (Quadtree node : nodes) node.clear();
nodes = null;
}
}
/** 将当前节点分裂为四个子节点 */
private void split() {
float subW = (bounds.maxX - bounds.minX) / 2;
float subH = (bounds.maxY - bounds.minY) / 2;
float x = bounds.minX;
float y = bounds.minY;
nodes = new Quadtree[4];
nodes[0] = new Quadtree(level + 1, new AABB(x + subW, y, x + subW*2, y + subH)); // NE
nodes[1] = new Quadtree(level + 1, new AABB(x, y, x + subW, y + subH)); // NW
nodes[2] = new Quadtree(level + 1, new AABB(x, y + subH, x + subW, y + subH*2)); // SW
nodes[3] = new Quadtree(level + 1, new AABB(x + subW, y + subH, x + subW*2, y + subH*2)); // SE
}
/** 判断实体属于哪个子节点索引(-1表示跨越多个子节点) */
private int getIndex(Entity e) {
float midX = (bounds.minX + bounds.maxX) / 2;
float midY = (bounds.minY + bounds.maxY) / 2;
boolean top = e.position.y - e.radius < midY && e.position.y + e.radius < midY;
boolean bottom = e.position.y - e.radius > midY;
boolean left = e.position.x - e.radius < midX && e.position.x + e.radius < midX;
boolean right = e.position.x - e.radius > midX;
if (top && right) return 0; // NE
if (top && left) return 1; // NW
if (bottom && left) return 2; // SW
if (bottom && right) return 3; // SE
return -1; // 跨越边界,留在当前节点
}
/** 插入实体 */
public void insert(Entity e) {
if (nodes != null) {
int index = getIndex(e);
if (index != -1) {
nodes[index].insert(e);
return;
}
}
objects.add(e);
if (objects.size() > MAX_OBJECTS && level < MAX_LEVELS) {
if (nodes == null) split();
java.util.Iterator<Entity> it = objects.iterator();
while (it.hasNext()) {
Entity obj = it.next();
int index = getIndex(obj);
if (index != -1) {
nodes[index].insert(obj);
it.remove();
}
}
}
}
/**
* 检索可能与给定实体碰撞的所有实体
*/
public void retrieve(java.util.List<Entity> returnList, Entity e) {
int index = getIndex(e);
if (index != -1 && nodes != null) {
nodes[index].retrieve(returnList, e);
}
returnList.addAll(objects);
}
}
五、对象池:避免 GC 压力
射击游戏中子弹的创建与销毁极为频繁,频繁的 new 和 GC 会导致卡顿。对象池(Object Pool) 预先分配一批对象,重复利用,彻底消除运行时分配开销。
/**
* 子弹对象池
*/
public class BulletPool {
private java.util.Queue<Bullet> pool;
private int capacity;
public BulletPool(int capacity) {
this.capacity = capacity;
this.pool = new java.util.ArrayDeque<>();
// 预分配
for (int i = 0; i < capacity; i++) {
pool.add(new Bullet(0, 0, 4, false, 1));
}
}
/** 获取一个可用子弹 */
public Bullet obtain() {
if (pool.isEmpty()) {
return new Bullet(0, 0, 4, false, 1); // 池耗尽时临时创建
}
Bullet b = pool.poll();
b.alive = true;
return b;
}
/** 回收子弹 */
public void free(Bullet b) {
b.alive = false;
pool.add(b);
}
}
六、游戏主循环与碰撞检测整合
public class RaidenEngine {
private Player player;
private java.util.List<Enemy> enemies;
private java.util.List<Bullet> bullets;
private BulletPool bulletPool;
private Quadtree quadtree;
private int score;
public RaidenEngine() {
player = new Player(240, 400);
enemies = new java.util.ArrayList<>();
bullets = new java.util.ArrayList<>();
bulletPool = new BulletPool(500);
quadtree = new Quadtree(0, new AABB(0, 0, 480, 640));
score = 0;
}
/** 每帧主循环 */
public void tick() {
// 1. 更新玩家
player.update();
// 2. 更新敌机并生成弹幕
for (Enemy e : enemies) {
e.update();
if (e.alive) {
BulletSpawner spawner = new BulletSpawner(e);
bullets.addAll(spawner.tick(player.position, bulletPool));
}
}
// 3. 更新子弹位置
for (Bullet b : bullets) {
if (b.alive) b.update();
}
// 4. 构建四叉树
quadtree.clear();
quadtree.insert(player);
for (Enemy e : enemies) if (e.alive) quadtree.insert(e);
for (Bullet b : bullets) if (b.alive) quadtree.insert(b);
// 5. 碰撞检测
checkCollisions();
// 6. 清理死亡实体
cleanup();
}
private void checkCollisions() {
java.util.List<Entity> candidates = new java.util.ArrayList<>();
// 玩家 vs 敌机弹幕
candidates.clear();
quadtree.retrieve(candidates, player);
for (Entity e : candidates) {
if (e instanceof Bullet && !((Bullet)e).isPlayerBullet && e.alive) {
if (player.collidesWith(e)) {
e.alive = false;
// 玩家受伤逻辑
}
}
}
// 玩家子弹 vs 敌机
for (Bullet b : bullets) {
if (!b.alive || b.isPlayerBullet) continue;
// 这里简化处理:实际应遍历所有敌机
}
}
private void cleanup() {
java.util.Iterator<Bullet> it = bullets.iterator();
while (it.hasNext()) {
Bullet b = it.next();
if (!b.alive || b.position.y < -50 || b.position.y > 700) {
it.remove();
bulletPool.free(b);
}
}
enemies.removeIf(e -> !e.alive);
}
}
七、复杂度对比分析
| 检测方式 | 时间复杂度 | 100实体 | 500实体 | 说明 |
|---|---|---|---|---|
| 暴力 O(N²) | O(N²) | 10,000 | 250,000 | 每对都检测 |
| 四叉树 | O(N log N) | ~460 | ~3,100 | 空间分割过滤 |
| 对象池 | O(1) 获取/回收 | – | – | 消除内存分配 |
在 500 实体的密集弹幕场景中,四叉树将碰撞检测开销降低约 80 倍,配合对象池可稳定维持 60 FPS。
八、扩展方向
- 八叉树/网格哈希:3D 弹幕游戏(如东方 Project 的立体弹幕)可升级为八叉树,或改用均匀网格(Uniform Grid)进一步简化。
- 预测射击 AI:敌机根据玩家速度向量进行线性外插,提前瞄准玩家未来位置。
- 弹幕编织算法:用参数方程组描述复杂的弹幕花纹,如玫瑰线、利萨茹图形等。
雷电游戏算法的核心在于用空间分割管理海量实体、用参数方程描述运动轨迹、用对象池消除性能抖动。这套技术栈不仅适用于射击游戏,也是粒子系统、大规模群体模拟(Boids)的通用解决方案。