从零手写Java飞机躲障碍游戏|Swing绘图、鼠标跟随、计时器碰撞检测实战(二)

上期回顾:

我们完成了窗体设计、鼠标监测、按钮监测以及点击事件的刷新功能。

本期目标:在上一代基础上进行更新,进一步完善程序功能;新增计时器与积分记录系统。具体包括:

  1. 计时器功能:实现游戏倒计时机制,设置初始时间(如60秒),时间归零时游戏结束。计时器需实时显示在游戏界面,支持暂停、重置功能,并能在游戏难度变化时动态调整时间限制。
  2. 积分记录系统 :设计一套完整的积分规则,包括:
    • 基础得分:每次成功点击目标获得固定分数
    • 连击奖励:连续点击成功可获得额外分数加成
    • 时间奖励:剩余时间可按比例转换为额外积分
    • 难度系数:不同难度级别设置不同的分数倍率
  3. 界面展示:在游戏主界面醒目位置显示当前分数、时间等信息,通过视觉反馈(如分数跳动、时间警告变色)增强游戏体验。

(一)需求分析与整体设计

  1. 游戏功能更新:在原有功能基础上,新增积分系统与计时器功能,并实现实时更新显示。
  2. 游戏代码完善:使用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)偶发闪退 :运行几分钟后,可能随机抛出 ConcurrentModificationExceptionIllegalComponentStateException 等异常,且并非每次必现。

(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 上绘制图形的做法。这种重构带来了几个显著优势:

  1. 职责分离:将游戏画面的绘制逻辑从主窗口类中剥离出来,使代码结构更清晰。
  2. 可维护性:DrawPanel 专注于绘图,Main 专注于窗口管理和组件布局,便于后续功能扩展。
  3. 性能优化: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);
    }
}

代码结构解析:

  1. 窗口初始化:创建 JFrame 并设置基本属性,这是 Swing 应用程序的标准启动流程。
  2. JPanel 集成 :DrawPanel 继承自 JPanel,专门负责游戏画面的绘制。通过 setBounds() 方法将其定位到窗口的 (0,0) 位置,覆盖整个窗口区域。
  3. 交互组件:游戏开始按钮是用户与游戏交互的主要入口,后续我们会在这里添加计时器和积分显示功能。
  4. 事件监听:GameListener 类集中处理所有用户交互事件,包括按钮点击和鼠标移动,这种设计符合 MVC(模型-视图-控制器)模式的思想。
  5. 显示逻辑 :最后调用 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);
        }
    }
}

实现要点解析:

  1. 封装设计

    • 将图片资源(bgImageplaneImagethemeImage)封装为私有成员变量,通过构造函数统一初始化。
    • 游戏状态(gameState)和飞机坐标(planeXplaneY)作为公开变量,便于外部监听器控制。
  2. 绘制逻辑

    • 重写 paint() 方法,根据 gameState 的值决定绘制内容。
    • 游戏开始前(gameState == 0):显示主题背景,营造游戏氛围。
    • 游戏进行中(gameState == 1):显示游戏背景和飞机,飞机位置由 planeXplaneY 动态控制。
  3. 布局管理

    • 使用 setLayout(null) 采用绝对布局,便于精确控制后续添加的 UI 组件位置。
    • 图片绘制使用 drawImage() 方法,指定坐标和尺寸,确保画面适配窗口大小。

这种设计将绘制逻辑集中在一个类中,提高了代码的可维护性,也为后续添加计时器、积分显示等 UI 元素奠定了基础。

GameListener的运行

GameListener 类是实现游戏交互逻辑的核心组件,它同时实现了 MouseMotionListenerActionListener 两个接口,负责处理用户的鼠标移动和按钮点击事件。通过监听器模式,我们将用户交互逻辑与界面绘制逻辑分离,使代码结构更加清晰。

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();
        }
    }
}

代码结构解析:

  1. 成员变量设计

    • drawPanel:持有对绘图面板的引用,用于更新游戏状态和触发重绘。
    • timer:预留的计时器字段,为后续实现倒计时功能做准备。
  2. 构造函数

    • 通过参数注入 DrawPanel 实例,建立监听器与绘图面板的关联。
    • 这种依赖注入的方式提高了代码的可测试性和模块化程度。
  3. 按钮事件处理(actionPerformed)

    • 事件源识别 :通过 e.getSource() 获取触发事件的按钮对象。
    • 状态切换 :将 drawPanel.gameState 从 0(开始前)切换到 1(游戏中)。
    • 界面反馈:隐藏开始按钮,防止用户重复点击,提供清晰的游戏状态反馈。
    • 扩展点:注释标明了后续可以添加音效、动画和计时器启动的位置。
  4. 鼠标移动事件处理(mouseMoved)

    • 坐标获取 :通过 e.getX()e.getY() 实时获取鼠标位置。
    • 飞机跟随:将飞机坐标设置为鼠标位置,实现"飞机跟随鼠标"的效果。
    • 条件重绘 :仅在游戏进行中(gameState == 1)才调用 repaint(),避免不必要的性能开销。
  5. 鼠标拖动事件(mouseDragged)

    • 当前为空实现,为后续功能扩展预留了接口。
    • 可用于实现拖拽交互、绘制轨迹等高级功能。

