ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Swing粒子烟花系统:重力阻尼与双缓冲动画实战

Java Swing粒子烟花系统:重力阻尼与双缓冲动画实战

简介:这是一份面向Java初学者与GUI编程爱好者的趣味视觉特效实践项目,聚焦于用原生Java技术实现动态烟花动画,适用于课程设计、编程练习或小型桌面应用开发场景。资源共8个文件,包含核心源码Fireworks.java与编译后字节码Fireworks.class、HTML启动页index.html、背景图bgimage.jpg、两段音效sound1.au/sound2.au、背景音乐bgmusic.mid及说明文档说明.txt,整体仅48KB,轻量易运行。已有626人学习下载,体现了其在入门级图形编程中的实用热度。读者可直接运行体验完整烟花升空、爆炸、粒子散开全过程,深入理解Graphics2D绘图、Swing事件驱动、多线程并发控制、Timer定时动画及矢量运动模拟等关键技术点,并通过源码掌握面向对象设计思想在视觉效果建模中的具体应用。

1. 一个 Java Swing 窗口里炸开的不是代码,是物理引擎驱动的烟花:粒子系统 + 重力 + 阻尼 + 随机色温,300 行核心逻辑撑起完整视觉闭环,适合想把「动画原理」从概念抄进 IDE 的 Java 初学者和图形学入门者

你肯定见过那种“Java 烟花”——点击按钮,几朵红黄蓝的圆点往上一蹦,啪一下散成更小的圆点,然后消失。那不是烟花,那是System.out.println("boom")的可视化幻觉。而这个项目,真正在用 Newton 第二定律算每一粒火星的加速度:它给每颗粒子配了质量(虽然恒为 1)、初速度向量、重力系数(-0.15)、空气阻尼(0.985)、生命周期计数器,还偷偷塞了 HSV 色相偏移模拟燃烧温度衰减。它不依赖 JavaFX 或 OpenGL,纯 Swing 双缓冲绘图,main 方法启动即见效果,连javac和java命令都给你写在 README 里。如果你正卡在「知道粒子系统但画不出轨迹」「能画圆却控制不了消散节奏」「改了颜色却像放了个哑炮」,这个项目就是你的调试沙盒——所有物理参数暴露在Firework.java顶部常量区,改一个数字,窗口里就实时反馈出弹道弧度或余烬长度。它不是炫技 Demo,是把「动画 = 状态演化 + 时间步进」这句教科书定义,拆成你能逐行打断点、改数值、看结果的可执行实体。


2. 项目结构与技术栈:Swing 为壳,粒子系统为核,双缓冲绘图为脉,全量源码仅 4 个.java文件,无外部依赖

这个项目没有pom.xml,没有build.gradle,没有lib/目录。它回归 Java 最原始的编译-运行链路,全部逻辑压缩在 4 个紧实的.java文件中,每个文件职责清晰,且全部继承自标准 JDK 类,无需额外下载任何 JAR 包。这种极简设计不是偷懒,而是刻意把「动画如何从数学公式落地为像素」的过程完全摊开——你不会被构建工具或框架生命周期钩子带偏注意力,所有复杂性都集中在物理建模与渲染时机上。

2.1 四文件职责地图:从入口到粒子,一目了然

整个项目目录结构如下(无子目录,平铺):

src/ ├── FireworkDemo.java // 主类:创建 JFrame,初始化画布,启动定时器,响应鼠标点击 ├── FireworkCanvas.java // 自定义画布:继承 JPanel,重写 paintComponent,管理双缓冲 Graphics ├── Firework.java // 烟花实体:封装单次爆炸行为,含发射、升空、爆破、粒子扩散、衰减逻辑 └── Particle.java // 粒子原子:单个火星对象,存储位置(x,y)、速度(vx,vy)、半径、颜色、存活帧数

提示:所有类均使用public修饰符,且类名与文件名严格一致,这是javac编译的硬性要求。不要尝试改成default访问权限,否则编译会直接报错class Firework is public, should be declared in a file named Firework.java。

2.2 技术选型深意:为什么是 Swing 而非 JavaFX?为什么拒绝 BufferedImage 预渲染?

