ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java潜艇大战游戏源码实战:Swing渲染、线程循环与碰撞检测

Java潜艇大战游戏源码实战:Swing渲染、线程循环与碰撞检测 简介这份Java潜艇大战游戏源码面向具备Java基础、希望进阶游戏开发的编程学习者以经典海战玩法为载体帮助读者理解面向对象设计、GUI编程与实时交互逻辑的落地方式。压缩包共78个文件约401KB包含20个java源文件、36个class编译文件、12个png图像素材以及xml、prefs等工程配置源码与资源分层存放便于对照阅读与二次修改。内容覆盖Swing界面搭建、键盘与鼠标事件监听、多线程刷新游戏逻辑、潜艇与炮弹的对象建模、矩形与AABB碰撞检测、得分与命中判定、音效图像资源加载及文件读写持久化等关键环节并附有Eclipse工程配置可直接导入运行调试。目前已有953人学习下载适合作为课程设计、毕业设计或Java游戏入门练手项目通过分析完整源码结构读者能系统掌握从界面渲染到游戏循环的实现思路积累对象交互与性能优化的实战经验。1. 潜艇大战游戏源码从零跑通一个能玩的 Java 桌面小游戏很多人第一次看到「Java实现潜艇大战游戏源码」这个标题脑子里冒出来的第一个念头是这玩意儿到底能干嘛是那种网上随便一搜就有的课程作业还是真能跑起来、能玩、能改的完整项目我一开始也这么想。后来自己动手把这类项目从编译到运行、从改参数到加功能走了一遍才发现它其实是一个特别适合练手的 Java 桌面游戏骨架——用 Swing 或 JavaFX 做渲染用线程跑游戏主循环用面向对象把潜艇、鱼雷、敌舰拆成独立类。它解决的核心问题是让你在一个有真实交互、有碰撞检测、有状态管理的场景里把 Java 基础、容器、多线程、事件监听这些知识点串起来。适合谁适合刚学完 Java 基础、想找个能看见效果的项目练手的人也适合想拿它当模板改出自己小游戏的人。下面我就按「能跑起来 → 能看懂 → 能改 → 能避坑」的顺序把这个源码的落地路径拆开讲。2. 潜艇大战源码的骨架Swing 渲染、线程循环与碰撞检测怎么搭2.1 为什么这类项目大多选 Swing 而不是 JavaFX你拿到一份「潜艇大战」的 Java 源码打开一看十有八九是javax.swing那一套。原因很实际Swing 是 JDK 自带的不需要额外配依赖JFrameJPanelGraphics就能把画面画出来对新手来说环境成本几乎为零。JavaFX 虽然渲染更现代但从 JDK 11 开始被移出标准库要单独引依赖、配模块很多人卡在环境上就放弃了。所以如果你只是想跑通、看懂、改着玩Swing 是更稳的选择。这类项目的典型结构是这样的一个继承JFrame的主窗口类一个继承JPanel的游戏面板类面板里重写paintComponent(Graphics g)负责画所有东西再用一个Thread或javax.swing.Timer驱动repaint()。游戏里的潜艇、鱼雷、敌舰各自是一个类持有x、y、width、height、speed这些字段面板在每一帧里遍历这些对象更新坐标、检测碰撞、画到屏幕上。提示如果你拿到的源码用的是Timer注意它的回调是在事件调度线程EDT里跑的逻辑太重会卡界面用独立Thread的话更新完坐标后要记得repaint()但别在非 EDT 线程里直接操作 Swing 组件。2.2 游戏主循环的最小可运行代码下面这段是我从这类项目里抽出来的最小骨架去掉美术和音效只保留「窗口 面板 循环 画一个会动的矩形」这条主线。你可以先把它跑起来确认环境没问题再往里填潜艇和鱼雷的逻辑。import javax.swing.*; import java.awt.*; public class SubmarineGame extends JPanel implements Runnable { // 游戏对象的位置和速度先用一个矩形代表潜艇 private int subX 200, subY 500; private int subSpeed 5; private boolean running true; public SubmarineGame() { this.setPreferredSize(new Dimension(800, 600)); this.setBackground(Color.BLACK); this.setFocusable(true); // 让面板能接收键盘事件 } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 画潜艇实际项目里这里会换成 Image 绘制 g.setColor(Color.CYAN); g.fillRect(subX, subY, 60, 20); // 画鱼雷、敌舰的代码会在这里依次调用 } Override public void run() { while (running) { update(); // 更新所有对象坐标 repaint(); // 请求重绘 try { Thread.sleep(16); // 约 60 FPS } catch (InterruptedException e) { e.printStackTrace(); } } } private void update() { // 这里处理键盘输入、移动潜艇、鱼雷飞行、碰撞检测 // 例如subX subSpeed; } public static void main(String[] args) { JFrame frame new JFrame(潜艇大战); SubmarineGame game new SubmarineGame(); frame.add(game); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); new Thread(game).start(); // 启动游戏循环线程 } }这段代码里几个关键点setPreferredSize决定了窗口初始大小Thread.sleep(16)控制帧率repaint()只是「请求」重绘真正画的时候 Swing 会调paintComponent。update()方法是整个游戏逻辑的入口潜艇移动、鱼雷生成、碰撞判断都塞在这里。参数上subSpeed改大改小直接影响手感sleep的毫秒数决定流畅度一般 16 到 33 之间比较合适。2.3 碰撞检测为什么用矩形相交就够了潜艇大战这类 2D 游戏碰撞检测不需要多精确。绝大多数源码用的是Rectangle.intersects()也就是把每个游戏对象抽象成一个矩形判断两个矩形有没有重叠。这样做的好处是代码简单、性能好缺点是视觉上可能「看着没碰到就炸了」或者「明明擦边却没炸」。如果你想让手感更准可以把碰撞矩形调得比图片略小一圈或者对鱼雷用点检测、对潜艇用矩形检测。// 假设 torpedo 和 enemy 都是持有 x, y, width, height 的对象 Rectangle torpedoRect new Rectangle(torpedo.x, torpedo.y, torpedo.width, torpedo.height); Rectangle enemyRect new Rectangle(enemy.x, enemy.y, enemy.width, enemy.height); if (torpedoRect.intersects(enemyRect)) { // 命中处理移除鱼雷、敌舰扣血或爆炸 torpedo.setAlive(false); enemy.setHp(enemy.getHp() - 1); }这里的参数调整很关键width和height如果直接取图片尺寸往往偏大因为图片可能有透明边距。我一般会把碰撞矩形在四个方向各缩 2 到 4 个像素具体缩多少要看素材。这个值没有标准答案调到手感自然就行。3. 把源码跑起来环境配置、编译命令与资源目录的坑3.1 JDK 版本选择和 java 环境变量配置这类潜艇大战源码大部分是在 JDK 8 时代写的用的 API 比较老。你用 JDK 17 或 21 去编译大概率能过但偶尔会遇到javax.swing相关的废弃警告或者源码里用了sun.*内部类导致编译失败。我的建议是先看你拿到的源码有没有module-info.java没有的话就用 JDK 8 或 11 跑最省事。如果你机器上已经装了高版本 JDK也不用卸载用JAVA_HOME切换就行。Windows 上配置环境变量核心就两步把 JDK 的bin目录加到Path再新建一个JAVA_HOME指向 JDK 根目录。配完之后开个新命令行敲java -version和javac -version两个版本号一致才算成功。很多人翻车在「改了 Path 但没关掉旧命令行」旧窗口读的还是旧环境怎么试都不对。# 验证环境两个命令输出的版本号应该一致 java -version javac -version # 编译假设源码都在 src 目录下输出到 out 目录 javac -encoding UTF-8 -d out src/*.java # 运行注意 -cp 指向 out后面跟主类全名 java -cp out SubmarineGame-encoding UTF-8这个参数别省。很多源码里有中文注释或中文字符串Windows 默认编码是 GBK不指定的话编译出来全是乱码游戏里的文字也显示不正常。-d out是把编译后的.class文件统一放到out目录保持源码目录干净。3.2 图片和音效资源加载失败怎么排查潜艇大战源码通常带一个images或res目录里面放潜艇、鱼雷、爆炸的 PNG可能还有.wav音效。跑起来黑屏、报NullPointerException九成是资源没加载到。Swing 里加载图片常见两种写法一种是new ImageIcon(images/sub.png)这种是相对路径相对于你「启动 java 命令时所在的目录」不是相对于 class 文件另一种是getClass().getResource(/images/sub.png)这种是从 classpath 里找更稳。// 方式一相对文件路径容易受启动目录影响 ImageIcon icon1 new ImageIcon(images/sub.png); // 方式二从 classpath 加载推荐 ImageIcon icon2 new ImageIcon(getClass().getResource(/images/sub.png));如果你用方式二编译的时候要把images目录一起复制到out下面或者把out和images的相对位置保持和源码里写的一致。音效那边Java 自带的是javax.sound.sampled只支持.wav和.au你拿.mp3直接放会报UnsupportedAudioFileException。要么转成 wav要么引第三方库但为了跑通源码转格式最快。注意资源路径里的斜杠方向、大小写在 Windows 上可能不敏感但换到 Linux 或打包成 jar 之后就会暴露问题。写/images/sub.png比写images\\sub.png更保险。3.3 用 Maven 或 Gradle 管理依赖的取舍如果你拿到的源码是「裸」的.java文件没有pom.xml也没有build.gradle那直接用javac编译最直接。但如果你想长期改、加功能、引第三方库比如换更好的音效库建议自己补一个pom.xml把源码放进src/main/java资源放进src/main/resources。这样mvn package能直接打出可执行 jar资源路径问题也一并解决了。project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdsubmarine-game/artifactId version1.0/version properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties /project这个pom.xml里没写依赖因为纯 Swing 项目不需要额外依赖。source和target设成 8是为了兼容老源码。如果你确定源码用了新语法再往上调。Maven 的好处是编译、打包、资源复制一条龙坏处是第一次配可能卡在下载插件上。取舍看你只想跑一次用javac想持续改上 Maven。4. 改源码潜艇移动、鱼雷发射与敌舰生成的参数怎么调4.1 键盘监听让潜艇动起来原始源码里潜艇可能只能左右移动或者移动很生硬。要让操作跟手核心是「按下键时记录状态循环里根据状态更新坐标」而不是在keyPressed里直接改坐标。前者叫状态驱动后者叫事件驱动游戏里前者更稳。// 在面板类里加一个 Set 记录当前按下的键 private SetInteger keys new HashSet(); // 注册键盘监听 this.addKeyListener(new KeyAdapter() { Override public void keyPressed(KeyEvent e) { keys.add(e.getKeyCode()); } Override public void keyReleased(KeyEvent e) { keys.remove(e.getKeyCode()); } }); // 在 update() 里根据按键状态移动 private void update() { if (keys.contains(KeyEvent.VK_LEFT)) { subX - subSpeed; } if (keys.contains(KeyEvent.VK_RIGHT)) { subX subSpeed; } // 边界限制别让潜艇跑出屏幕 subX Math.max(0, Math.min(subX, getWidth() - 60)); }subSpeed这个参数直接决定手感。设成 3 到 5 比较跟手设成 10 以上会感觉「滑」设成 1 又太肉。边界限制那行别漏不然潜艇跑出屏幕就找不回来了。getWidth()是面板当前宽度窗口拉伸后它会变所以用getWidth()而不是写死 800。4.2 鱼雷发射的冷却时间和速度参数鱼雷不能按住就无限发得有冷却。常见做法是给每个鱼雷一个alive标记发射时检查「当前活着的鱼雷数量」或者「距离上次发射过了多少毫秒」。private long lastFireTime 0; private static final long FIRE_COOLDOWN 300; // 毫秒 private void fire() { long now System.currentTimeMillis(); if (now - lastFireTime FIRE_COOLDOWN) { return; // 冷却中不发射 } lastFireTime now; Torpedo t new Torpedo(subX 25, subY - 10, 0, -8); torpedoes.add(t); }FIRE_COOLDOWN设 300 毫秒大概每秒能发 3 发节奏比较舒服。设成 100 会变成弹幕游戏设成 1000 又太慢。鱼雷速度-8表示每帧向上移动 8 像素配合 60 FPS一秒能飞 480 像素从屏幕底部到顶部大概一秒多这个速度感比较合适。你可以根据屏幕高度调屏幕高就调快屏幕矮就调慢。4.3 敌舰生成频率和难度曲线敌舰生成如果写死「每 60 帧生成一个」玩久了会腻。稍微改一下用随机间隔加难度递增体验会好很多。private int spawnCounter 0; private int spawnInterval 90; // 初始每 90 帧生成一个 private void updateEnemies() { spawnCounter; if (spawnCounter spawnInterval) { spawnCounter 0; enemies.add(new Enemy(randomX(), -50, 0, 2)); // 每生成 5 个间隔缩短一点最低到 30 帧 if (spawnInterval 30) { spawnInterval - 5; } } }spawnInterval从 90 降到 30意味着生成频率从每秒 0.67 个升到每秒 2 个难度是渐进上去的。randomX()要保证敌舰生成在屏幕宽度范围内别生成到屏幕外。敌舰速度2是每帧下移 2 像素比鱼雷慢给玩家反应时间。这些参数没有绝对最优你改完自己玩两把觉得难就调大间隔觉得简单就调小。5. 潜艇大战源码避坑编译报错、画面闪烁与线程卡死的排查5.1 编译报错「找不到符号」或「程序包不存在」现象javac一跑满屏cannot find symbol或者package xxx does not exist。原因源码里引了第三方库比如 JSON 解析、日志库但你没把对应的 jar 放进 classpath或者源码分了多个包你编译时没指定源码路径。解决先看import语句里有没有非java.*、非javax.*的包。有的话找到对应 jar编译和运行时都用-cp带上。如果是多包结构用javac -d out src/com/example/*.java这种按目录编译或者直接上 Maven。5.2 画面闪烁严重潜艇和敌舰一直在抖现象游戏能跑但画面一闪一闪物体边缘有撕裂感。原因Swing 默认是「先清空背景再画」如果清空和绘制之间有时间差就会闪。另外在非 EDT 线程里直接repaint()也可能加剧这个问题。解决在面板构造里开双缓冲setDoubleBuffered(true)。如果还闪把paintComponent里的super.paintComponent(g)保留但确保所有绘制都在这个方法里完成不要在update()里直接画。双缓冲是 Swing 自带的不用引额外库。public SubmarineGame() { this.setDoubleBuffered(true); // 开启双缓冲减少闪烁 // ... 其他初始化 }5.3 游戏跑一会儿就卡死界面无响应现象玩了几十秒画面不动了点关闭也没反应。原因多半是游戏循环线程里抛了异常但没打印线程悄悄死了或者while(running)里的sleep被中断后没处理循环空转吃满 CPU。解决在run()的while里加try-catch包住整个循环体捕获Exception并打印堆栈。InterruptedException单独处理要么break退出循环要么重新设置中断标志。另外检查update()里有没有对空对象调方法比如鱼雷已经移除了但还在遍历。Override public void run() { while (running) { try { update(); repaint(); Thread.sleep(16); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 break; } catch (Exception e) { e.printStackTrace(); // 别让异常悄悄吃掉线程 } } }5.4 打包成 jar 后图片全丢现象在 IDE 里跑得好好的java -jar game.jar一运行图片全没了报NullPointerException。原因jar 里的资源不能用new File(images/sub.png)这种方式读因为 jar 是一个压缩包文件不在文件系统里。解决统一用getClass().getResourceAsStream(/images/sub.png)读成流再交给ImageIO.read()。Maven 项目把images放在src/main/resources下打包时会自动进 jar。// 从 jar 内读取图片的正确姿势 InputStream is getClass().getResourceAsStream(/images/sub.png); BufferedImage img ImageIO.read(is);5.5 中文显示成方块或乱码现象游戏里的「开始」「得分」等中文变成方块。原因编译时没指定-encoding UTF-8或者源码文件本身存的是 GBK 但按 UTF-8 读了。解决编译加-encoding UTF-8同时确认源码文件编码。如果源码是 GBK要么转成 UTF-8要么编译时用-encoding GBK。字体方面Swing 默认字体对中文支持一般可以显式设成new Font(宋体, Font.BOLD, 20)或Microsoft YaHei。6. 从能跑到能改给潜艇大战加一个得分系统和关卡递进把源码跑通、看懂之后最有价值的动作是加一个它原本可能没有的功能验证你真的理解了这套结构。我一般会先加得分系统因为它牵涉到「碰撞检测 → 状态变更 → 界面更新」这条完整链路改完这一条整个项目的脉络就清楚了。具体做法在面板类里加一个private int score 0;在鱼雷命中敌舰的那个if里score 10;然后在paintComponent里用g.drawString(得分: score, 20, 30)画出来。就这么三处改动你就能看到分数实时变化。如果分数不更新说明碰撞检测没进if回去检查矩形坐标和intersects的参数。// 命中时加分 if (torpedoRect.intersects(enemyRect)) { torpedo.setAlive(false); enemy.setAlive(false); score 10; // 加分 } // 绘制时显示 g.setColor(Color.WHITE); g.setFont(new Font(Microsoft YaHei, Font.BOLD, 18)); g.drawString(得分: score, 20, 30);得分有了下一步是关卡递进。最简单的做法每得 100 分spawnInterval减 10同时敌舰速度加 0.5。这样玩家能感觉到「越玩越难」而不是一成不变。再进一步可以加一个level变量每关背景色或敌舰图片换一下视觉上也有反馈。参数初始值每关变化作用spawnInterval90 帧-10 帧敌舰生成变快enemySpeed2 像素/帧0.5敌舰下移变快torpedoSpeed-8 像素/帧不变保持玩家火力scorePerHit10 分不变得分基准这张表里的值是我自己调过觉得顺手的你可以直接抄也可以按自己手感改。关键是理解每个参数影响什么spawnInterval影响压力enemySpeed影响反应时间torpedoSpeed影响打击感。改完自己玩三把如果第三把就手忙脚乱说明难度曲线太陡把-10改成-5。最后说一个我踩过的坑加得分系统时我一开始把score定义成了static结果重新开始游戏时分数不清零怎么调都不对。后来改成实例变量在restart()方法里手动score 0才正常。游戏状态这种东西能不用static就别用不然多开一局就翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表