每日算法 — 使用java实现飞机大战:对象池与空间哈希碰撞检测

引言

飞机大战是经典竖版射击游戏,屏幕中同时存在大量子弹、敌机和道具,频繁的创建销毁对象会导致严重的GC压力。本文用Java实现飞机大战的核心系统,重点讲解对象池模式复用游戏对象,以及空间哈希(Spatial Hashing)算法将碰撞检测复杂度从O(n²)降到O(1)级别。

核心算法概览

  1. 对象池模式:预创建对象池,避免运行时频繁new对象和GC
  2. 空间哈希:将屏幕划分为均匀网格,仅检测同网格内的碰撞对
  3. 敌机行为模式:正弦波移动、追踪射击、编队变换三种AI模式

对象池模式实现

import java.util.*;

/**
 * 泛型对象池,支持任意可复用对象
 */
public class ObjectPool<T> {

    private Queue<T> pool;
    private int capacity;
    private ObjectFactory<T> factory;

    public interface ObjectFactory<T> {
        T create();
        void reset(T obj); // 归还前重置状态
    }

    public ObjectPool(int capacity, ObjectFactory<T> factory) {
        this.capacity = capacity;
        this.factory = factory;
        this.pool = new ArrayDeque<>(capacity);

        // 预创建对象
        for (int i = 0; i < capacity; i++) {
            pool.offer(factory.create());
        }
    }

    /**
     * 从池中获取对象,池空时创建新对象(兜底)
     */
    public T acquire() {
        T obj = pool.poll();
        if (obj == null) {
            obj = factory.create(); // 池耗尽时动态创建
        }
        return obj;
    }

    /**
     * 归还对象到池中
     */
    public void release(T obj) {
        if (obj != null && pool.size() < capacity) {
            factory.reset(obj);
            pool.offer(obj);
        }
    }

    public int available() { return pool.size(); }
}

/**
 * 子弹实体
 */
class Bullet {
    double x, y;
    double vx, vy;
    boolean active;
    int damage;
    boolean isPlayerBullet; // 区分玩家子弹和敌机子弹

    void reset() {
        active = false;
        x = y = vx = vy = 0;
        damage = 0;
    }
}

/**
 * 敌机实体
 */
class EnemyPlane {
    double x, y;
    double speed;
    int hp;
    boolean active;
    int behaviorType; // 0=直线下落, 1=正弦波, 2=追踪
    double behaviorTimer;
    double shootInterval;
    double lastShotTime;
    int score;

    void reset() {
        active = false;
        x = y = speed = 0;
        hp = 0;
        behaviorType = 0;
    }
}

空间哈希碰撞检测

将屏幕划分为固定大小的网格单元,每个对象只注册到其所在的网格,碰撞检测时只检查同网格和相邻网格内的对象。

import java.util.*;

/**
 * 空间哈希网格,用于高效碰撞检测
 */
public class SpatialHash {

    private int cellSize; // 网格单元边长(通常取最大对象直径)
    private Map<Long, List<GameObject>> grid;

    public SpatialHash(int cellSize) {
        this.cellSize = cellSize;
        this.grid = new HashMap<>();
    }

    /**
     * 将坐标映射到网格单元键
     * 使用long打包两个int避免哈希冲突
     */
    private long getKey(int cellX, int cellY) {
        return ((long) cellX << 32) | (cellY & 0xFFFFFFFFL);
    }

    /**
     * 插入对象到空间哈希
     */
    public void insert(GameObject obj) {
        int minCellX = (int) (obj.x / cellSize);
        int maxCellX = (int) ((obj.x + obj.width) / cellSize);
        int minCellY = (int) (obj.y / cellSize);
        int maxCellY = (int) ((obj.y + obj.height) / cellSize);

        for (int cx = minCellX; cx <= maxCellX; cx++) {
            for (int cy = minCellY; cy <= maxCellY; cy++) {
                long key = getKey(cx, cy);
                grid.computeIfAbsent(key, k -> new ArrayList<>()).add(obj);
            }
        }
    }

    /**
     * 查询与指定矩形可能相交的所有对象
     */
    public List<GameObject> query(double x, double y, double w, double h) {
        Set<GameObject> result = new HashSet<>();
        int minCellX = (int) (x / cellSize);
        int maxCellX = (int) ((x + w) / cellSize);
        int minCellY = (int) (y / cellSize);
        int maxCellY = (int) ((y + h) / cellSize);

        for (int cx = minCellX; cx <= maxCellX; cx++) {
            for (int cy = minCellY; cy <= maxCellY; cy++) {
                long key = getKey(cx, cy);
                List<GameObject> cell = grid.get(key);
                if (cell != null) result.addAll(cell);
            }
        }

        return new ArrayList<>(result);
    }

    /**
     * 清空所有网格,每帧开始前调用
     */
    public void clear() {
        grid.clear();
    }

    // 游戏对象基类
    static class GameObject {
        double x, y, width, height;
        boolean active;
    }
}