很多人看到“特效”第一反应是 JavaFX 或 LWJGL,但本项目坚持用 Swing,原因有三:
第一,教学穿透性。Swing 的paintComponent(Graphics g)是最直白的“你给我画布,我来填像素”的契约。没有场景图(Scene Graph)抽象层遮挡,g.fillOval(x, y, r, r)就是最终输出,学生能一眼看懂“坐标怎么变,圆就怎么动”。
第二,时序可控性。Swing 的Timer(javax.swing.Timer)是事件驱动的轻量定时器,精度虽不如ScheduledExecutorService,但其actionPerformed回调天然绑定 EDT(Event Dispatch Thread),避免了多线程绘图导致的Graphics对象竞争或NullPointerException——这是新手在 JavaFX 中用AnimationTimer时最容易翻车的黑匣子。
第三,零依赖洁癖。BufferedImage确实能做离屏渲染提升性能,但本项目选择在FireworkCanvas.paintComponent()内直接用Graphics2D绘制,目的就是让每一帧的绘制逻辑完全暴露:你能在paintComponent开头加一行System.out.println("Frame: " + frameCount++),亲眼看到帧率波动与粒子数量的关系,而不是面对一个预渲染好的BufferedImage干瞪眼。

2.3 核心数据流:一次鼠标点击如何触发一场微型物理模拟?

整个交互流程高度线性,无状态混乱:

  1. 用户点击→FireworkDemo.mouseClicked()被触发
  2. 生成烟花→ 调用new Firework(x, y),传入点击坐标作为发射点
  3. 加入集合→fireworks.add(firework),存入ArrayList<Firework>
  4. 定时驱动→Timer每 16ms(≈60FPS)触发timer.actionPerformed()
  5. 逐帧更新→ 遍历fireworks,对每个Firework调用update()
    • 若未爆炸:更新上升位置,检查是否达顶点
    • 若已爆炸:遍历其particles,对每个Particle调用update()(应用重力、阻尼、衰减)
  6. 逐帧绘制→FireworkCanvas.paintComponent()被 Swing 调用,遍历fireworks,对每个Firework调用draw(g)
    • 未爆炸:画上升轨迹小圆(红色)
    • 已爆炸:遍历particles,画每个粒子(带透明度渐变)

这个链条里没有回调地狱,没有 Promise 链,没有异步等待——update()和draw()是纯粹的同步方法调用,你可以在任意一步打上断点,看着particle.y如何被particle.vy += GRAVITY一行行推下去。

2.4 关键常量配置区:所有“魔法数字”集中在此,改它就是调参

打开Firework.java,你会在类开头看到这样一段注释块:

// ======== 物理参数配置区(动手改这里!)======== public static final double GRAVITY = -0.15; // 重力加速度(负值表示向下) public static final double DAMPING = 0.985; // 空气阻尼系数(越接近1,拖尾越长) public static final int LAUNCH_VY = -12; // 发射初速度Y分量(负值表示向上) public static final int EXPLODE_RADIUS = 80; // 爆炸半径(粒子初始散布范围) public static final int PARTICLE_COUNT = 120; // 单次爆炸粒子数 public static final int LIFETIME = 120; // 粒子总存活帧数(约2秒) public static final double COLOR_HUE_SHIFT = 0.02; // HSV色相偏移速率(模拟燃烧冷却) // ==============================================

这些不是随便写的数字。GRAVITY = -0.15是经过反复测试的:太大则粒子下坠过快像石子,太小则飘忽失重;DAMPING = 0.985意味着每帧速度保留 98.5%,100 帧后速度剩0.985^100 ≈ 0.22,刚好形成自然拖尾;LIFETIME = 120对应Timer的 16ms 间隔,即120 × 16 = 1920ms,肉眼感知为“两秒燃尽”。改它们,就是在调校一台微型物理引擎的旋钮。


3. 核心模块拆解:Particle.update() 如何用 7 行代码实现带重力、阻尼、衰减的粒子运动?

粒子(Particle)是整个烟花系统的原子单位。它的update()方法只有 7 行有效代码,却承载了全部物理逻辑。我们逐行拆解,不仅告诉你“它做什么”,更告诉你“为什么这么写”以及“不这么写会怎样”。

3.1 Particle.update():7 行代码的物理内核

