引言

在V1版本中,我们实现了玩家飞机移动、敌方自动生成和简单的子弹框架,但游戏缺少核心玩法——射击和碰撞,敌方也不会攻击。V2版本针对这些痛点进行了全面升级,真正让游戏“活”了起来。

本文详细解析V2版本的核心改进,涵盖:

  • 线程安全集合解决并发修改异常

  • 双缓冲技术 + 滚动背景,消除闪烁、增强视觉效果

  • 完整的碰撞检测系统(子弹与飞机、敌机与玩家)

  • 生命值与积分系统,并可视化血条

  • 敌方AI:自动射击 + 随机水平移动,增加游戏难度

  • 玩家射击功能(空格键)

  • 统一图片管理(ImageManager

让我们一步步拆解这些新特性,看看它们是如何实现的。


项目架构

V2版本在V1基础上新增了两个类,并对原有类进行了重构:

类名 职责
GameUI 主窗口,改用CopyOnWriteArrayList存储游戏元素,启动自动射击线程
GameThread 游戏主线程,增加了碰撞检测、积分血量绘制、双缓冲滚动背景
AutoFighterThread 自动生成敌方,调整生成间隔和敌方速度
AutoShootThread 新增:控制所有敌方飞机自动发射子弹,并随机改变其水平速度(走位)
Fighter 飞机类,新增shoot()方法,玩家射击用;敌方移动增加水平速度
Bullet 子弹类,区分敌我图片,尺寸不同,移动边界判断增加上边界
GameListener 键盘监听,增加空格键发射子弹,并传入子弹列表
ImageManager 新增:统一加载和存放所有图片资源,避免各类型重复加载
Data 常量接口,保持不变

项目新功能详解

1. 线程安全:从 ArrayList 到 CopyOnWriteArrayList

问题背景

V1版本中,AutoFighterThread 向 fighters 添加敌机,同时 GameThread 遍历 fighters 进行绘制和移动,容易抛出 ConcurrentModificationException

解决方案

为了图方便,我直接使用了 java.util.concurrent.CopyOnWriteArrayList,用这样一个并发容器在写操作时创建底层数组的新副本,读操作直接返回原数组

// GameUI.java
CopyOnWriteArrayList<Fighter> fighters = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList<Bullet> enBullets = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList<Bullet> myBullets = new CopyOnWriteArrayList<>();

解析

  • 所有集合声明为 CopyOnWriteArrayList,并传递到各个线程。

  • 在 GameThread 中遍历时,不再需要手动同步,也不会出现并发异常。

但是,它的代价是写操作(添加/删除)有额外开销,(附上:比如他add方法的源代码)

 public boolean add(E e) {
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            setArray(es);
            return true;
        }
    }

但还好是现在游戏中元素数量不多。

总体来说性能比较低,只适合小游戏,读多写少的情况。(后续还需要调整)

2. 图片管理:ImageManager 封装统一加载

问题背景
V1中每个类都单独加载自己的图片,且使用绝对路径,重复代码多、移植性差。

解决方法
创建 ImageManager 类,集中加载所有图片,并提供静态字段供全局访问。

public class ImageManager {
    public static Image myFighterImage;
    public static Image enemyImage;
    public static Image myBulletImage;
    public static Image enemyBulletImage;
    public static Image backgroundImage;