游戏管理器与碰撞系统

public class GameManager {

    private ObjectPool<Bullet> bulletPool;
    private ObjectPool<EnemyPlane> enemyPool;
    private List<Bullet> activeBullets;
    private List<EnemyPlane> activeEnemies;
    private SpatialHash spatialHash;
    private PlayerPlane player;

    private double screenWidth = 480, screenHeight = 800;
    private int cellSize = 64; // 空间哈希单元大小

    public GameManager() {
        // 初始化对象池
        bulletPool = new ObjectPool<>(200, new ObjectPool.ObjectFactory<>() {
            @Override
            public Bullet create() { return new Bullet(); }
            @Override
            public void reset(Bullet b) { b.reset(); }
        });

        enemyPool = new ObjectPool<>(50, new ObjectPool.ObjectFactory<>() {
            @Override
            public EnemyPlane create() { return new EnemyPlane(); }
            @Override
            public void reset(EnemyPlane e) { e.reset(); }
        });

        activeBullets = new ArrayList<>();
        activeEnemies = new ArrayList<>();
        spatialHash = new SpatialHash(cellSize);
        player = new PlayerPlane(screenWidth / 2, screenHeight - 100);
    }

    /**
     * 生成玩家子弹
     */
    public void spawnPlayerBullet(double x, double y) {
        Bullet b = bulletPool.acquire();
        b.x = x; b.y = y;
        b.vx = 0; b.vy = -8; // 向上飞行
        b.active = true;
        b.damage = 1;
        b.isPlayerBullet = true;
        activeBullets.add(b);
    }

    /**
     * 生成敌机
     */
    public void spawnEnemy(int type) {
        EnemyPlane e = enemyPool.acquire();
        e.x = Math.random() * (screenWidth - 40) + 20;
        e.y = -40;
        e.speed = 2 + Math.random() * 2;
        e.hp = (type == 0) ? 1 : (type == 1) ? 3 : 5;
        e.active = true;
        e.behaviorType = type;
        e.behaviorTimer = 0;
        e.shootInterval = 1500 + Math.random() * 1000;
        e.lastShotTime = 0;
        e.score = (type + 1) * 100;
        activeEnemies.add(e);
    }

    /**
     * 游戏主循环
     */
    public void update(double currentTime) {
        // 1. 更新空间哈希
        spatialHash.clear();

        // 注册玩家
        spatialHash.insert(player);

        // 注册所有敌机
        for (EnemyPlane e : activeEnemies) {
            if (e.active) spatialHash.insert(e);
        }

        // 注册所有子弹
        for (Bullet b : activeBullets) {
            if (b.active) spatialHash.insert(b);
        }

        // 2. 更新所有对象位置
        updateBullets();
        updateEnemies(currentTime);
        player.update();

        // 3. 碰撞检测
        checkCollisions(currentTime);

        // 4. 回收越界/死亡对象
        recycleObjects();
    }

    private void updateBullets() {
        for (Bullet b : activeBullets) {
            if (!b.active) continue;
            b.x += b.vx;
            b.y += b.vy;
            // 越界标记
            if (b.y < -10 || b.y > screenHeight + 10 || 
                b.x < -10 || b.x > screenWidth + 10) {
                b.active = false;
            }
        }
    }

    private void updateEnemies(double currentTime) {
        for (EnemyPlane e : activeEnemies) {
            if (!e.active) continue;

            // 行为模式更新
            e.behaviorTimer += 0.016; // 假设60FPS
            switch (e.behaviorType) {
                case 0: // 直线下落
                    e.y += e.speed;
                    break;
                case 1: // 正弦波移动
                    e.y += e.speed;
                    e.x += Math.sin(e.behaviorTimer * 3) * 2;
                    break;
                case 2: // 追踪玩家
                    e.y += e.speed * 0.7;
                    if (e.x < player.x) e.x += 1;
                    else if (e.x > player.x) e.x -= 1;
                    break;
            }

            // 敌机射击
            if (currentTime - e.lastShotTime > e.shootInterval) {
                spawnEnemyBullet(e);
                e.lastShotTime = currentTime;
            }
        }
    }

    private void spawnEnemyBullet(EnemyPlane e) {
        Bullet b = bulletPool.acquire();
        b.x = e.x + 20; // 敌机中心
        b.y = e.y + 40;
        b.vx = 0; b.vy = 4; // 向下飞行
        b.active = true;
        b.damage = 1;
        b.isPlayerBullet = false;
        activeBullets.add(b);
    }