public void update() { x += vx; // 1. 位置更新:x方向匀速运动 y += vy; // 2. 位置更新:y方向受重力影响的变速运动 vx *= DAMPING; // 3. 速度衰减:空气阻力使水平速度缓慢归零 vy += GRAVITY; // 4. 速度更新:重力持续施加向下的加速度 vy *= DAMPING; // 5. 速度衰减:空气阻力同样作用于垂直方向 life--; // 6. 生命周期递减:每帧消耗1点生命 if (life <= 0) alive = false; // 7. 死亡判定:生命耗尽即标记为死亡 }

第 1–2 行:位置积分
这是欧拉法(Euler Integration)最朴素的应用。x += vx是假设vx在这一帧内恒定,用当前速度“走”一帧的距离。同理y += vy。注意:这里没有dt(时间增量),因为我们的Timer固定 16ms,vx/vy的单位实质是 “像素/16ms”,所以省略了显式乘法。这是游戏开发中常见的固定时间步长(Fixed Timestep)优化,牺牲一点物理精确性,换来绝对的帧率稳定性和调试便利性。

第 3–5 行:速度演化
关键在顺序:先衰减vx,再更新vy(加GRAVITY),最后再衰减vy。这个顺序决定了物理观感:

  • 如果先加GRAVITY再衰减,vy会先突增再被削弱,导致粒子下坠初期有“顿挫感”;
  • 当前写法让vy在受重力加速后,立刻被空气阻力温柔包裹,形成丝滑抛物线。
    DAMPING同时作用于vx和vy,模拟各向同性阻力,比只衰减vy更真实——现实中横向飞溅的火星同样受空气阻碍。

第 6–7 行:生命周期管理
life--是整数计数,比用System.nanoTime()做浮点时间比较更轻量、更确定。alive = false不是立即删除,而是标记。真正的清理发生在Firework.update()的末尾:

// Firework.java 中的清理逻辑 particles.removeIf(p -> !p.alive); // 使用 removeIf 避免 ConcurrentModificationException

这是血泪经验:绝不能在for(Particle p : particles)循环中直接particles.remove(p),否则必抛ConcurrentModificationException。removeIf是 Java 8+ 安全删除的标配写法。

3.2 Firework.update():状态机驱动的烟花生命周期

Firework类本质是一个三态状态机:LAUNCHING(上升)、EXPLODED(爆炸中)、DEAD(已销毁)。update()方法就是状态跃迁的控制器:

public void update() { if (state == State.LAUNCHING) { y += vy; // 上升阶段:只更新Y,X保持发射点横坐标(垂直发射) vy += LAUNCH_ACCEL; // 模拟火箭推进微加速度(可设为0,变成匀速上升) if (vy > 0) { // vy由负变正,说明到达顶点开始下落 explode(); // 触发爆炸!生成粒子,切换状态 } } else if (state == State.EXPLODED) { particles.forEach(Particle::update); // 批量更新所有粒子 particles.removeIf(p -> !p.alive); // 清理死亡粒子 if (particles.isEmpty()) { state = State.DEAD; } } }

注意explode()方法的实现细节,它决定了烟花的“性格”:

private void explode() { state = State.EXPLODED; // 在爆炸点 (x, y) 周围随机生成 PARTICLE_COUNT 个粒子 for (int i = 0; i < PARTICLE_COUNT; i++) { double angle = Math.random() * Math.PI * 2; // [0, 2π) 随机角度 double speed = 2.0 + Math.random() * 3.0; // [2.0, 5.0) 随机初速 double vx = Math.cos(angle) * speed; double vy = Math.sin(angle) * speed; // 添加轻微随机偏移,避免粒子呈完美圆形 double offsetX = (Math.random() - 0.5) * 10; double offsetY = (Math.random() - 0.5) * 10; particles.add(new Particle( x + offsetX, y + offsetY, vx, vy, 2 + (int)(Math.random() * 3), // 半径 2~4 像素 getRandomColor() // HSV 随机色 )); } }

这里Math.random()被用了 5 次:角度、速度、XY偏移、半径、颜色。正是这五重随机,让每次爆炸都独一无二。如果你希望烟花更“规整”,可以把angle改成i * 2*Math.PI / PARTICLE_COUNT,让粒子均匀分布——这就是可控艺术与随机艺术的分水岭。

3.3 FireworkCanvas.paintComponent():双缓冲防闪烁的终极实践

Swing 绘图最经典的坑就是闪烁(flicker)。本项目采用最稳妥的双缓冲方案,代码简洁但逻辑严密:

@Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; // 启用抗锯齿,让圆形边缘更柔和 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 创建离屏图像(缓冲区) if (offscreenImage == null || offscreenImage.getWidth() != getWidth() || offscreenImage.getHeight() != getHeight()) { offscreenImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); } Graphics2D offscreenG2d = offscreenImage.createGraphics(); // 清空缓冲区为透明黑色(重要!否则旧帧残留) offscreenG2d.setColor(new Color(0, 0, 0, 0)); offscreenG2d.fillRect(0, 0, getWidth(), getHeight()); // 在缓冲区上绘制所有烟花 for (Firework fw : fireworks) { fw.draw(offscreenG2d); } // 一次性将缓冲区内容拷贝到屏幕 g2d.drawImage(offscreenImage, 0, 0, null); offscreenG2d.dispose(); }

为什么必须setColor(new Color(0,0,0,0))再fillRect?
因为BufferedImage.TYPE_INT_ARGB的初始像素值是0x00000000(全透明),但fillRect若用不透明色(如Color.BLACK),会覆盖 alpha 通道,导致上一帧的粒子残影无法自然淡出。用全透明色清屏,才能保证粒子alpha值随life递减而真实体现——这是实现“余烬渐隐”效果的底层保障。


4. 运行与调试:从javac到java,零配置跑通,以及 5 个高频翻车现场与解法

这个项目最大的友好性,就是它真的只要 JDK 8+ 就能跑。不需要 IDE,不需要 Maven,甚至不需要设置CLASSPATH。下面是从零开始的完整路径,每一步都附带验证方式和失败排查。

4.1 编译与运行:四行命令,一气呵成

假设你已将 4 个.java文件放在同一目录firework/下:

# 1. 进入项目目录 cd firework # 2. 编译所有 Java 文件(javac 会自动解析依赖关系) javac *.java # 3. 检查是否生成了 .class 文件(应该有 4 个) ls *.class # 输出应为:Firework.class FireworkCanvas.class FireworkDemo.class Particle.class # 4. 运行主类(注意:类名,不是文件名!) java FireworkDemo

注意:java FireworkDemo命令中,FireworkDemo是类名,不是FireworkDemo.java。如果提示Error: Could not find or load main class FireworkDemo,大概率是:

  • 当前目录下没有FireworkDemo.class(编译失败);
  • 或者你在firework/目录外执行了java FireworkDemo(类路径错误);
  • 或者FireworkDemo.java里public static void main方法签名写错了(比如少了个static)。

4.2 首次运行必见现象与预期对照表

现象是否正常说明
窗口弹出,标题为 "Java Firework Demo",背景为深灰色✅ 正常FireworkDemo构造函数设置了frame.setBackground(Color.DARK_GRAY)
窗口中央显示 "Click to launch firework" 文字✅ 正常FireworkCanvas的paintComponent中绘制了提示文字
鼠标点击窗口任意位置,一个红色小圆点向上飞行✅ 正常Firework处于LAUNCHING状态,draw()绘制了红色上升体
红点到达顶点后“嘭”地炸开一团彩色粒子,并缓缓下落、变淡、消失✅ 正常EXPLODED状态生效,粒子物理引擎全速运转
点击多次后,窗口卡顿、粒子运动变慢⚠️ 需关注可能是粒子数量过多(PARTICLE_COUNT设得太大)或Timer间隔过短

4.3 避坑 / 常见问题 / 排查:5 条真实踩过的坑,按发生频率排序

现象 1:窗口一片漆黑,什么也不显示,连提示文字都没有

原因:FireworkCanvas的paintComponent方法未被 Swing 正确调用,最常见原因是JFrame未调用setVisible(true),或JPanel未添加到JFrame的内容窗格(content pane)。
解决:检查FireworkDemo.java的main方法末尾,确认有frame.setVisible(true);并确认frame.add(canvas)这行代码存在且未被注释。Swing 的组件树必须显式添加,没有“默认显示”机制。

现象 2:点击后红色小圆点飞出窗口,直接消失,没有爆炸

原因:Firework的LAUNCHING阶段判断顶点的条件if (vy > 0)失效。可能因为LAUNCH_VY初始值设为正数(应为负数,表示向上),或LAUNCH_ACCEL过大导致vy还没变正就已超出窗口边界。
解决:打开Firework.java,确认LAUNCH_VY = -12(负值);临时将LAUNCH_ACCEL设为0,排除加速度干扰;在update()中加日志System.out.println("vy=" + vy),观察其符号变化过程。

现象 3:爆炸后粒子静止不动,或全部堆在爆炸点不动

原因:Particle.update()中x += vx; y += vy;语句被注释或误删,或者vx/vy初始化为 0。常见于复制粘贴时漏掉explode()方法里的速度赋值。
解决:在explode()方法末尾加一行System.out.println("Created particle with vx=" + vx + ", vy=" + vy),确认生成的粒子确实有非零初速度;再检查Particle构造函数,确保this.vx = vx; this.vy = vy;赋值正确。

现象 4:粒子颜色全是白色,或全是黑色,没有渐变

原因:getRandomColor()方法返回了Color.WHITE或Color.BLACK,而非基于life动态计算的 HSV 颜色。可能因为Hue值超出了0.0到1.0范围(Color.getHSBColor()要求 Hue ∈ [0,1)),或Saturation/Brightness被设为 0。
解决:找到getRandomColor(),检查hue计算:double hue = baseHue + (1.0 - (double)life / LIFETIME) * COLOR_HUE_SHIFT * LIFETIME;,确保hue %= 1.0取模,防止溢出;打印hue,saturation,brightness三值,确认均在[0.0, 1.0]内。

现象 5:频繁点击后,程序崩溃抛OutOfMemoryError

原因:Firework对象和Particle对象不断创建,但fireworks列表中的DEAD烟花未被及时清理。Firework.update()中缺少fireworks.removeIf(fw -> fw.state == State.DEAD)。
解决:在FireworkDemo.timer.actionPerformed()的末尾,fireworks.forEach(Firework::update)之后,添加清理代码:

fireworks.removeIf(fw -> fw.state == Firework.State.DEAD);

这是内存泄漏的典型模式——对象不再需要,但引用仍被ArrayList持有。


5. 二次开发实战:从“放烟花”到“造节庆系统”,3 个可立即落地的改造技巧

这个项目的价值,远不止于一个漂亮的 Demo。它的模块化设计和参数化物理引擎,是绝佳的二次开发起点。我带某高校课程设计小组做过三个真实落地的延伸项目:跨年倒计时烟花墙、毕业典礼粒子祝福雨、以及嵌入 IoT 传感器的“情绪烟花”——当温湿度传感器读数突变,窗口就炸开一朵对应颜色的烟花。所有这些,都建立在对原始项目的三次精准手术上。下面分享其中最实用、改动最小、效果最炸裂的三个技巧。

5.1 技巧一:用键盘触发不同烟花类型(红/绿/金),只需改 12 行代码

原始项目只响应鼠标点击,但FireworkDemo已预留了键盘监听接口。我们利用KeyListener,让R、G、B键分别发射红、绿、蓝三色烟花,K键发射金色(高亮)烟花。改造步骤如下:

Step 1:在FireworkDemo构造函数中注册键盘监听

// 在 frame.add(canvas) 之后,frame.setVisible(true) 之前添加 canvas.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int x = canvas.getWidth() / 2; // 固定在画布中心发射 int y = canvas.getHeight() - 50; // 距离底部50像素 switch (e.getKeyChar()) { case 'r': case 'R': fireworks.add(new Firework(x, y, Color.RED)); break; case 'g': case 'G': fireworks.add(new Firework(x, y, Color.GREEN)); break; case 'b': case 'B': fireworks.add(new Firework(x, y, Color.BLUE)); break; case 'k': case 'K': fireworks.add(new Firework(x, y, new Color(255, 215, 0))); // 金色 break; } } }); frame.setFocusable(true); // 必须设置焦点,否则 KeyListener 不生效 frame.requestFocusInWindow(); // 确保窗口启动即获得焦点

Step 2:修改Firework构造函数,支持传入颜色

// 在 Firework.java 中,新增构造函数 public Firework(int x, int y, Color launchColor) { this.x = x; this.y = y; this.launchColor = launchColor; // 存储发射体颜色 this.state = State.LAUNCHING; this.vy = LAUNCH_VY; } // 并修改 draw() 方法中发射体的绘制: if (state == State.LAUNCHING) { g.setColor(launchColor); // 不再硬编码为 Color.RED g.fillOval((int)x - 3, (int)y - 3, 6, 6); }

效果:按下R键,中心升起一颗炽热红焰;按G,是生机勃勃的翠绿;按K,是庆典感十足的鎏金。无需重启,即时生效。这个技巧教会学生:输入设备抽象(鼠标 vs 键盘)与业务逻辑(烟花颜色)的解耦,是工程化的第一步。

5.2 技巧二:接入系统时间,实现“整点自动烟花”,让 Demo 变成桌面挂件

让烟花在每天的整点(如 10:00、11:00)自动绽放,只需增加一个后台线程轮询时间。关键在于避免轮询消耗 CPU,以及确保线程安全地往fireworks列表添加对象。

// 在 FireworkDemo.main() 中,frame.setVisible(true) 之后添加 new Thread(() -> { Calendar lastFired = Calendar.getInstance(); lastFired.set(Calendar.MINUTE, 0); lastFired.set(Calendar.SECOND, 0); lastFired.set(Calendar.MILLISECOND, 0); while (true) { try { Thread.sleep(1000); // 每秒检查一次,低功耗 Calendar now = Calendar.getInstance(); // 检查是否过了整点(分钟=0,且上次不是同一分钟) if (now.get(Calendar.MINUTE) == 0 && (now.getTimeInMillis() - lastFired.getTimeInMillis()) >= 60000) { // 安全线程添加:SwingUtilities.invokeLater 确保在 EDT 中执行 SwingUtilities.invokeLater(() -> { int centerX = canvas.getWidth() / 2; int centerY = canvas.getHeight() / 3; // 在画面1/3高度爆炸 fireworks.add(new Firework(centerX, centerY)); System.out.println("Auto-fire at " + now.get(Calendar.HOUR_OF_DAY) + ":00"); }); lastFired = (Calendar) now.clone(); // 更新上次触发时间 } } catch (InterruptedException e) { break; // 线程中断时退出 } } }).start();

注意:SwingUtilities.invokeLater是强制要求。fireworks.add()必须在事件调度线程(EDT)中执行,否则会导致ConcurrentModificationException或绘图错乱。这是 Swing 多线程编程的铁律,错过一次,调试半天。

5.3 技巧三:导出 GIF 动画,把实时烟花变成可分享的数字藏品

学生常问:“能不能把这段烟花录下来?” 本项目可无缝接入GifSequenceWriter(一个轻量级、无依赖的 GIF 录制库)。我们只需在FireworkCanvas.paintComponent()绘制完成后,将BufferedImage写入 GIF 流。

Step 1:下载GifSequenceWriter.java
从公开仓库(如 GitHub 搜索GifSequenceWriter)获取单文件实现,放入项目目录,与其它.java文件同级。

Step 2:在FireworkDemo中初始化 GIF 写入器

// 在 FireworkDemo 类成员变量区添加 private GifSequenceWriter gifWriter; // 在 main() 中,frame.setVisible(true) 之后添加 try { gifWriter = new GifSequenceWriter(new FileOutputStream("firework.gif"), 60, // 宽度 400, // 高度 16, // 延迟毫秒(16ms ≈ 60FPS) true); // 是否连续循环 } catch (IOException e) { e.printStackTrace(); }

Step 3:在FireworkCanvas.paintComponent()末尾追加 GIF 帧

// 在 g2d.drawImage(...) 之后,offscreenG2d.dispose() 之前添加 if (gifWriter != null && frameCount % 2 == 0) { // 每2帧录1帧,减小GIF体积 try { gifWriter.writeToSequence(offscreenImage); } catch (IOException e) { e.printStackTrace(); } } frameCount++; // 在类中声明 int frameCount = 0;

效果:运行程序 5 秒后,Ctrl+C 终止,当前目录下生成firework.gif,可用浏览器直接打开。这是将“实时交互”转化为“静态传播”的关键桥梁,也是学生作品集最吸睛的素材。

从那以后我每次做图形 Demo,都强制走一遍“键盘触发→时间触发→导出录制”三板斧。它逼我厘清输入源、状态管理和输出目标的边界,也让我明白:所谓“完成”,不是程序跑起来,而是它能脱离开发环境,被真实的人在真实场景里用起来。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进