Java飞机大战小游戏源码实战:Swing碰撞检测与面向对象设计

发布时间:2026/10/30 15:39:36

Java飞机大战小游戏源码实战:Swing碰撞检测与面向对象设计 简介一份基于Java实现的飞机大战小游戏完整项目源码适合Java编程初学者、课程设计学生以及游戏开发入门者用来学习图形界面编程、游戏循环机制、事件监听与碰撞检测等核心技能。项目基于Eclipse构建源码与编译文件齐全可直接导入开发环境运行也能对照源码逐模块分析弹幕生成、敌机刷新、得分计分等实现思路。压缩包共73个文件包括52张游戏素材图片飞机、爆炸、背景等、7个Java源文件、10个class编译文件以及工程配置的.project、.classpath和prefs文件结构完整便于本地调试与二次修改整包仅774KB非常轻量。现有444人浏览学习适合需要一份短小而典型的Java游戏案例来巩固面向对象编程和Swing/AWT界面开发的读者。通过该项目可以快速理解小游戏的完整框架并基于素材和源码进行功能扩展是期末作业或自学练手的实用资料。1. 飞机大战小游戏项目源码适合课程设计也适合Java入门飞机大战小游戏项目源码是Java学习里最经典的一类练手项目几乎所有Java基础书都会拿它当综合案例。你会用到Swing做图形界面、KeyListener处理键盘、javax.swing.Timer驱动游戏循环、矩形相交做碰撞检测再加上面向对象编程Java常见的类设计思路——这个项目正好把这些散落的知识点拧成一个完整可运行的程序。如果你正在准备Java面试或课程设计把这份源码跑起来、改一改、能讲清楚每段代码为什么这么写比背十道java面试题更有说服力。下面我按自己从空窗到跑通的做法把整个项目拆开讲一遍。2. 直接能跑的架构把飞机大战拆成5个类和一个主循环2.1 为什么选Swing而不是JavaFX或控制台很多人在选界面库时纠结。控制台版飞机大战虽然能跑但用字符模拟飞机和子弹根本练不到GUI编程和事件驱动JavaFX足够现代但引入FXML、Scene Builder概念对新手来说学习曲线陡而且很多学校OJ和课程设计还在用Swing。我从实战角度首选SwingJDK内置、无需额外依赖、跨平台稳定你写出的GamePanel继承JPanel重写paintComponent方法就能在窗口里画飞机、画子弹、画分数。这个项目用Swing还能顺带理解JFrame和JPanel的层级关系JFrame是外壳JPanel是画布两者关系搞明白了后面改界面布局不会懵。Swing的绘制机制值得先弄清楚。游戏画面不是一遍遍静态画上去而是每一帧都要清屏、重绘。paintComponent里你先调用super.paintComponent(g)清理画布然后根据当前游戏对象的状态玩家位置、子弹坐标、敌机列表全部画一遍下一帧之前状态变了再画一遍视觉上就成了动画。这就是飞机大战源码最核心的“重绘画布”思路后面所有玩法都挂在这上面。从维护角度看Swing的版本兼容性也更好。同样的源码放在JDK8和JDK17上大部分情况下不用改就能跑这对交课程设计时到处换电脑演示很重要。如果用JavaFX不同版本的模块导出配置就可能把人折腾半小时。我在实际带项目的过程中见过太多次“代码没问题就是跑不起来”的尴尬最后都死在环境上。所以小游戏源码选Swing不是因为高级而是因为省心。2.2 源码目录怎么组织GameFrame / GamePanel / Player / Enemy / Bullet我一般会把源码分成五个类加一个入口。GameFrame负责创建窗口设置大小、标题、关闭策略GamePanel里放游戏循环、绘制、键盘监听、碰撞检测、分数生命值这些主逻辑Player、Enemy、Bullet三个类只管理自己“是什么、在哪、怎么动”——这种把数据和动作封装到类里的方式正是面向对象编程Java的精髓。偷懒做法是把所有代码写在一个300行的GamePanel里课程设计能过但后续加道具、加Boss会非常痛苦所以我建议一开始就按五个类分。为了让新手机器能跑我给出典型的源码目录src/ Main.java GameFrame.java GamePanel.java Player.java Enemy.java Bullet.java有人会把每个类单独一个包我不反对但课程设计提交时越简单越不容易出现包名不一致的问题。直接放在默认包下编译运行最省事。各个类的职责可以对照下表看运行时对象之间的关系是“面板持有玩家、敌机、子弹的集合玩家/敌机/子弹各自负责自己的绘制”。类名职责关键成员Main程序入口main方法GameFrame创建窗口、装配面板JFrame属性GamePanel游戏主循环、键盘、碰撞、状态Timer, Player, ListPlayer玩家飞机位置与移动x, y, speedEnemy敌人飞机位置与移动x, y, speedBullet子弹位置与移动x, y, speed这里额外提一句Enemy里我原本会放一个alive布尔值后来发现直接用remove掉更省事布尔值反而可能引发“删除后还被后续碰撞误判”的bug。如果你是初学者坚持一个原则一个对象被判定死亡后立刻从集合里remove而不是给它翻身真需要延迟消失也要在碰撞检测开头判断alive。这是第一个可能会让你感到玄学掉帧的小陷阱后面还会细说。2.3 从零搭出主入口Main和GameFrame的最小代码Main.java最简单的写法是创建一个GameFrame实例让它可见。常见做法是写一个main方法new GameFrame()。这里注意两点一是窗口默认不可见必须调用setVisible(true)二是关闭策略要选EXIT_ON_CLOSE否则窗口关闭后后台线程还可能继续跑。代码如下public class Main { public static void main(String[] args) { // 创建主窗口传入宽高内部完成所有加载 GameFrame frame new GameFrame(800, 600); frame.setVisible(true); } }这段代码很简单但参数说明一下宽高我习惯用800x600这个尺寸在大多数屏幕上都能完整显示碰撞检测坐标也不会因为窗口大小不同而错位。如果你要在高分屏上跑可以把窗口设成960x640但注意飞机和敌人的初始坐标、移动边界都要跟着改否则会出现飞机飞不出屏幕一半的怪现象。GameFrame.java同样不复杂。它继承JFrame构造器里设置标题、大小、居中、关闭策略然后把GamePanel塞进窗口。关键点在于GamePanel里的游戏循环需要拿到焦点才能响应键盘所以要把GamePanelsetFocusable(true)并且放在窗口内容区域的最前面。代码演示如下import javax.swing.JFrame; public class GameFrame extends JFrame { public GameFrame(int width, int height) { setTitle(飞机大战小游戏); setSize(width, height); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 窗口居中 GamePanel panel new GamePanel(); add(panel); // 把画布放进窗口 pack(); // 按面板首选大小撑起窗口 setVisible(true); } }这里有一个常见的坑如果你给GamePanel设置了setPreferredSize(new Dimension(width, height))那pack()之后窗口大小正好等于面板大小如果你只setSize(width, height)面板可能铺不满窗口导致右边和下边露出白边。我建议用pack()让面板自己决定尺寸更符合Swing布局习惯。另外JFrame的setVisible(true)要放在添加面板之后否则在某些Linux窗口管理器下面板会不显示这也是“运行后什么都不显示”的排查方向之一。2.4 玩家类和敌机类把坐标、速度和绘制封装起来Player类要清楚自己是谁一张飞机图片或一个矩形位置(x, y)速度speed宽度和高度。这里我倾向于不预先加载图片先用Color填充矩形代替飞机等全部功能跑通后再换图片。用矩形而不是图片能让碰撞检测简单一个量级这个决定后面会帮你省不少力气。下面是Player类的完整代码import java.awt.Graphics; import java.awt.Color; public class Player { // 坐标与尺寸speed 表示每次刷新移动的像素 public int x, y, width 50, height 60; public int speed 5; public Player(int startX, int startY) { this.x startX; this.y startY; } // 自己画自己方便GamePanel统一遍历绘制 public void draw(Graphics g) { g.setColor(Color.GREEN); g.fillRect(x, y, width, height); } // 左移、右移边界由GamePanel来约束 public void moveLeft() { x - speed; } public void moveRight() { x speed; } }为什么把draw放在Player内部而不是GamePanel里因为一个类如果只存数据GamePanel里就要写一堆switch分支来根据类型画不同形状现在每个对象都有draw方法GamePanel只需要遍历所有对象调用draw代码结构立刻清爽。这也是一个经典的面向对象编程Java练习一个类该干什么由它自己负责。后续想换图片也只需要在Player类里把fillRect换成g.drawImage游戏面板不用动。Enemy类的结构和Player类似只是多一个“掉落速度”和“是否存活”字段。后续章节会写敌人队列这里不展开。当你看到这里整个源码骨架已经有了窗口、画布、玩家还差循环和交互下一章就把游戏真正“启动”起来。3. 核心玩法定时循环、键盘控制和碰撞检测的落地写法3.1 游戏循环与刷新率javax.swing.Timer的interval参数怎么定游戏要动起来必须每隔一小段时间更新所有对象的位置并重绘。常见做法有两个一是Java线程加while(true)循环二是javax.swing.Timer。我强烈推荐Timer它会在事件分发线程EDT上周期性执行天然规避了多线程操作Swing组件的线程安全问题。interval参数决定一秒钟多少帧对于飞机大战我一般用16ms约62FPS和显示器刷新率接近画面顺滑如果机器性能差可以放宽到30ms但不能再大否则子弹一帧会跳好几个像素碰撞检测会变得诡异。GamePanel里的Timer初始化如下import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class GamePanel extends JPanel implements ActionListener { private Timer timer; private Player player; private java.util.ListEnemy enemies; private java.util.ListBullet bullets; public GamePanel() { setPreferredSize(new java.awt.Dimension(800, 600)); setFocusable(true); player new Player(375, 520); enemies new java.util.ArrayList(); bullets new java.util.ArrayList(); timer new Timer(16, this); // 16毫秒一帧 timer.start(); } Override public void actionPerformed(ActionEvent e) { updateGame(); // 更新所有对象位置 repaint(); // 触发paintComponent重绘 } }这里的关键是actionPerformed在Timer每触发一次时执行一次你在这个方法里改状态、调用repaint。repaint不是立刻画而是请求Swing在合适时机调用paintComponent所以就算你的updateGame里做了大量运算也不会每帧都阻塞窗口响应。我见过有人手动循环里直接调用paintComponent这种写法会闪烁且很难控制刷新节奏时间一长CPU占用会很高。Timer的interval不是越小越好。IDE里运行时的调试、断点甚至后台GC都可能导致某几帧卡顿如果interval只有5ms一卡就是好几个周期游戏会一冲一冲。16ms给了系统足够的缓冲。如果你要严格固定帧率更稳妥的办法是记录上一帧的System.nanoTime()再根据实际消耗时间调整位置增量不过对飞机大战这个量级固定间隔完全够用。3.2 键盘控制KeyListener还是KeyBindings飞机需要左右移动还要能按空格发子弹。新手往往直接加KeyListener但在JPanel上常常有“按键失灵”问题——因为JPanel没有焦点时KeyListener根本收不到事件。有两个办法第一在GamePanel构造器里加setFocusable(true)然后在addNotify()里请求焦点第二用KeyBindings把键位绑定到Action不需要焦点。项目源码里最常用的是KeyListener代码直观适合教学。下面给出实现import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; // 在构造器里添加 addKeyListener(new KeyAdapter() { Override public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: player.moveLeft(); break; case KeyEvent.VK_RIGHT: player.moveRight(); break; case KeyEvent.VK_SPACE: shoot(); break; } } });注意这段代码放在构造器里。移动边界需要在GamePanel的updateGame里做否则飞机能一直跑到窗口外面。边界约束很简单x不能小于0x加宽度不能大于面板宽度y固定不变。我会把边界判断放在updateGame里不放在Player类里因为Player不知道自己所在窗口多大由面板来约束更合理。至于按一下动好几格的问题原因和键盘重复触发有关系。KeyListener按下后系统会持续触发keyPressed所以就算手指不抖动飞机也会一直前进想实现“按一下走一步”的话需要一个boolean数组记录按键状态在updateGame里根据状态移动。后面避坑章会再讲。还有一个隐藏参数Timer间隔和speed的乘积决定了飞机的实际移动速度。16ms一帧、speed5时飞机每秒移动约312像素如果你把Timer改成10msspeed还是5飞机就会明显变快。所以调整手感时不要只动speed要同时看每帧时间。我一般以“每秒移动多少像素”为目标反向推算speed目标速度300px/s每帧16ms那speed300/60≈5。这个换算关系很多人忽略导致改完Timer后飞机快得没法控制。3.3 碰撞检测如何用矩形相交来判断命中碰撞检测看起来复杂其实飞机大战这种2D游戏用矩形相交就够了每个对象都有x、y、width、heightSwing的Rectangle类提供了intersects方法判断两个矩形是否重叠。子弹打中敌机就是子弹的矩形和某个敌机的矩形相交玩家撞上敌机就是玩家矩形和该敌机矩形相交。我在updateGame里这样处理private void checkCollisions() { // 用迭代器遍历一边删除一边遍历 java.util.IteratorBullet bIt bullets.iterator(); while (bIt.hasNext()) { Bullet b bIt.next(); // 子弹飞出屏幕顶部直接移除 if (b.y 0) { bIt.remove(); continue; } java.util.IteratorEnemy eIt enemies.iterator(); while (eIt.hasNext()) { Enemy e eIt.next(); // 坐标重叠即判定命中 if (b.x e.x e.width b.x b.width e.x b.y e.y e.height b.y b.height e.y) { bIt.remove(); eIt.remove(); score 10; break; // 一颗子弹只打一架敌机 } } } }这里我故意没直接用Rectangle.intersects而是展开了条件判断。为什么因为Rectangle.intersects的逻辑是“严格内部相交”两个矩形刚好边缘贴在一起会返回false展开写可以自己控制“是否包含边缘”。实际游戏里子弹和敌机都运动很快使用还是影响不大但展开写对新手学习碰撞几何很有帮助。参数说明score放在碰撞检测里加每命中一架加10分。b.y 0的边界移除必须放在碰撞之前否则子弹飞出屏幕后仍然和敌机列表比较白白浪费CPU。如果以后要做“子弹擦边也判命中”把改成就行。想做圆形判定也只需要改成比较圆心距和半径之和。这些细节在真实项目里就是调手感的地方不要迷信Rectangle.intersects它不是为游戏设计的而是为UI布局设计的语义和我们想要的碰撞不完全一样。为了验证碰撞检测是否准确我通常在paintComponent里临时给飞机和敌人画上边框用不同颜色标出矩形范围。如果肉眼看到矩形重叠但没触发得分说明碰撞条件是边缘相等的问题如果矩形没重叠却得分了说明你的坐标基准和宽度高度换算错了。这种方法比打日志更直观算是手写碰撞检测的调试利器。4. 给源码加上分数、血量和敌人刷新的小设计4.1 分数和生命值的变量设计与UI显示有了碰撞检测顺手就能加分数和血量。分数是一个int血量也是一个int。但要注意GamePanel的生命周期很长如果直接在面板里声明变量要小心重置逻辑。我建议在GamePanel里定义private int score; private int life;在resetGame方法里初始化为0和3。绘制时在paintComponent里用g.drawString画到左上角。绘制代码示例Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 绘制背景 g.setColor(Color.BLACK); g.fillRect(0, 0, getWidth(), getHeight()); // 绘制所有对象 player.draw(g); for (Enemy e : enemies) { e.draw(g); } for (Bullet b : bullets) { b.draw(g); } // 绘制分数和血量 g.setColor(Color.WHITE); g.drawString(分数: score, 10, 30); g.drawString(生命: life, 10, 50); // 生命为0时显示结束信息 if (life 0) { g.setColor(Color.RED); g.drawString(游戏结束, getWidth() / 2 - 40, getHeight() / 2); } }绘画时注意顺序先画背景再画所有对象最后画UI信息否则UI会被飞机盖住。我见过有人先画飞机再画背景结果飞机直接消失了这个顺序很基础但在源码里经常错。生命值归零的提示也可以加一局再玩一次后面避坑章会说重启逻辑。还有一点drawString的参数里坐标是文本左下角的基线位置不是左上角。如果想让文本显示在“距离窗口左侧10像素、顶部30像素”的位置直接写10,30即可想垂直居中需要基于FontMetrics计算文本高度不能简单用getHeight()/2否则会偏上。这个小细节不影响功能但影响观感。4.2 敌机生成策略定时生产与难度递增敌机不是一开始就铺满屏幕而是每隔一段时间生成一架否则游戏没法玩。常见做法是在actionPerformed里记录帧数每当累计一定帧数就new一个Enemy加入列表同时随着score增大缩短生成间隔。两个参数在这里很关键生成间隔帧数和敌机下落速度。private int frameCount 0; private int enemyInterval 60; // 初始每60帧生成一架敌机 private void spawnEnemy() { frameCount; if (frameCount enemyInterval) { int x (int)(Math.random() * (getWidth() - 60)); Enemy e new Enemy(x, 0, 2 score / 100); // 速度随分数微增 enemies.add(e); frameCount 0; } }Math.random()只能生成[0,1)的浮点数要生成范围坐标需要乘上宽度再减掉敌机宽度不然敌机可能贴着右边界生成一半身体留在窗外。enemyInterval从60逐渐降到20配合速度增加就是难度曲线。我这里用了一个简单公式速度2 score/100分数越高敌人掉得越快。学有余力的话把敌人设成几波一波生成更像商业游戏。这里有个参数调整的血泪经验敌人速度和player速度要成正比不然玩家躲不开。假设player.speed5每帧移动5像素而敌机下落速度一开始就是5那么玩家和敌人相对速度接近0你几乎躲不掉垂直掉下来的敌人。初始速度设2等玩家逐渐适应再随着分数涨到4、5才合理。这也是做游戏平衡时最容易翻车的点。4.3 列表遍历的坑为什么ConcurrentModificationException代码跑到一半你会突然看到java.util.ConcurrentModificationException。原因很简单你一边用for-each遍历enemies列表一边又在碰撞检测或分数更新里remove元素。for-each实际用的是迭代器如果在迭代过程中列表被修改不是通过同一个迭代器迭代器就会立刻抛异常。我的建议是凡是需要在遍历时删除元素的循环一律用Iterator或者先收集要删的引用循环结束后统一remove。下面这段是我推荐的统一删除模式java.util.ListEnemy toRemove new java.util.ArrayList(); for (Enemy e : enemies) { if (e.y getHeight()) { toRemove.add(e); // 飞出屏幕待删除 } } enemies.removeAll(toRemove);这样主遍历用的列表没有在遍历中被修改等到循环完成再调用removeAll不会抛异常。代价是有一个临时列表对几百个敌人来说可以忽略。子弹的遍历也应该用这种方式只是示例中我用了迭代器因为碰撞检测的删除逻辑是嵌套的写起来更直观。也有同学图省事用ConcurrentLinkedQueue或CopyOnWriteArrayList来避免异常但我认为没必要。一个是普通单线程游戏不会有并发写另一个是CopyOnWriteArrayList的复制成本在高帧率下很明显还会掩盖真正的设计问题。老老实实用迭代器或临时列表源码的可读性和性能都更好。4.4 子弹发射频率和对象上限两个容易被忽略的参数shoot()方法里new一个Bullet加入bullets列表。如果不限制发射频率按住空格时每帧都生成一颗子弹几十帧后列表里会有几百颗子弹游戏逻辑变慢。解决方法是记录上一次发射时间或帧数间隔至少10帧即每秒6发左右。另外可以对bullets列表上限做约束比如当列表长度超过20时最早的子弹必须移除防止内存无限增长。private int lastShootFrame 0; private void shoot() { if (frameCount - lastShootFrame 10) { return; // 未达到射击冷却 } lastShootFrame frameCount; Bullet b new Bullet(player.x player.width / 2 - 2, player.y - 15, 5); bullets.add(b); if (bullets.size() 20) { bullets.remove(0); // 队列太长丢掉最早的子弹 } }这里frameCount用上一节累积的帧计数不重置只让它递增。子弹速度设成5像素/帧60FPS下每秒300像素看起来正好。上下限设置的作用是让源码稳定运行也是面试官爱问的“内存边界”问题。子弹生成位置要用player.x player.width / 2 - 2来居中很多人直接写player.x子弹就会从飞机左上角射出去看着像飞机外挂。5. 常见问题排查飞机大战源码的5个翻车现场5.1 画面闪烁严重飞机像在跳舞现象运行后窗口不停闪动飞机边缘有拖影敌人一多画面就跟心电图一样。原因最常见的是paintComponent没有先调用super.paintComponent(g)。Swing的重绘机制会先用背景色填充画布你跳过这一步后上一帧遗留的图形就会和新图形叠在一起产生残影。第二个常见原因是Timer间隔太小比如8ms一帧绘制耗时超过帧间隔系统开始排队重绘表现出来就是闪。第三个原因是加载了带透明通道的PNG图片直接drawImage时没有做去残影处理。解决paintComponent第一行写super.paintComponent(g)这是Java Swing绘制规范不能省。Timer间隔我建议下限设在16ms不要低于10ms。另外把背景用fillRect直接覆盖整个面板再用repaint触发重绘。如果你还觉得闪就在GamePanel构造器中加上setDoubleBuffered(true)虽然JPanel默认就是双缓冲但显式写出来排除环境差异。做完这三步闪烁基本消失如果还闪就要考虑是不是显卡驱动在特定分辨率下的已知问题换台机器对比定位。5.2 按键不灵敏或按一下飞一半屏现象按一下左箭头飞机直接飞到最左边明明按了空格子弹要等零点几秒才出来。原因前一个现象是系统key repeat导致。操作系统会在你按住键之后自动重复触发keyPressed默认重复间隔约500ms但一旦触发连续事件会让飞机在几帧内连续移动。后者是焦点丢失JPanel没拿到键盘焦点按键事件被窗口的其他组件吃掉。解决用boolean数组记录按键状态在keyPressed和keyReleased里对应true/false在updateGame里根据状态移动。这样既不受重复触发影响也方便实现多键同按。焦点方面在GamePanel构造器里setFocusable(true)并在GameFrame构造器最后写panel.requestFocusInWindow()确保窗口显示后焦点落在画布上。代码如下private boolean leftPressed, rightPressed; addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { if (e.getKeyCode() KeyEvent.VK_LEFT) leftPressed true; if (e.getKeyCode() KeyEvent.VK_RIGHT) rightPressed true; } public void keyReleased(KeyEvent e) { if (e.getKeyCode() KeyEvent.VK_LEFT) leftPressed false; if (e.getKeyCode() KeyEvent.VK_RIGHT) rightPressed false; } }); // updateGame内 if (leftPressed) player.moveLeft(); if (rightPressed) player.moveRight();这段代码比原来的switch更可靠也方便后续加“上、下”移动。参数上player.speed为5时持续按键移动很顺滑如果设成10你会发现很难瞄准敌人这是速度与手感不匹配的典型问题。5.3 子弹穿过敌机或者飞机被空气撞死现象子弹明显穿过敌机身体分数没变或者玩家飞机离敌人还有几个像素生命值却减了。原因碰撞条件不完整。只比较左上角坐标当然命中不了用了Rectangle.contains(x,y)而不是intersects会只拿点去判断无限扩大或缩小碰撞范围。还有一个隐蔽问题是如果敌机速度大于自身高度两帧之间会越过子弹产生“隧穿”子弹从敌人这边穿到那边没有相交帧。解决用矩形相交条件展开判断四个边。隧穿问题在飞机大战里很少发生因为子弹和敌机速度都不高但如果以后做高速弹幕就得用细分步进或射线检测。另一个技巧是把玩家判定框缩小10%在碰撞检测时用player.x5、player.y5、width-10、height-10这样玩家看着接近敌机但还没死手感更宽容。项目源码里我见过大量“空气撞死”案例都是没做判定框收缩。5.4 游戏结束后按什么都没反应只能强退现象生命归零显示“游戏结束”按空格、按回车全无响应只能关闭窗口重新启动。原因没有游戏状态机。游戏结束后updateGame还在刷新但玩家已经不动键盘事件也都走同一个keyPressed方法没有区分“游戏中”和“结束”。更严重的是有些人的Timer还在继续执行导致游戏陷入一个既不能操作又不退出的死循环。解决加一个枚举或布尔量gameOver。在keyPressed里判断gameOver为true且按下空格时调用resetGame。resetGame把score重置为0life重置为3清空enemies和bullets把player坐标拉回初始位置最后把gameOver设回false。注意resetGame里不能只重置变量不重置Sprite列表否则新开局一开始就能看到上一局残留的敌机。这是“后悔药”最该有的功能实现起来也就十行。private void resetGame() { score 0; life 3; enemies.clear(); bullets.clear(); player.x 375; player.y 520; gameOver false; }5.5 关闭窗口后javaw进程还在后台占CPU现象关掉游戏窗口任务管理器里javaw.exe不消失CPU占用一直20%以上。原因十有八九是手写线程。用while(true)循环做游戏刷新在窗口关闭后线程没有退出条件还有一个可能是在某个类里启动了Timer但没stop不过Swing Timer在UI线程销毁时会自动停止因此多半是手写线程。解决优先用javax.swing.Timer别用while(true)。如果确实要用线程就写boolean running在windowClosing事件里把running置为false线程循环自然退出。音效播放也会让进程不退出使用Clip时在窗口关闭回调里clip.stop()和clip.close()。这也是面试里Java进程生命周期的一个高频追问点。以上五个坑我在不同版本的项目里都碰到过。前三个是“逻辑不对”后两个是“生命周期没管好”。排查手段也很有套路闪烁看绘制按键看焦点穿模看碰撞条件卡死看状态退不出看线程。按照这个顺序去排查基本都能在十分钟内定位。6. 进阶玩法用FPS显示和Boss战来验证你的源码能不能扩展当核心游戏已经稳定你手里其实拿到的是一个可以随便改的骨架。我通常用两个方向来验证这份源码的扩展性一是加一个实时FPS显示快速判断性能余量二是加一个Boss敌机检验你的碰撞和绘制逻辑有没有写死。先看FPS显示怎么做。private long lastTime System.nanoTime(); private int fps 0; private int measuredFrames 0; Override protected void paintComponent(Graphics g) { // ... 原有绘制 ... measuredFrames; long now System.nanoTime(); if (now - lastTime 1_000_000_000L) { fps measuredFrames; measuredFrames 0; lastTime now; } g.drawString(FPS: fps, 10, 70); }这个FPS计数器每秒更新一次能清楚看到游戏刷新是否稳定。如果FPS低于50优先检查是不是敌机列表或子弹列表增长太多性能问题通常是内存队列问题而不是绘制问题。另一个方向是Boss战你可以新建Boss类血量300每50帧发射一圈子弹。实现时不需要改动GamePanel的绘制框架只需要在updateGame里给Boss增加一个状态机血量降到50%时变红、加速。这份飞机大战小游戏项目源码把Boss做出来后你的简历上就可以写“实现多类型敌人与Boss战”而不是“写了个小游戏”。具体实现时注意Boss的矩形碰撞框要比视觉尺寸小一圈否则玩家会在离Boss很远的地方被判定撞机这就是手感玄学的一部分其实还是边界数据在作怪。我在最初的项目里没有做FPS显示后来加音效时发现每次播放AudioClip都会造成明显的掉帧才意识到性能检测应该早一点加。从此以后凡是用Swing做动效我都先显示FPS再谈功能。这种做法能让你看到参数改动后的真实反馈比如把Timer从16ms改成30msFPS立刻下降但CPU占用也下降这就是性能和流畅度的取舍。希望这份源码结构和排查思路能帮到你少走几条弯路。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/30 15:34:36

超级具身智能“感知-决策中枢”研究(13):基于FRA架构的增量因子更新与记忆回放网络

前沿技术探索:FRA核心定义与设计理念 FRA(Factorized Robotic Agent, FRA,亦称:因式分解机器人智能体)是一套因式化数实融合一体化新型智能系统,核心设计理念是全链路系统解耦,将机器人作业的感知、推理、动作执行三大核心能力拆解为独立平行子模块,整合为统一、闭环、…

2026/10/30 15:34:36

便宜一半、快三倍:GPT-6.1 Terra 给电脑操作重新定价

便宜一半、快三倍:GPT-6.1 Terra 给电脑操作重新定价原文:OpenAI - 《Introducing GPT-6.1 Terra》(https://openai.com/index/introducing-gpt-6-1-terra/)一、这是一台什么样的「电脑操作模型」 10 月 26 日 OpenAI 发布 GPT-6.…

2026/10/30 16:40:00

RAG答非所问?先查向量检索链路再调模型

1. 先搞清楚问题出在“找”还是“编”做RAG(检索增强生成)项目的人,十有八九都遇到过这种场景:知识库明明有正确答案,模型却一本正经地胡编乱造。我最初做本地知识库问答时也这样,系统答非所问,…

2026/10/30 16:40:00

基于深度学习的多弹时间角度协同制导律设计与验证

简介:一份面向人工智能与制导控制交叉领域学习者的实践项目,聚焦如何利用深度神经网络优化时间角度协同制导律,提升飞行器在时间、角度双重约束下的自主控制能力。资源包共六个文件,包含深度学习源程序、数据生成脚本、已训练好的…

2026/10/30 16:40:00

Excel隔行上色技巧与高级应用详解

1. Excel隔行上色的基础原理与应用场景Excel表格隔行上色(又称斑马纹效果)是数据处理中最基础却最实用的视觉优化技巧。这种看似简单的操作背后,其实融合了条件格式、函数计算和视觉设计的多重逻辑。为什么需要隔行上色?当处理超过…

2026/10/30 16:40:00

图像分类技术:从CNN原理到PyTorch实践

1. 图像分类基础概述图像分类是计算机视觉领域最基础也最重要的任务之一。简单来说,就是让计算机能够识别一张图片中包含了什么物体或场景。比如给定一张猫的照片,计算机能够正确识别出"猫"这个类别。这项技术在日常生活中有着广泛应用&#x…

2026/10/30 16:40:00

SpringBoot+MySQL羽毛球馆预约系统源码:毕业设计完整工程解析

简介:这是一份基于Spring Boot与MySQL的羽毛球馆在线预约系统毕业设计完整源码包,面向计算机相关专业毕业生、课程设计学生以及需要快速搭建同类型预约项目的开发者。系统覆盖用户注册登录、场馆信息浏览与搜索、场地预约与取消、订单管理、在线/线下支付…

2026/10/30 16:34:58

微信小程序+SpringBoot刷题系统毕业源码

简介:本资源是一套完整的微信小程序SpringBoot双端刷题系统毕业设计源码,面向计算机专业本科生、Java与前端初学者及毕设开发者,解决在线学习平台从0到1的全栈开发实践问题。资源包共773个文件,涵盖101个Java后端业务逻辑与实体类…

2026/10/29 17:05:52

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/29 17:06:02

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/29 17:06:13

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/30 0:00:46

NCP1342高频准谐振反激控制器:从设计约束到验收的完整方案

简介:NCP1342 高频准谐振反激控制器应用介绍译文文档,面向电源设计工程师、硬件研发人员及开关电源学习者,帮助读者跨越英文原版资料的语言门槛,系统理解这款高集成度 QR Flyback 控制器的设计要点。文档围绕芯片核心特性展开&…

2026/10/30 0:00:46

NCP1342准谐振反激电源设计:从选型到验收的全流程解析

简介:这份文档面向电源设计工程师与开关电源学习者,系统梳理NCP1342高频准谐振反激控制器的应用要点,帮助读者快速理解这款高集成度离线电源变换器控制芯片的工作机制与设计方法。内容涵盖芯片核心特性、引脚功能定义、电气特性参数表、最大额…

2026/10/30 0:00:46

Web系统自动登出机制设计与实现全解析

1. 系统自动登出机制的设计初衷在各类Web应用和后台管理系统中,自动登出功能早已成为标配。这个看似简单的功能背后,实际上涉及用户安全、资源管理和体验优化的多重考量。作为从业十多年的老开发,我见过太多因为登出逻辑设计不当导致的安全事…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