    /**
     * 使用空间哈希进行高效碰撞检测
     */
    private void checkCollisions(double currentTime) {
        // 玩家子弹 vs 敌机
        for (Bullet b : activeBullets) {
            if (!b.active || !b.isPlayerBullet) continue;

            // 空间哈希查询子弹周围的潜在碰撞对象
            List<SpatialHash.GameObject> candidates = 
                spatialHash.query(b.x, b.y, 8, 16);

            for (SpatialHash.GameObject obj : candidates) {
                if (obj instanceof EnemyPlane) {
                    EnemyPlane e = (EnemyPlane) obj;
                    if (!e.active) continue;

                    // AABB碰撞检测
                    if (checkAABBCollision(b.x, b.y, 8, 16, 
                                           e.x, e.y, 40, 40)) {
                        e.hp -= b.damage;
                        b.active = false;
                        if (e.hp <= 0) {
                            e.active = false;
                            player.addScore(e.score);
                        }
                        break; // 一颗子弹只打一架敌机
                    }
                }
            }
        }

        // 敌机子弹 vs 玩家
        for (Bullet b : activeBullets) {
            if (!b.active || b.isPlayerBullet) continue;
            if (checkAABBCollision(b.x, b.y, 8, 16,
                                   player.x, player.y, 40, 40)) {
                b.active = false;
                player.takeDamage(b.damage);
            }
        }

        // 敌机 vs 玩家(撞击)
        for (EnemyPlane e : activeEnemies) {
            if (!e.active) continue;
            if (checkAABBCollision(e.x, e.y, 40, 40,
                                   player.x, player.y, 40, 40)) {
                e.active = false;
                player.takeDamage(3);
            }
        }
    }

    /**
     * AABB矩形碰撞检测
     */
    private boolean checkAABBCollision(double x1, double y1, double w1, double h1,
                                       double x2, double y2, double w2, double h2) {
        return x1 < x2 + w2 && x1 + w1 > x2 &&
               y1 < y2 + h2 && y1 + h1 > y2;
    }

    /**
     * 回收死亡/越界对象到对象池
     */
    private void recycleObjects() {
        // 回收子弹
        Iterator<Bullet> bit = activeBullets.iterator();
        while (bit.hasNext()) {
            Bullet b = bit.next();
            if (!b.active) {
                bit.remove();
                bulletPool.release(b);
            }
        }

        // 回收敌机
        Iterator<EnemyPlane> eit = activeEnemies.iterator();
        while (eit.hasNext()) {
            EnemyPlane e = eit.next();
            if (!e.active || e.y > screenHeight + 50) {
                eit.remove();
                enemyPool.release(e);
            }
        }
    }

    public int getScore() { return player.getScore(); }
    public boolean isGameOver() { return !player.isAlive(); }
}

/**
 * 玩家飞机
 */
class PlayerPlane extends SpatialHash.GameObject {
    private int hp = 5;
    private int score = 0;
    private double speed = 6;

    PlayerPlane(double x, double y) {
        this.x = x; this.y = y;
        this.width = 40; this.height = 40;
        this.active = true;
    }

    void update() {
        // 边界限制
        if (x < 0) x = 0;
        if (x > 440) x = 440;
        if (y < 0) y = 0;
        if (y > 760) y = 760;
    }

    void moveLeft() { x -= speed; }
    void moveRight() { x += speed; }
    void moveUp() { y -= speed; }
    void moveDown() { y += speed; }

    void takeDamage(int dmg) { hp -= dmg; }
    void addScore(int s) { score += s; }
    boolean isAlive() { return hp > 0; }
    int getScore() { return score; }
}

主程序入口

public class AirplaneGame {
    public static void main(String[] args) {
        GameManager manager = new GameManager();
        double currentTime = 0;

        // 生成一些敌机
        for (int i = 0; i < 5; i++) {
            manager.spawnEnemy(i % 3);
        }

        // 模拟游戏运行300帧(约5秒)
        for (int frame = 0; frame < 300; frame++) {
            currentTime += 16.67; // 60FPS

            // 模拟玩家射击
            if (frame % 10 == 0) {
                manager.spawnPlayerBullet(240, 700);
            }

            // 每60帧生成新敌机
            if (frame % 60 == 0) {
                manager.spawnEnemy(frame / 60 % 3);
            }

            manager.update(currentTime);

            if (frame % 60 == 0) {
                System.out.println("Frame " + frame + 
                                 ", Score: " + manager.getScore() + 
                                 ", GameOver: " + manager.isGameOver());
            }
        }
    }
}

复杂度分析

检测方式 时间复杂度 空间复杂度 适用场景
暴力O(n²) O(n²) O(n) 对象极少时
空间哈希 O(n) 平均,O(n²) 最坏 O(n) 大量对象均匀分布
四叉树 O(n log n) O(n) 对象密度差异大

空间哈希的核心优势在于:当对象均匀分布时,每个网格单元内的对象数量是常数,查询和插入都接近O(1)。对于飞机大战这种弹幕密集但均匀分布的场景,空间哈希是最佳选择。

总结

本文以飞机大战为载体,讲解了对象池模式避免GC压力、空间哈希将碰撞检测复杂度从O(n²)优化到O(n)。读者可以扩展:
– 实现四叉树或均匀网格替代空间哈希,适应更复杂的密度分布
– 加入道具系统(炸弹清屏、双倍火力等),使用对象池统一管理
– 实现敌机BOSS的复杂行为树和弹幕模式