    static {
        String myFighterPath = "C:\\Users\\zgr\\Desktop\\代码库\\飞机大战\\GameV1\\pictures\\myfighternobg.png";
        // ... 其他路径
        try {
            myFighterImage = ImageIO.read(new File(myFighterPath));
            // ... 加载其他图片
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    private ImageManager() {}
}

解析

  • 静态初始化块在类加载时执行,确保所有图片只加载一次。

  • 其他类(如 FighterBullet)不再自己加载图片,直接使用 ImageManager.xxxImage

但路径仍然是绝对路径,后续将优化改为相对路径或类路径资源。

3.滚动背景:双图无缝滚动

问题背景

想让游戏画面真实自然一些

解决方案

在 GameThread 中添加一个滚动偏移量 scrollY,每帧增加2,并绘制两张背景图(一张在上方,一张在下方)。背景图片从下往上循环滚动,模拟飞机向前飞行的视觉效果。

// GameThread.java 部分代码
private int scrollY = 0;

public void run() {
    while (true) {
        Thread.sleep(30);
        scrollY += 2;
        if (scrollY >= Data.UIHEIGHT) {
            scrollY -= Data.UIHEIGHT;
        }

        // 绘制第一张背景(上方)
        bufferGraphics.drawImage(ImageManager.backgroundImage, 0, scrollY - Data.UIHEIGHT, null);
        // 绘制第二张背景(下方)
        bufferGraphics.drawImage(ImageManager.backgroundImage, 0, scrollY, null);
        // ... 绘制游戏元素
    }
}

解析

  • 当 scrollY 超过窗口高度时,减去 UIHEIGHT 实现循环。

  • 两张图交替位置,保证任何时候都能覆盖整个窗口,形成连续滚动。

4. 碰撞检测(核心玩法)

V2实现了两类碰撞检测:

  • 敌方子弹与玩家飞机的碰撞

  • 我方子弹与敌方飞机的碰撞


            //从后向前遍历,避免删除元素时索引混乱
            for (int i = enBullets.size() - 1; i >= 0; i--) {
                enBullets.get(i).draw(bufferGraphics);
                enBullets.get(i).move();
                //判断子弹是否超出边界
                if (enBullets.get(i).getBlood() == 0) {
                    enBullets.remove(i);
                } else {
                    //遍历所有我方战机的位置,判断是否发生碰撞
                    if (enBullets.get(i).getX() >= myFighter.getX() && enBullets.get(i).getX() <= myFighter.getX() + myFighter.getWidth() &&
                            enBullets.get(i).getY() >= myFighter.getY() && enBullets.get(i).getY() <= myFighter.getY() + myFighter.getHeight()){
                        myFighter.setBlood(myFighter.getBlood() - 1);
                        enBullets.get(i).setBlood(0);
                    }
                }
            }

            //用fori循环
            for (int i = myBullets.size() - 1; i >= 0; i--) {
                myBullets.get(i).draw(bufferGraphics);
                myBullets.get(i).move();
                //判断子弹是否超出边界
                if (myBullets.get(i).getBlood() == 0) {
                    myBullets.remove(i);
                    continue;
                }
                //遍历所有敌方战机的位置,判断是否发生碰撞
                for (int j = fighters.size() - 1; j >= 0; j--) {
                    //坐标判断
                    if (myBullets.get(i).getX() >= fighters.get(j).getX() && myBullets.get(i).getX() <= fighters.get(j).getX() + fighters.get(j).getWidth() &&
                            myBullets.get(i).getY() >= fighters.get(j).getY() && myBullets.get(i).getY() <= fighters.get(j).getY() + fighters.get(j).getHeight()){
                        fighters.get(j).setBlood(0);
                        myBullets.get(i).setBlood(0);
                        score += 10;
                        break;
                    }
                }
            }

解析

  • 采用简单的矩形(AABB)碰撞检测,比较子弹左上角坐标是否落在玩家矩形内。

  • 一旦命中,玩家血量减1,子弹血量置0(子弹消失),积分加10。

5.生命值系统与可视化血条

玩家生命值
在 Fighter 构造方法中,玩家血量为100,敌方为10。被击中时,玩家血量减少1(

血条绘制
在 GameThread 中绘制血条背景和当前血量对应的绿色条。

// 绘制血条背景(黑色)
bufferGraphics.setColor(Color.BLACK);
bufferGraphics.fillRect(25, 55, 200, 20);
// 绘制血量(绿色)
bufferGraphics.setColor(Color.GREEN);
bufferGraphics.fillRect(26, 56, myFighter.getBlood() * 2 - 2, 18);

解析

  • 血条宽度200,对应最大血量100,因此每个血量单位占2像素。

  • 计算时减2是为了边框留白,视觉效果更好。

生命值文本

// 绘制血条背景(黑色)
bufferGraphics.setColor(Color.BLACK);
bufferGraphics.fillRect(25, 55, 200, 20);
// 绘制血量(绿色)
bufferGraphics.setColor(Color.GREEN);
bufferGraphics.fillRect(26, 56, myFighter.getBlood() * 2 - 2, 18);

6. 积分系统

积分累计
定义变量 int score = 0,每击中一架敌机增加10分。

score += 10;

积分显示

积分直接以文字形式写在窗口右上角

bufferGraphics.setColor(Color.RED);
bufferGraphics.drawString("积分:" + score, 370, 55);

7. 游戏难度:敌机随机走位 + 自动射击

随机水平移动
在 AutoShootThread 中,每0.9秒遍历所有敌方,并随机改变其水平速度。

Fighter fighter = fighters.get(i);
                if (count % 2 == 0) {
                    int speedx = random.nextInt(7) - 3;
                    fighter.setSpeedx(speedx);
                }

解析

  • 每两次循环改变一次敌方水平速度,模拟敌方左右飘移。

  • 速度值在 -3 到 3 之间,使移动平滑且不至于太快飞出屏幕。

自动射击
在同一个循环中,为每个敌方创建子弹并添加到 enBullets

Bullet bullet = new Bullet(true, fighter.getX() + fighter.getWidth() / 2 - 15,
                            fighter.getY() + fighter.getHeight() / 2 + 12, 0, 5);
enBullets.add(bullet);

解析

  • 子弹从飞机中心偏下位置发射,向下速度5。

8. 玩家射击

触发方式
在 GameListener 中增加对空格键的处理:

case KeyEvent.VK_SPACE:
    myFighter.shoot(myBullets);
    break;

Fighter.shoot() 方法

public void shoot(CopyOnWriteArrayList<Bullet> myBullets) {
    Bullet bullet = new Bullet(isEnemy, x + width/2 - 12, y + height/2 - 38, 0, -10);
    myBullets.add(bullet);
}

解析

  • 子弹从玩家飞机中心偏上位置发出,向上速度10。

  • 注意 isEnemy 此时为 false,所以子弹图片会是玩家子弹。

  • 该方法被调用时,将子弹加入传入的 myBullets 列表。


项目展示

GameV2


项目优化方向

增加功能:

1.增加爆炸特效、击中特效等

2.增加游戏背景音效、打击音效等

3.增加飞机飞行特效姿势等

4.增加boss战斗,设置关卡难度

5.支持更多敌人类型,引入不同血量、速度、射击方式的敌机

6.增加游戏状态管理,设置开始界面、暂停功能、游戏结束检测

7.开设道具系统,比如护盾、双倍子弹等

优化功能:

1.将所有图片打包放入资源根目录里

2.使用更精确的碰撞检测,避免矩形检测的误差

3.线程管理方面的优化


项目总结

V2版本在V1的基础上实现了质的飞跃,从一个“飞机移动演示”变成了一个具有基本玩法的可玩游戏。主要成果包括:

  • ✅ 完整的游戏循环(双缓冲 + 滚动背景)

  • ✅ 碰撞检测(子弹命中、敌机撞玩家)

  • ✅ 生命值与积分系统(血条可视化)

  • ✅ 敌方AI(自动射击 + 随机走位)

  • ✅ 玩家交互(WASD移动 + 空格射击)

  • ✅ 线程安全(使用 CopyOnWriteArrayList 解决并发问题)

  • ✅ 资源管理ImageManager 统一管理图片)

为后续版本(V3)增加更多特效、道具、难度关卡等扩展打下了坚实基础。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