上期回顾:
我们完成了窗体设计、鼠标监测、按钮监测以及点击事件的刷新功能。
本期目标:在上一代基础上进行更新,进一步完善程序功能;新增计时器与积分记录系统。具体包括:
- 计时器功能:实现游戏倒计时机制,设置初始时间(如60秒),时间归零时游戏结束。计时器需实时显示在游戏界面,支持暂停、重置功能,并能在游戏难度变化时动态调整时间限制。
- 积分记录系统 :设计一套完整的积分规则,包括:
- 基础得分:每次成功点击目标获得固定分数
- 连击奖励:连续点击成功可获得额外分数加成
- 时间奖励:剩余时间可按比例转换为额外积分
- 难度系数:不同难度级别设置不同的分数倍率
- 界面展示:在游戏主界面醒目位置显示当前分数、时间等信息,通过视觉反馈(如分数跳动、时间警告变色)增强游戏体验。
(一)需求分析与整体设计
- 游戏功能更新:在原有功能基础上,新增积分系统与计时器功能,并实现实时更新显示。
- 游戏代码完善:使用JPanel替代JFrame进行界面重构,提升代码的可维护性和扩展性。
本次功能实现相对简单,下面我们开始具体的编码工作。
(二)代码实现
(一). 计时器功能实现
在添加计时器之前,我们先来了解一下 Java 中常见的四种计时器方案。
1.1 javax.swing.Timer(本次飞机游戏正在使用,Swing GUI 首选)
- 所属包 :
javax.swing.Timer - 底层机制:内部封装了后台线程,回调函数会自动运行在 Swing 事件调度线程(EDT)中。
- 特点 :UI 安全,直接修改
JLabel、调用repaint()不会出现闪烁或偶现的 bug;启动和停止简单(start()/stop())。 - 使用场景:Swing 图形界面小游戏、界面定时刷新。
java
Timer timer = new Timer(1000, e -> {
// 每秒执行这里的代码,可以直接操作界面组件
});
timer.start();
1.2 手动 new Thread() + sleep()
- 底层机制 :基于 JDK 基础线程,自己编写循环并使用
Thread.sleep()。 - 特点:自由度很高;但禁止直接修改 Swing 界面,否则容易导致闪烁、数据错乱;停止控制较为麻烦,容易引发多线程并发问题。
- 使用场景:后台计算任务,不涉及 GUI 界面操作。
java
new Thread(() -> {
while (true) {
Thread.sleep(1000);
}
}).start();
1.3 java.util.Timer + TimerTask(旧版工具,不推荐用于 Swing)
- 所属包 :
java.util.Timer - 底层机制:使用单独的后台线程执行任务。
- 缺点:任务抛出异常会导致整个计时器直接崩溃;运行在后台线程,不能直接更新 Swing 界面;API 较为老旧。
- 使用场景:简单的后台定时任务,无图形界面的项目。
1.4 ScheduledExecutorService(线程池定时,企业级项目)
- 所属包 :
java.util.concurrent - 底层机制:基于线程池实现定时调度,工业级设计,稳定性高;支持延迟执行和周期执行。
- 同样运行在后台线程,不能直接操作 Swing 组件。适用于大型后端项目,对于小游戏来说略显重量级。
2. 计时器对比表格
| 计时器 | 所在包 | 执行线程 | 适合 Swing 界面? | 特点 |
|---|---|---|---|---|
| Swing Timer | javax.swing |
Swing‑EDT 界面线程 | ✅ 推荐 | 专为 GUI 设计,启停简单 |
| Thread+sleep | java.lang |
自定义后台子线程 | ❌ 不建议 | 灵活,但 UI 会出现诡异 bug |
| util.Timer | java.util |
独立后台线程 | ❌ 不建议 | 老旧,异常直接挂掉计时器 |
| ScheduledExecutorService | java.util.concurrent |
线程池后台 | ❌ 不建议 | 后端大型项目,功能强大 |
相信大家也注意到了表格中加粗的"诡异 bug"。这里我结合自己开发飞机游戏时的实际经历,跟大家分享一下这类问题的典型表现和原因。
现象:问题出现时好时坏,没有固定的复现规律。偶尔会出现标签文字不更新、图片残影、界面花屏等情况,而且控制台没有任何报错,调试起来非常困难。
错误示范(反面案例,切勿在游戏中使用) :
以下代码展示了使用普通 Thread + sleep() 直接修改 JLabel 文字的典型错误做法:
java
// 错误!子线程直接修改 Swing 组件
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
// 后台子线程,直接操作界面 JLabel
drawPanel.time++;
drawPanel.updateTime();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
会出现哪些诡异现象(没有 Exception 崩溃,但是界面异常) :
(1)标签文字偶尔不刷新 :控制台打印的 time 数字一直在增加,但界面上的时间或积分却纹丝不动,程序不报任何错误。
(2)界面花屏、残影 :飞机图片有时擦不掉,画面上会出现残留的碎片。
(3)偶发闪退 :运行几分钟后,可能随机抛出 ConcurrentModificationException、IllegalComponentStateException 等异常,且并非每次必现。
(4)间歇性失效:程序刚启动时正常,但鼠标快速拖动一会儿后,UI 直接停止更新。
原因:Swing 的所有组件都只允许在 EDT(事件调度线程,即 UI 线程)中访问。普通后台线程与 UI 线程并发抢夺组件,导致线程竞争和数据不同步。这类 bug 随机出现,并非 100% 复现,排查起来极其折磨人,因此被称为"诡异 bug"。
如果非要在后台线程修改 Swing(了解即可) :
必须使用 SwingUtilities.invokeLater() 将更新 UI 的代码投递回 UI 线程执行。
java
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
// 业务数据可以在子线程中修改
drawPanel.time++;
// UI 操作,切回 EDT 界面线程
SwingUtilities.invokeLater(() -> {
drawPanel.updateTime();
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
使用 Thread + sleep() 虽然可以实现定时逻辑,但它运行在自定义的后台子线程中。Swing 框架规定所有界面组件只能由 EDT(事件调度线程,即 UI 线程)操作。
如果直接在子线程中修改标签、画布等界面元素,会产生偶现的"诡异 bug":控制台数据正常,但界面文字偶尔停止刷新、画面出现残影、程序随机闪退。这类 bug 复现没有固定规律,调试难度很高。
如果必须使用后台线程更新界面,必须借助 SwingUtilities.invokeLater() 将 UI 操作交给界面线程执行。
因此,开发 Swing 游戏时,直接选用 javax.swing.Timer 是最佳选择------它的回调函数天然运行在 EDT 线程中,可以完全避开这类并发问题。
3. 小结
- 开发 Swing 桌面小游戏,优先使用
javax.swing.Timer。 - 其他计时器全部运行在后台线程,如果需要刷新界面,必须借助
SwingUtilities.invokeLater()将代码切回 UI 线程,增加了复杂度。 - 后台服务程序,优先使用
ScheduledExecutorService。
程序搭建
在本次程序更新中,我们引入了 JPanel 来重构界面。为了更好地理解后续的计时器与积分显示功能的实现,我们先来梳理一下 JPanel 的基本逻辑。
本次更新涉及三个核心类:
- GameListener(监听器):处理用户交互事件
- Main(基础设计与程序入口):程序主类,负责界面初始化
- DrawPanel(JPanel 绘图类):继承 JPanel,负责游戏画面的绘制
Main 类的实现:
Main 类是程序的入口点,负责初始化游戏窗口和核心组件。本次更新中,我们最大的变化是引入了 JPanel 来替代直接在 JFrame 上绘制图形的做法。这种重构带来了几个显著优势:
- 职责分离:将游戏画面的绘制逻辑从主窗口类中剥离出来,使代码结构更清晰。
- 可维护性:DrawPanel 专注于绘图,Main 专注于窗口管理和组件布局,便于后续功能扩展。
- 性能优化:JPanel 提供了双缓冲机制,可以有效减少画面闪烁,提升游戏流畅度。
除了 JPanel 的引入,Main 类的其他部分与上一篇博文保持一致,保持了代码的延续性。下面我们详细分析 Main 类的各个部分:
java
import javax.swing.*;
import java.awt.*;
public class Main {
// 程序入口方法
static void main() {
Main main = new Main();
main.showUI();
}
public void showUI() {
// 1. 基本窗口设置
JFrame jf = new JFrame("飞行器游戏");
jf.setSize(600, 800); // 设置窗口大小
jf.setResizable(false); // 禁止调整窗口大小,保持游戏画面稳定
jf.setLocationRelativeTo(null); // 窗口居中显示
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // 点击关闭按钮时退出程序
jf.setLayout(null); // 使用绝对布局,便于精确控制组件位置
// 2. 创建绘图面板 - 本次更新的核心
DrawPanel drawPanel = new DrawPanel();
drawPanel.setBounds(0, 0, 600, 800); // 设置面板位置和大小,覆盖整个窗口
jf.add(drawPanel); // 将面板添加到窗口中
// 3. 添加开始游戏按钮
JButton gameStart = new JButton("开始游戏");
gameStart.setBounds(230, 420, 140, 40); // 设置按钮位置和大小
jf.add(gameStart); // 将按钮添加到窗口
// 4. 设置事件监听器
GameListener gameListener = new GameListener(drawPanel);
gameStart.addActionListener(gameListener); // 为按钮添加点击事件监听
jf.addMouseMotionListener(gameListener); // 为窗口添加鼠标移动事件监听
// 5. 显示窗口
jf.setVisible(true);
}
}
代码结构解析:
- 窗口初始化:创建 JFrame 并设置基本属性,这是 Swing 应用程序的标准启动流程。
- JPanel 集成 :DrawPanel 继承自 JPanel,专门负责游戏画面的绘制。通过
setBounds()方法将其定位到窗口的 (0,0) 位置,覆盖整个窗口区域。 - 交互组件:游戏开始按钮是用户与游戏交互的主要入口,后续我们会在这里添加计时器和积分显示功能。
- 事件监听:GameListener 类集中处理所有用户交互事件,包括按钮点击和鼠标移动,这种设计符合 MVC(模型-视图-控制器)模式的思想。
- 显示逻辑 :最后调用
setVisible(true)显示窗口,完成界面初始化。
为什么使用 JPanel?
在之前的版本中,我们直接在 JFrame 上绘制图形,这种方式虽然简单,但随着功能增加会变得难以维护。JPanel 提供了以下优势:
- 专门的绘制区域 :JPanel 有自己的
paintComponent()方法,可以专注于图形绘制。 - 自动重绘 :调用
repaint()时,只有面板区域会被重绘,效率更高。 - 组件容器:可以方便地添加其他 Swing 组件(如标签、按钮等)到面板上。
- 布局灵活:可以设置不同的布局管理器,或者使用绝对布局进行精确控制。
在接下来的章节中,我们将详细介绍 DrawPanel 的实现,以及如何在这个基础上添加计时器和积分显示功能。
DrawPanel的实现:
DrawPanel 类继承自 JPanel,是游戏画面的核心绘制组件。它主要负责加载游戏图片资源,并根据游戏状态动态绘制不同的界面内容。在实现过程中,我们运用了面向对象封装的思想,将图片资源和游戏状态封装在类内部。
java
import javax.swing.*;
import java.awt.*;
public class DrawPanel extends JPanel {
// 飞机坐标
public int planeX, planeY;
// 游戏状态:0-游戏开始前,1-游戏中
public int gameState = 0;
// 图片资源
private final Image bgImage; // 游戏开始后的背景
private final Image planeImage; // 飞机图片
private final Image themeImage; // 游戏开始前的背景
// 构造函数:初始化图片资源
public DrawPanel() {
bgImage = new ImageIcon("游戏开始后的背景").getImage();
planeImage = new ImageIcon("飞机图片").getImage();
themeImage = new ImageIcon("游戏开始前的背景").getImage();
this.setLayout(null); // 使用绝对布局
}
// 重写 paint 方法,实现自定义绘制
@Override
public void paint(Graphics g) {
super.paintComponent(g); // 调用父类方法清空画布
// 根据游戏状态绘制不同的背景
if (bgImage != null) {
g.drawImage(bgImage, 0, 0, 600, 800, null);
}
// 游戏进行中:绘制飞机
if (gameState == 1 && planeImage != null) {
// 飞机绘制在 (planeX-15, planeY-15) 位置,大小为 30x30
g.drawImage(planeImage, planeX - 15, planeY - 15, 30, 30, null);
}
// 游戏开始前:绘制主题背景
else if (gameState == 0) {
g.drawImage(themeImage, 0, 0, 600, 800, null);
}
}
}
实现要点解析:
-
封装设计:
- 将图片资源(
bgImage、planeImage、themeImage)封装为私有成员变量,通过构造函数统一初始化。 - 游戏状态(
gameState)和飞机坐标(planeX、planeY)作为公开变量,便于外部监听器控制。
- 将图片资源(
-
绘制逻辑:
- 重写
paint()方法,根据gameState的值决定绘制内容。 - 游戏开始前(
gameState == 0):显示主题背景,营造游戏氛围。 - 游戏进行中(
gameState == 1):显示游戏背景和飞机,飞机位置由planeX和planeY动态控制。
- 重写
-
布局管理:
- 使用
setLayout(null)采用绝对布局,便于精确控制后续添加的 UI 组件位置。 - 图片绘制使用
drawImage()方法,指定坐标和尺寸,确保画面适配窗口大小。
- 使用
这种设计将绘制逻辑集中在一个类中,提高了代码的可维护性,也为后续添加计时器、积分显示等 UI 元素奠定了基础。
GameListener的运行
GameListener 类是实现游戏交互逻辑的核心组件,它同时实现了 MouseMotionListener 和 ActionListener 两个接口,负责处理用户的鼠标移动和按钮点击事件。通过监听器模式,我们将用户交互逻辑与界面绘制逻辑分离,使代码结构更加清晰。
java
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
public class GameListener implements MouseMotionListener, ActionListener {
// 关联的绘图面板
private DrawPanel drawPanel;
/**
* 构造函数:初始化监听器并关联绘图面板
* @param drawPanel 游戏绘图面板实例
*/
public GameListener(DrawPanel drawPanel) {
this.drawPanel = drawPanel;
// 计时器初始化将在后续章节中实现
this.timer = null;
}
/**
* 处理按钮点击事件
* 当用户点击"开始游戏"按钮时,切换游戏状态并隐藏按钮
*/
@Override
public void actionPerformed(ActionEvent e) {
// 获取触发事件的按钮
JButton btn = (JButton) e.getSource();
String actionCommand = btn.getActionCommand();
// 判断是否为"开始游戏"按钮
if ("开始游戏".equals(actionCommand)) {
// 切换游戏状态:0->1(从开始前切换到游戏中)
drawPanel.gameState = 1;
// 隐藏开始按钮,避免重复点击
btn.setVisible(false);
// 这里可以添加游戏开始的音效或动画效果
// 后续将在这里启动计时器
}
}
/**
* 处理鼠标拖动事件(当前未使用,保留接口)
*/
@Override
public void mouseDragged(MouseEvent e) {
// 预留接口,可用于实现拖拽功能
// 例如:拖拽飞机、绘制轨迹等
}
/**
* 处理鼠标移动事件
* 实时更新飞机位置,并在游戏进行中触发画面重绘
*/
@Override
public void mouseMoved(MouseEvent e) {
// 获取鼠标当前位置
int mouseX = e.getX();
int mouseY = e.getY();
// 更新飞机坐标(飞机中心点跟随鼠标)
drawPanel.planeX = mouseX;
drawPanel.planeY = mouseY;
// 仅在游戏进行中才触发重绘
if (drawPanel.gameState == 1) {
// 调用 repaint() 触发 DrawPanel 的 paint() 方法
drawPanel.repaint();
}
}
}
代码结构解析:
-
成员变量设计:
drawPanel:持有对绘图面板的引用,用于更新游戏状态和触发重绘。timer:预留的计时器字段,为后续实现倒计时功能做准备。
-
构造函数:
- 通过参数注入
DrawPanel实例,建立监听器与绘图面板的关联。 - 这种依赖注入的方式提高了代码的可测试性和模块化程度。
- 通过参数注入
-
按钮事件处理(actionPerformed):
- 事件源识别 :通过
e.getSource()获取触发事件的按钮对象。 - 状态切换 :将
drawPanel.gameState从 0(开始前)切换到 1(游戏中)。 - 界面反馈:隐藏开始按钮,防止用户重复点击,提供清晰的游戏状态反馈。
- 扩展点:注释标明了后续可以添加音效、动画和计时器启动的位置。
- 事件源识别 :通过
-
鼠标移动事件处理(mouseMoved):
- 坐标获取 :通过
e.getX()和e.getY()实时获取鼠标位置。 - 飞机跟随:将飞机坐标设置为鼠标位置,实现"飞机跟随鼠标"的效果。
- 条件重绘 :仅在游戏进行中(
gameState == 1)才调用repaint(),避免不必要的性能开销。
- 坐标获取 :通过
-
鼠标拖动事件(mouseDragged):
- 当前为空实现,为后续功能扩展预留了接口。
- 可用于实现拖拽交互、绘制轨迹等高级功能。
设计模式应用:
GameListener 采用了监听器模式,这是 Swing 事件处理的标准方式。这种设计有以下几个优点:
- 职责分离:将事件处理逻辑从主窗口和绘图面板中分离出来,符合单一职责原则。
- 松耦合:监听器只依赖 DrawPanel 的公共接口,不关心其内部实现细节。
- 可扩展性:可以轻松添加新的事件类型(如键盘事件、窗口事件)而不影响现有代码。
与 DrawPanel 的协作流程:
用户点击"开始游戏"按钮
↓
actionPerformed() 被调用
↓
drawPanel.gameState = 1(切换游戏状态)
↓
开始按钮隐藏
↓
用户移动鼠标
↓
mouseMoved() 被调用
↓
更新 drawPanel.planeX/planeY(飞机位置)
↓
drawPanel.repaint() 触发重绘
↓
DrawPanel.paint() 根据新位置重新绘制飞机
ok啊,我们的程序更新已经完成了,接下来我们开始添加计时器与积分(easy~)
计时器与积分的添加
现在,我们的程序基础框架已经搭建完成,接下来开始为游戏添加计时器和积分显示功能。
1. 声明积分与时间变量
首先,在 DrawPanel 类中声明积分(score)和时间(time)变量,并准备好对应的标签组件:
java
public int score = 0; // 积分变量
public int time = 0; // 时间变量(单位:秒)
private JLabel scoreLabel; // 积分显示标签
private JLabel timeLabel; // 时间显示标签
2. 初始化积分与时间标签
在 DrawPanel 的构造函数中,初始化积分和时间标签,并设置它们的样式和位置:
java
// 初始化积分标签
scoreLabel = new JLabel("积分:0");
scoreLabel.setFont(new Font("黑体", Font.BOLD, 20));
scoreLabel.setBounds(20, 50, 150, 30); // 设置位置和大小
this.add(scoreLabel); // 添加到面板
scoreLabel.setVisible(true); // 设置为可见
// 初始化时间标签
timeLabel = new JLabel("时间:0");
timeLabel.setFont(new Font("黑体", Font.BOLD, 20));
timeLabel.setForeground(Color.white); // 设置文字颜色为白色
timeLabel.setBounds(420, 50, 150, 30); // 设置位置和大小
this.add(timeLabel); // 添加到面板
timeLabel.setVisible(true); // 设置为可见
3. 添加更新方法
接下来,在 DrawPanel 类中添加更新时间和积分显示的方法:
java
public void updateTime() {
timeLabel.setText("时间:" + time);
}
public void updateScore() {
scoreLabel.setText("积分:" + score);
}
4. 界面渲染问题排查
在实际运行中,你可能会遇到一个常见问题:虽然添加了 JLabel 组件,但界面上却无法显示积分和时间信息。这通常是由于 Swing 的渲染机制导致的。
为了排查这个问题,我们可以添加调试语句来确认方法是否被正常调用:
java
public void updateTime() {
timeLabel.setText("时间:" + time);
System.out.println("updateTime 方法被调用,当前时间:" + time);
}
public void updateScore() {
scoreLabel.setText("积分:" + score);
System.out.println("updateScore 方法被调用,当前积分:" + score);
}
运行程序后,观察控制台输出:
- 如果控制台持续输出调试信息,说明方法被正常调用,但
JLabel的渲染可能存在问题 - 如果控制台没有输出,说明方法没有被调用,需要检查调用时机
5. 备用渲染方案
如果确认 JLabel 渲染存在问题,我们可以采用备用方案:直接在 DrawPanel 的 paint() 方法中使用 Graphics 绘制文字。这种方法虽然不如 JLabel 灵活,但能确保文字正常显示:
java
@Override
public void paint(Graphics g) {
super.paintComponent(g);
// ... 原有的绘制代码 ...
// 绘制时间和积分(备用方案)
g.setColor(Color.white);
String timeStr = timeLabel.getText();
g.drawString(timeStr, 20, 50); // 在坐标 (20, 50) 绘制时间
String scoreStr = scoreLabel.getText();
g.drawString(scoreStr, 420, 50); // 在坐标 (420, 50) 绘制积分
}
注意事项:
- 使用
JLabel是更标准的做法,因为它支持自动更新、样式设置和事件处理 - 直接使用
Graphics.drawString()绘制文字时,需要手动处理重绘和位置计算 - 建议先尝试
JLabel方案,如果遇到渲染问题再使用备用方案
通过以上步骤,我们就成功为游戏添加了计时器和积分显示功能。
6. 集成计时器与游戏逻辑
现在,我们需要将计时器功能集成到游戏监听器中,实现游戏开始时的计时和积分更新逻辑。
6.1 在 GameListener 中添加计时器
首先,在 GameListener 类中添加计时器成员变量,并在构造函数中初始化它:
java
private Timer timer;
public GameListener(DrawPanel dp) {
this.drawPanel = dp;
// 创建 Swing Timer,每秒触发一次
timer = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 每秒时间增加 1 秒
drawPanel.time++;
drawPanel.updateTime();
// 每秒积分增加 10 分(示例逻辑,可根据实际游戏规则调整)
drawPanel.score += 10;
drawPanel.updateScore();
}
});
}
代码说明:
- 使用
javax.swing.Timer,这是 Swing 应用中最适合的计时器方案 - 计时器间隔设置为 1000 毫秒(1 秒),每秒自动触发一次
- 在计时器的回调函数中,同时更新时间和积分,并调用相应的更新方法刷新界面显示
6.2 完善按钮事件处理
接下来,修改 actionPerformed 方法,在游戏开始时启动计时器并重置游戏状态:
java
@Override
public void actionPerformed(ActionEvent e) {
String actionCommand = e.getActionCommand();
JButton btn = (JButton) e.getSource();
if ("开始游戏".equals(actionCommand)) {
// 切换游戏状态:0->1(从开始前切换到游戏中)
drawPanel.gameState = 1;
// 重置时间和积分
drawPanel.time = 0;
drawPanel.updateTime();
drawPanel.score = 0;
drawPanel.updateScore();
// 启动计时器
timer.start();
// 隐藏开始按钮,避免重复点击
btn.setVisible(false);
}
}
功能说明:
- 游戏状态切换 :将游戏状态设置为进行中(
gameState = 1) - 数据重置:将时间和积分归零,确保每次游戏都从初始状态开始
- 计时器启动 :调用
timer.start()开始计时,计时器会每秒自动触发更新 - 界面反馈:隐藏开始按钮,防止用户在游戏进行中重复点击
6.3 完整的工作流程
整合后的游戏工作流程如下:
- 游戏初始化:程序启动,显示开始界面,等待用户点击"开始游戏"按钮
- 游戏开始 :用户点击按钮后:
- 游戏状态切换为进行中
- 时间和积分重置为 0
- 计时器开始运行
- 开始按钮隐藏
- 游戏进行中 :
- 计时器每秒触发一次,自动更新时间和积分
- 玩家通过鼠标控制飞机移动
- 界面实时显示当前时间和积分
- 游戏结束:(后续可扩展)当时间到达设定上限或满足其他结束条件时,停止计时器并显示游戏结果
通过以上实现,我们成功将计时器和积分系统集成到了游戏的核心逻辑中。计时器会在游戏开始时自动启动,每秒更新一次时间和积分,为游戏增加了时间限制和得分机制,使游戏体验更加完整。
在后续开发中,你可以根据需要扩展以下功能:
- 添加游戏结束条件(如时间耗尽、积分达到目标等)
- 实现暂停/继续功能
- 添加不同难度级别的时间限制
- 设计更丰富的积分规则(如击中目标得分、连击奖励等)
到这里,我们已经完成飞机躲避障碍物游戏的基础版本,实现了图片背景、鼠标控制飞机移动、计时计分系统等核心功能。
在本次开发过程中,我踩过不少Swing绘图的坑:比如后台线程造成的偶现诡异BUG、组件监听器绑定错误、paint与paintComponent的混淆,也深入理解了不同计时器之间的区别,体会到Swing单线程模型带来的限制。
GUI开发不能只顾修改数据,更要理清数据与视图分离的思想:监听器负责接收交互、修改游戏状态,绘图面板根据最新状态完成画面渲染。
目前项目还只是基础雏形,后续还可以继续拓展,增加障碍物生成、碰撞检测、游戏结束判定等功能。如果文章有错误,欢迎大家评论区指正,也希望这篇实战可以给正在学习Java Swing的同学带来一点参考,让我们共同进步!
程序运行结果:

