老旧未完善打飞机
下面这篇博客将严格以你最开始发来的未经修改的 Java 代码为基础不引入后续优化版本中的敌机、碰撞检测、Swing Timer 等功能。文章会按照初学者容易理解的方式介绍原始代码的设计思路、各个类的作用、游戏运行流程以及其中涉及的 Java 基础知识。对于原代码中存在的问题也会在最后单独分析。Java Swing 入门实战从零实现一个简单的飞机射击游戏Java · Swing · 面向对象编程 · 多线程 · 键盘事件监听一、项目简介在学习 Java 面向对象编程的过程中我们经常会接触到类Class、对象Object、继承Inheritance、接口Interface和集合Collection等基础知识。但对于初学者来说仅仅通过简单的代码示例学习这些知识可能很难理解它们在实际项目中的作用。本文通过一个简单的 Java Swing 飞机射击游戏将这些基础知识结合起来实现一个能够控制飞机移动和发射子弹的小游戏。本项目采用 Java 原生的 Swing 和 AWT 库不需要安装额外的游戏开发框架。游戏基本功能游戏功能展示Game Window · 800 × 800PlayerBullets ↑示意图原始版本使用黑色矩形表示玩家飞机红色圆形表示子弹。游戏操作SPACE创建玩家飞机并启动绘制线程。A向左移动。D向右移动。J发射一颗子弹。Q启动自动生成子弹的线程。需要注意原始版本尚未实现敌机、碰撞检测、得分和游戏结束等功能因此它更接近一个飞机射击游戏的基础框架。二、开发环境与技术本项目使用以下 Java 技术技术作用Java编写游戏逻辑Swing创建游戏窗口和面板AWT Graphics绘制飞机和子弹KeyListener监听键盘输入ArrayList保存子弹对象Thread实现游戏绘制和定时任务Runnable定义自动生成子弹的任务通过这些技术可以实现一个具有基本交互能力的桌面小游戏。三、项目整体结构原始代码主要由以下几个类组成GameUI │ └── 创建 JFrame 和 JPanel │ └── GameListener │ ├── 监听键盘输入 │ ├── 创建 MPlane │ ├── 创建 Bullet │ ├── 管理 ArrayListBullet │ ├── 启动 GameThread │ └── 启动 TimeThread GameThread extends Thread │ ├── 绘制玩家飞机 └── 绘制所有子弹 TimeThread implements Runnable │ └── 每隔 200ms 创建一颗子弹 MPlane ├── 保存飞机位置和速度 ├── drawPlane() └── move() Bullet ├── 保存子弹位置和速度 └── drawBullet()这种结构将窗口创建、键盘监听、游戏对象和线程任务分别放在不同的类中体现了面向对象编程中按职责划分类的基本思想。四、创建游戏窗口GameUI 类首先需要创建一个窗口来承载游戏画面。原始代码Javapublic class GameUI { public void initUI() { JFrame jf new JFrame(游戏界面); jf.setSize(800, 800); jf.setLocationRelativeTo(null); jf.setDefaultCloseOperation(3); JPanel gamePanel new JPanel(); gamePanel.setBackground(Color.WHITE); jf.add(gamePanel, BorderLayout.CENTER); jf.setVisible(true); Graphics g gamePanel.getGraphics(); GameListener listener new GameListener(g); gamePanel.addKeyListener(listener); gamePanel.requestFocus(); } public static void main(String[] args) { GameUI ui new GameUI(); ui.initUI(); } }1. JFrame创建窗口JavaJFrame jf new JFrame(游戏界面);JFrame是 Swing 提供的顶层窗口组件。通过它可以创建一个独立的桌面窗口并设置窗口标题、大小和关闭行为。Javajf.setSize(800, 800);表示窗口宽度和高度均为 800 像素。Javajf.setLocationRelativeTo(null);表示将窗口显示在屏幕中央。2. JPanel创建游戏区域JavaJPanel gamePanel new JPanel(); gamePanel.setBackground(Color.WHITE); jf.add(gamePanel, BorderLayout.CENTER);JPanel是一个轻量级容器可以用来承载其他组件也可以作为自定义绘图区域。本项目将游戏面板添加到窗口中央并设置白色背景。3. 获取 Graphics 对象JavaGraphics g gamePanel.getGraphics();Graphics是 Java AWT 提供的绘图工具可以绘制矩形、圆形、文字等图形。例如Javag.setColor(Color.BLACK); g.fillRect(100, 100, 50, 50);这段代码会在指定位置绘制一个黑色矩形。在本项目中Graphics对象被传递给游戏监听器和绘制线程用于绘制飞机和子弹。不过这种直接获取Graphics并长期保存使用的方式存在绘制稳定性问题后文会进行分析。五、使用 KeyListener 监听键盘事件游戏需要通过键盘控制玩家飞机因此使用 Java 的KeyListener接口。Javaclass GameListener implements KeyListener {KeyListener是一个接口定义了三个需要实现的方法Javapublic void keyPressed(KeyEvent e); public void keyReleased(KeyEvent e); public void keyTyped(KeyEvent e);它们分别用于处理按键按下、按键释放和按键输入事件。1. 获取按键编码Javapublic void keyPressed(KeyEvent e) { int key e.getKeyCode(); System.out.println(key key); switch (key) { // ... } }getKeyCode()可以获取当前按键对应的键值。例如JavaKeyEvent.VK_A KeyEvent.VK_D KeyEvent.VK_J KeyEvent.VK_Q KeyEvent.VK_SPACE这些常量分别代表 A、D、J、Q 和空格键。2. 实现飞机左右移动当按下 A 键Javacase KeyEvent.VK_A: mp.speedX -5; break;当按下 D 键Javacase KeyEvent.VK_D: mp.speedX 5; break;这里并没有直接修改飞机的 X 坐标而是修改了飞机的水平速度。真正的移动操作由MPlane类中的move()方法完成。这种方式将键盘输入与飞机移动逻辑分离使代码更加容易理解。3. 松开按键后停止移动Javapublic void keyReleased(KeyEvent e) { int key e.getKeyCode(); switch (key) { case KeyEvent.VK_A: mp.speedX 0; break; case KeyEvent.VK_D: mp.speedX 0; break; } }当玩家松开 A 或 D 键时将飞机的水平速度设为 0。这样就实现了按住移动、松开停止的基本控制方式。六、设计玩家飞机MPlane 类MPlane类用于表示玩家控制的飞机。Javaclass MPlane { public int x, y, size; public int speedX, speedY; public MPlane(int x, int y) { this.x x; this.y y; size 100; } }1. 使用构造方法初始化对象构造方法Javapublic MPlane(int x, int y)在创建飞机对象时执行。例如Javamp new MPlane(350, 600);表示创建一架初始坐标为(350, 600)的飞机。其中x飞机的水平位置。y飞机的垂直位置。size飞机的尺寸。speedX水平移动速度。speedY垂直移动速度。2. 绘制飞机Javapublic void drawPlane(Graphics g) { g.setColor(Color.WHITE); g.fillRect(x, y, size, size); move(); g.setColor(Color.BLACK); g.fillRect(x, y, size, size); }这里通过两次绘制实现简单的移动效果。第一次使用白色矩形覆盖飞机之前的位置第二次在新位置绘制黑色矩形。不过这种方式依赖背景颜色和绘制时机可能产生闪烁或残影。3. 实现飞机移动Javapublic void move() { x speedX; y speedY; }这是整个飞机移动逻辑的核心。例如假设飞机当前坐标为x 350 y 600按下 D 键后speedX 5执行一次move()x 355 y 600飞机就向右移动了 5 像素。七、设计子弹Bullet 类子弹使用独立的Bullet类表示。Javaclass Bullet { public int x, y, speedX, speedY, size; public Bullet(int x, int y) { this.x x; this.y y; speedY -8; size 8; } }1. 子弹的初始属性创建子弹时需要传入初始坐标。JavaBullet bullet new Bullet(mp.x, mp.y);子弹的初始位置与玩家飞机当前位置一致。同时设置JavaspeedY -8;表示子弹每次更新时向上移动 8 像素。在 Java 图形坐标系中屏幕左上角为原点X 轴向右增加。Y 轴向下增加。因此Y 轴速度为负数代表向上移动。2. 绘制子弹Javapublic void drawBullet(Graphics g) { g.setColor(Color.white); g.fillOval(x, y, size, size); y speedY; g.setColor(Color.red); g.fillOval(x, y, size, size); }fillOval()用于绘制填充椭圆。由于宽度和高度相同因此绘制出来的是圆形。这里先绘制白色圆形再更新 Y 坐标最后绘制红色圆形。白色圆形用于覆盖子弹之前的位置红色圆形表示更新后的子弹位置。八、使用 ArrayList 管理子弹对象游戏中可能同时存在多颗子弹因此需要使用集合保存它们。在GameListener中定义Javapublic ArrayListBullet bullets new ArrayList();ArrayList是 Java 集合框架中的动态数组。与普通数组相比它可以根据需要动态增加元素。1. 创建子弹按下 J 键时Javacase KeyEvent.VK_J: Bullet bullet new Bullet(mp.x, mp.y); bullets.add(bullet); break;执行过程如下根据飞机当前位置创建一个Bullet对象。使用add()将对象加入集合。后续绘制线程可以通过集合访问这颗子弹。2. 遍历所有子弹在GameThread中Javafor (int i 0; i bullets.size(); i) { Bullet bullet bullets.get(i); bullet.drawBullet(g); }通过循环遍历集合中的所有子弹并依次调用它们的绘制方法。这里也体现了面向对象编程的一个特点每颗子弹都拥有自己的位置和速度可以独立更新。九、使用 Thread 实现游戏绘制循环原始代码使用GameThread类实现持续绘制。Javaclass GameThread extends Thread {通过继承Thread类可以创建一个独立执行的线程。1. 构造方法Javapublic GameThread( Graphics g, MPlane mp, ArrayListBullet bullets ) { this.g g; this.mp mp; this.bullets bullets; }这里将Graphics、玩家飞机和子弹集合传递给线程。需要注意bullets并不是复制了一份新的集合而是保存了传入集合的引用。因此GameListener添加子弹后GameThread也能够访问到新添加的子弹。2. 重写 run() 方法Javapublic void run() { System.out.println( Thread.currentThread().getName() 线程启动... ); while (true) { try { Thread.sleep(50); } catch (InterruptedException ex) { throw new RuntimeException(ex); } mp.drawPlane(g); System.out.println(size bullets.size()); for (int i 0; i bullets.size(); i) { Bullet bullet bullets.get(i); bullet.drawBullet(g); } } }run()是线程启动后执行的方法。其中JavaThread.sleep(50);表示让当前线程暂停约 50 毫秒。随后程序绘制飞机和所有子弹。理论上每秒循环约 20 次10005020\frac{1000}{50}2050100020这就形成了一个简单的游戏循环。3. 启动线程在玩家按下空格键时JavaGameThread gt new GameThread(g, mp, bullets); gt.start();需要注意start()和直接调用run()不一样。start()启动一个新线程由 JVM 调度执行run()。run()普通方法调用不会自动创建新线程。这是 Java 多线程编程中的一个重要知识点。十、使用 Runnable 实现自动发射子弹除了手动按 J 发射子弹原始代码还通过TimeThread实现自动生成子弹。Javaclass TimeThread implements Runnable {与继承Thread不同这里使用implements Runnable实现任务接口。1. 设置时间间隔Javapublic long time 200;表示每隔约 200 毫秒执行一次子弹生成操作。理论上每秒生成10002005\frac{1000}{200}520010005颗子弹。2. 实现 run() 方法Javapublic void run() { while (true) { try { Thread.sleep(time); } catch (InterruptedException e) { throw new RuntimeException(e); } Bullet bullet new Bullet(mp.x, mp.y); bullets.add(bullet); } }线程启动后会不断执行以下操作暂停 200 毫秒。获取玩家飞机当前坐标。创建新的子弹对象。将子弹添加到集合。重复执行。3. 启动自动发射线程在GameListener中Javacase KeyEvent.VK_Q: TimeThread timer new TimeThread(bullets, mp); new Thread(timer).start(); break;这里使用Javanew Thread(timer).start();将实现了Runnable的对象交给Thread执行。十一、完整游戏运行流程将以上代码组合起来游戏的运行流程如下游戏执行流程启动程序main() → initUI()创建游戏窗口JFrame JPanel注册键盘监听器GameListener按下 SPACE创建 MPlane 和 GameThread游戏循环运行移动飞机 → 绘制飞机 → 绘制子弹按下 J / Q手动或自动生成子弹子弹加入 ArrayListGameThread 遍历并绘制十二、原始代码存在的问题与改进方向虽然这份代码已经展示了 GUI、事件监听、集合和多线程的基本用法但它仍然是一个初级练习项目。以下问题值得在后续学习中逐步解决。1. Graphics 对象的使用方式不够规范原始代码通过JavaGraphics g gamePanel.getGraphics();获取绘图对象并在独立线程中持续使用。Swing 推荐通过重写paintComponent(Graphics g)进行自定义绘制并通过repaint()请求界面刷新。这样可以更好地处理窗口遮挡、重绘和绘图状态管理。2. JPanel 可能无法正确接收键盘事件原始代码使用JavagamePanel.requestFocus();但JPanel默认不一定能够获得焦点。此外调用焦点请求的时机也会影响键盘监听是否正常工作。可以考虑设置面板为可聚焦并在窗口显示后请求焦点或者使用 Swing 的键盘绑定机制。3. 多线程共享 ArrayList 存在风险GameListener、GameThread和TimeThread同时访问同一个ArrayList。例如一个线程正在遍历集合另一个线程可能正在添加子弹。普通ArrayList并不是线程安全的因此这种并发访问可能造成数据一致性问题或运行异常。4. 游戏线程没有停止机制GameThread和TimeThread都使用Javawhile (true)因此线程会一直运行。即使玩家关闭游戏窗口也没有通过代码主动停止这些循环。5. 缺少子弹生命周期管理目前子弹只会向上移动不会在离开窗口后从集合中删除。因此长时间运行时子弹集合会不断增大。6. 游戏功能仍然比较基础当前项目没有实现敌机、碰撞检测、分数系统和游戏结束机制。这些都可以作为后续扩展方向。十三、总结这个项目虽然简单但完整展示了一个基础 Java GUI 程序从创建窗口、接收用户输入到更新对象状态和绘制图形的基本过程。知识点实际应用Class Object创建飞机和子弹对象Constructor初始化飞机与子弹属性Interface实现 KeyListener 和 RunnableInheritanceGameThread 继承 ThreadArrayList管理多个子弹对象Event Listener处理键盘输入Multithreading实现持续绘制和自动发射Graphics绘制游戏对象Reference Passing多个对象共享子弹集合对于 Java 初学者来说理解这些模块之间的关系比单纯记忆 API 的用法更加重要。从一个能够移动和发射子弹的简单窗口开始我们就已经迈出了将 Java 基础知识应用于实际项目的第一步。