设计模式应用:

GameListener 采用了监听器模式,这是 Swing 事件处理的标准方式。这种设计有以下几个优点:

  1. 职责分离:将事件处理逻辑从主窗口和绘图面板中分离出来,符合单一职责原则。
  2. 松耦合:监听器只依赖 DrawPanel 的公共接口,不关心其内部实现细节。
  3. 可扩展性:可以轻松添加新的事件类型(如键盘事件、窗口事件)而不影响现有代码。

与 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 渲染存在问题,我们可以采用备用方案:直接在 DrawPanelpaint() 方法中使用 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) 绘制积分
}

注意事项:

  1. 使用 JLabel 是更标准的做法,因为它支持自动更新、样式设置和事件处理
  2. 直接使用 Graphics.drawString() 绘制文字时,需要手动处理重绘和位置计算
  3. 建议先尝试 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);
    }
}

功能说明:

  1. 游戏状态切换 :将游戏状态设置为进行中(gameState = 1
  2. 数据重置:将时间和积分归零,确保每次游戏都从初始状态开始
  3. 计时器启动 :调用 timer.start() 开始计时,计时器会每秒自动触发更新
  4. 界面反馈:隐藏开始按钮,防止用户在游戏进行中重复点击
6.3 完整的工作流程

整合后的游戏工作流程如下:

  1. 游戏初始化:程序启动,显示开始界面,等待用户点击"开始游戏"按钮
  2. 游戏开始 :用户点击按钮后:
    • 游戏状态切换为进行中
    • 时间和积分重置为 0
    • 计时器开始运行
    • 开始按钮隐藏
  3. 游戏进行中
    • 计时器每秒触发一次,自动更新时间和积分
    • 玩家通过鼠标控制飞机移动
    • 界面实时显示当前时间和积分
  4. 游戏结束:(后续可扩展)当时间到达设定上限或满足其他结束条件时,停止计时器并显示游戏结果

通过以上实现,我们成功将计时器和积分系统集成到了游戏的核心逻辑中。计时器会在游戏开始时自动启动,每秒更新一次时间和积分,为游戏增加了时间限制和得分机制,使游戏体验更加完整。

在后续开发中,你可以根据需要扩展以下功能:

  • 添加游戏结束条件(如时间耗尽、积分达到目标等)
  • 实现暂停/继续功能
  • 添加不同难度级别的时间限制
  • 设计更丰富的积分规则(如击中目标得分、连击奖励等)

到这里,我们已经完成飞机躲避障碍物游戏的基础版本,实现了图片背景、鼠标控制飞机移动、计时计分系统等核心功能。

在本次开发过程中,我踩过不少Swing绘图的坑:比如后台线程造成的偶现诡异BUG、组件监听器绑定错误、paint与paintComponent的混淆,也深入理解了不同计时器之间的区别,体会到Swing单线程模型带来的限制。

GUI开发不能只顾修改数据,更要理清数据与视图分离的思想:监听器负责接收交互、修改游戏状态,绘图面板根据最新状态完成画面渲染。

目前项目还只是基础雏形,后续还可以继续拓展,增加障碍物生成、碰撞检测、游戏结束判定等功能。如果文章有错误,欢迎大家评论区指正,也希望这篇实战可以给正在学习Java Swing的同学带来一点参考,让我们共同进步!

程序运行结果:

相关推荐
YOLO数据集集合1 小时前
火情监测新方案:无人机红外-可见光多模态火点烟雾数据集(含YOLOv11实战)
人工智能·yolo·计算机外设·无人机·无人机数据集·可见光多模态
Rain的Java大神之路1 小时前
如何避免订单重复提交
java·redis·后端·面试·架构·rabbitmq·rocketmq
ps酷教程2 小时前
解析自定义数据
java
wuminyu2 小时前
JDK21中FFM api的upcall回调机制解析
java·linux·c语言·jvm·c++
桦说编程2 小时前
深入理解 FutureTask 状态机——从契约到实现
java·后端·性能优化
松仔log2 小时前
Kotlin中级——协程
java·jvm·kotlin
wifi___7 小时前
全局异常处理的原理
java·开发语言
2601_9638702011 小时前
【计算机毕业设计】基于Spring Boot的专科医院医疗管理系统
java·spring boot·课程设计