简介本资源是一套完整的Java潜艇大战游戏源码实现面向Java初学者与GUI编程进阶学习者旨在通过经典小游戏项目掌握面向对象设计、Swing图形界面开发、事件驱动交互及基础游戏逻辑构建等核心能力。压缩包共78个文件含20个可读性良好的Java源文件涵盖Submarine、Torpedo、GamePanel等关键类、36个编译后class文件、12张游戏素材PNG图像如战舰、鱼雷、爆炸效果等以及4个XML配置文件和IDE相关工程文件整体体积仅401KB轻量易导入运行。已有953人下载学习适合用于课程设计、Java实训或自学项目实践。读者可直接编译运行深入理解潜艇移动控制、碰撞检测算法、多线程游戏循环、资源加载与得分系统等完整实现细节并基于清晰的包结构com.SubmarineWar快速定位与扩展功能。1. 为什么用 Java 写潜艇大战不是“练手玩具”而是面向对象落地的硬核沙盒你打开 GitHub 搜 “Java 游戏源码”90% 是贪吃蛇、打砖块、俄罗斯方块——它们教你怎么画矩形、怎么监听键盘但一碰“多状态实体协同”就崩潜艇要潜航/上浮/发射鱼雷/被击中/爆炸动画鱼雷要追踪/碰撞检测/命中反馈敌舰要巡逻/变向/发射声呐水下背景要随深度渐变、气泡要按物理规律上升……这些不是 if-else 堆出来的是类与类之间靠继承、组合、观察者模式、状态模式咬合运转的。《潜艇大战》恰恰卡在这个临界点它足够小单机、无网络、无资源管理器能三天跑通又足够真实体生命周期完整、事件驱动清晰、帧率敏感逼你把 Java 的封装边界划准、把update()和render()分开、把GameEntity抽成抽象基类、把CollisionSystem独立成服务类——而不是写完发现所有逻辑挤在MainPanel.paintComponent()里改个鱼雷速度得翻 200 行。这项目适合两类人一是刚学完 Java 基础、正卡在“知道语法但不会组织代码”的人它用潜艇的沉浮告诉你什么叫“状态即数据行为”二是准备 Java 面试题的开发者Swing双缓冲防闪烁、Timer精确帧控、BufferedImage预加载图层、KeyListener与KeyBinding的取舍——全是高频考点且全在真实交互中暴露问题。别把它当怀旧小游戏它是你把“面向对象编程 Java”从课本术语变成肌肉记忆的压舱石。2. 从零搭起游戏骨架用 Swing 构建可扩展的主循环与实体系统2.1 主窗口与双缓冲渲染为什么不用repaint()而要自己管Graphics2DJFrame只是容器真正决定画面是否撕裂、动画是否卡顿的是渲染方式。repaint()触发 AWT 的被动重绘线程不可控帧率飘忽而潜艇大战要求稳定 60FPS即每 16ms 更新一次必须主动控制渲染时机。我们用CanvasBufferStrategy实现双缓冲public class GameCanvas extends Canvas { private BufferStrategy strategy; private Graphics2D g2d; public void initBuffer() { createBufferStrategy(2); // 双缓冲前台显示/后台绘制 strategy getBufferStrategy(); // 启用抗锯齿和高质量渲染 g2d (Graphics2D) strategy.getDrawGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } public void render() { if (strategy null) return; do { try { g2d (Graphics2D) strategy.getDrawGraphics(); // 清屏用深蓝渐变模拟水深 GradientPaint waterGradient new GradientPaint(0, 0, new Color(10, 30, 80), 0, getHeight(), new Color(5, 15, 40)); g2d.setPaint(waterGradient); g2d.fillRect(0, 0, getWidth(), getHeight()); // 绘制所有实体顺序很重要背景→潜艇→鱼雷→爆炸→UI for (GameEntity entity : gameWorld.getEntities()) { entity.render(g2d); } // 绘制分数、血条等UI drawUI(g2d); } finally { g2d.dispose(); } strategy.show(); // 交换缓冲区后台变前台 } while (strategy.contentsRestored()); // 处理缓冲区丢失 } }关键参数说明createBufferStrategy(2)中的2表示双缓冲比单缓冲1减少闪烁比三缓冲3更省内存strategy.contentsRestored()是必须的兜底检查——当窗口最小化再恢复时缓冲区可能失效不加这句会导致黑屏。2.2 游戏主循环用ScheduledExecutorService替代Timer的血泪经验早期教程常用javax.swing.Timer但它在高负载时会丢帧Timer的scheduleAtFixedRate在任务超时时会跳过执行导致潜艇突然瞬移、鱼雷轨迹断层。我们改用ScheduledExecutorService并手动控制帧间隔public class GameLoop { private final ScheduledExecutorService executor Executors.newSingleThreadScheduledExecutor(); private static final long TARGET_FRAME_TIME_MS 16; // ≈60 FPS private volatile long lastFrameTime System.nanoTime(); public void start() { executor.scheduleAtFixedRate(this::tick, 0, TARGET_FRAME_TIME_MS, TimeUnit.MILLISECONDS); } private void tick() { long now System.nanoTime(); long elapsedMs (now - lastFrameTime) / 1_000_000; lastFrameTime now; // 实际帧率补偿如果上一帧耗时 16ms则跳过本次渲染只更新逻辑 if (elapsedMs TARGET_FRAME_TIME_MS * 1.5) { gameWorld.update(elapsedMs); // 用实际耗时做物理积分 return; } gameWorld.update(TARGET_FRAME_TIME_MS); // 固定步长更新逻辑 canvas.render(); // 渲染 } }为什么固定步长更稳潜艇上浮速度 velocity * deltaTime若deltaTime波动大如 10ms→25ms潜艇会忽快忽慢。固定16ms步长后所有运动计算可预测配合插值下一节讲就能平滑。2.3 实体基类设计GameEntity如何承载潜艇、鱼雷、敌舰的共性所有可动对象潜艇、鱼雷、敌舰、气泡都继承GameEntity它定义了游戏世界的最小契约public abstract class GameEntity { protected double x, y; // 物理坐标像素 protected double vx, vy; // 速度像素/秒 protected double width, height; // 碰撞箱尺寸 protected boolean alive true; // 生存状态用于批量回收 protected Rectangle2D.Double boundingBox; // 缓存的碰撞箱避免每次 new public GameEntity(double x, double y, double width, double height) { this.x x; this.y y; this.width width; this.height height; this.boundingBox new Rectangle2D.Double(x, y, width, height); } // 子类必须实现每帧调用更新位置、状态、AI public abstract void update(double deltaTimeMs); // 子类必须实现每帧调用绘制自身 public abstract void render(Graphics2D g2d); // 碰撞检测通用方法返回是否与另一实体相交 public boolean collidesWith(GameEntity other) { updateBoundingBox(); // 位置变化后刷新碰撞箱 return boundingBox.intersects(other.getBoundingBox()); } protected void updateBoundingBox() { boundingBox.setRect(x, y, width, height); } public Rectangle2D.Double getBoundingBox() { updateBoundingBox(); return boundingBox; } public void kill() { alive false; } public boolean isAlive() { return alive; } }设计意图alive标志位让GameWorld能在update()后统一清理死亡实体避免ArrayList.remove()导致的并发修改异常boundingBox缓存减少Rectangle2D对象创建GC 压力大时帧率暴跌collidesWith()提供默认矩形检测子类可重写为圆形或像素级检测。3. 潜艇核心行为状态机驱动的潜航、攻击与生存逻辑3.1 潜艇状态机用枚举 策略模式解耦“上浮/下潜/静止/爆炸”潜艇不是简单地y vy它的垂直运动受状态约束静止状态可转向、可加速但vy0下潜状态引擎推力向下vy为负但受最大下潜速度限制上浮状态引擎推力向上vy为正但需模拟浮力衰减爆炸状态禁用所有输入播放粒子动画alivefalse后销毁。用枚举定义状态每个状态封装自己的update()和handleInput()public enum SubmarineState { IDLE { Override public void handleInput(Submarine sub, InputState input) { if (input.isDownPressed()) sub.setState(DIVING); if (input.isUpPressed()) sub.setState(RISING); if (input.isFirePressed()) sub.fireTorpedo(); } }, DIVING { Override public void update(Submarine sub, double deltaTimeMs) { sub.vy Math.min(sub.vy - 0.8, -sub.maxDiveSpeed); // 加速度向下 } }, RISING { Override public void update(Submarine sub, double deltaTimeMs) { sub.vy Math.max(sub.vy 0.5, 0); // 浮力衰减越接近水面越慢 if (sub.y 50) { // 到达水面 sub.y 50; sub.vy 0; sub.setState(IDLE); } } }, EXPLODING { Override public void update(Submarine sub, double deltaTimeMs) { sub.explosionTimer - deltaTimeMs; if (sub.explosionTimer 0) { sub.kill(); } } }; public void update(Submarine sub, double deltaTimeMs) {} public void handleInput(Submarine sub, InputState input) {} }为什么不用 if-else 嵌套状态切换逻辑如“下潜中按上浮键→切到上浮状态”集中在setState()方法里update()和handleInput()由枚举实例接管新增状态如“声呐扫描”只需加枚举项不改潜艇主类。3.2 鱼雷发射与追踪贝塞尔曲线模拟真实鱼雷航迹真实鱼雷不会直线飞向目标——它有初速、有转向延迟、有最大转弯半径。我们用二次贝塞尔曲线模拟起点潜艇炮口、控制点目标前方偏移量、终点目标当前位置。这样鱼雷会“绕弯”逼近而非瞬移public class Torpedo extends GameEntity { private final Point2D.Double target; // 锁定的目标敌舰中心 private final double maxTurnRadius 30; // 米像素 private final double speed 120; // 像素/秒 private final Path2D.Double trajectory new Path2D.Double(); public Torpedo(double startX, double startY, Point2D.Double target) { super(startX, startY, 8, 4); this.target target; // 初始化贝塞尔曲线起点→控制点→终点 trajectory.moveTo(x, y); // 控制点目标前方 50px横向偏移 20px 模拟转向惯性 double ctrlX target.x 50 * Math.cos(Math.atan2(target.y - y, target.x - x)) 20; double ctrlY target.y 50 * Math.sin(Math.atan2(target.y - y, target.x - x)); trajectory.quadTo(ctrlX, ctrlY, target.x, target.y); } Override public void update(double deltaTimeMs) { // 沿贝塞尔曲线匀速采样用 De Casteljau 算法 double t Math.min(1.0, progress / totalDuration); Point2D.Double pos bezierPoint(trajectory, t); x pos.x; y pos.y; progress deltaTimeMs * speed / totalDuration; // 进度归一化 // 检测是否到达终点或超出范围 if (t 0.99 || !isInBounds()) { kill(); } } private Point2D.Double bezierPoint(Path2D.Double path, double t) { // 简化对二次贝塞尔 P(t) (1-t)²P₀ 2t(1-t)P₁ t²P₂ // 这里 P₀(x,y), P₁(ctrlX,ctrlY), P₂(target.x,target.y) // 实际代码需解析 Path2D 获取三点坐标 return new Point2D.Double(x (target.x - x) * t, y (target.y - y) * t); // 简化版线性插值 } }参数意义maxTurnRadius控制鱼雷灵活性值越小越“灵敏”但易被敌舰规避speed影响战斗节奏120px/s 对应 60FPS 下每帧移动 2px肉眼可见流畅quadTo()生成的曲线在render()中用g2d.draw(trajectory)可视化调试。3.3 敌舰 AI有限状态机FSM实现巡逻、追击、逃跑三态切换敌舰不能傻追——它要感知潜艇距离、判断威胁等级、动态切换行为public enum EnemyState { PATROL { // 沿预设路径往返 Override public void update(EnemyShip enemy, double deltaTimeMs) { enemy.moveAlongPath(deltaTimeMs); if (enemy.isPlayerInRange(300)) { // 300px 内进入警戒 enemy.setState(CHASE); } } }, CHASE { // 直线追击但带 0.3s 延迟模拟反应时间 Override public void update(EnemyShip enemy, double deltaTimeMs) { if (enemy.lastSightTime 0) enemy.lastSightTime System.currentTimeMillis(); if (System.currentTimeMillis() - enemy.lastSightTime 300) { enemy.setState(RETREAT); // 失踪 300ms 切逃跑 return; } // 向潜艇位置匀速移动 double dx enemy.playerX - enemy.x; double dy enemy.playerY - enemy.y; double dist Math.sqrt(dx*dx dy*dy); if (dist 10) { enemy.vx (dx / dist) * enemy.speed; enemy.vy (dy / dist) * enemy.speed; } } }, RETREAT { // 向远离潜艇的方向逃逸 Override public void update(EnemyShip enemy, double deltaTimeMs) { double dx enemy.x - enemy.playerX; double dy enemy.y - enemy.playerY; double dist Math.sqrt(dx*dx dy*dy); if (dist 10) { enemy.vx (dx / dist) * enemy.speed * 1.2; // 比巡逻快20% enemy.vy (dy / dist) * enemy.speed * 1.2; } } }; public void update(EnemyShip enemy, double deltaTimeMs) {} }关键设计lastSightTime记录最后看到潜艇的时间避免敌舰在潜艇隐身如贴海底时无限原地转圈isPlayerInRange()用平方距离比较dx*dx dy*dy 300*300省去开方运算每帧省 1μs积少成多。4. 避坑指南Swing 游戏开发中 5 个让你凌晨三点还在 debug 的真实陷阱4.1 现象潜艇按键响应延迟 200ms松开后还继续移动原因KeyListener在JFrame上注册时焦点未落在可接收键盘事件的组件上。JFrame默认不获取焦点KeyListener无效实际事件被Canvas或JPanel拦截但未正确传递。解决在GameCanvas构造函数末尾加setFocusable(true); requestFocusInWindow();改用KeyBinding更可靠InputMap inputMap canvas.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap canvas.getActionMap(); inputMap.put(KeyStroke.getKeyStroke(UP), moveUp); actionMap.put(moveUp, new AbstractAction() { Override public void actionPerformed(ActionEvent e) { submarine.handleUpPress(); } });4.2 现象鱼雷发射后屏幕出现多个残影像拖尾特效原因Graphics2D绘制时未清除上一帧的鱼雷图像双缓冲的后台缓冲区未完全清空或render()中g2d.fillRect()覆盖不全如窗口缩放后getWidth()/getHeight()返回旧尺寸。解决在render()开头强制用g2d.clearRect(0, 0, getWidth(), getHeight())全屏清空比渐变填充更快在GameCanvas的componentResized()事件中重新初始化BufferStrategyOverride public void componentResized(ComponentEvent e) { if (strategy ! null) { strategy.dispose(); createBufferStrategy(2); strategy getBufferStrategy(); } }4.3 现象敌舰巡逻路径在窗口最大化后错位跑到屏幕外原因巡逻路径坐标用绝对像素值如new Point(100, 200)硬编码未适配窗口尺寸变化。当用户调整窗口大小路径点仍按原始分辨率计算。解决将路径定义为相对坐标0.0~1.0运行时按当前宽高缩放public class PatrolPath { private final ListPoint2D.Double rawPoints Arrays.asList( new Point2D.Double(0.1, 0.3), // 左侧10%高度30% new Point2D.Double(0.9, 0.7), new Point2D.Double(0.1, 0.7) ); public Point2D.Double getPointAt(int index, int width, int height) { Point2D.Double p rawPoints.get(index); return new Point2D.Double(p.x * width, p.y * height); } }4.4 现象游戏运行 10 分钟后明显卡顿CPU 占用 95%原因ArrayListGameEntity中死亡实体未及时移除update()循环遍历数千个alivefalse的对象collidesWith()频繁调用new Rectangle2D.Double()创建临时对象触发频繁 GC。解决GameWorld.update()末尾批量清理entities.removeIf(entity - !entity.isAlive()); // Java 8 一行解决GameEntity中复用boundingBox已实现且collidesWith()不 new 对象用ArrayDeque替代ArrayList存储待渲染实体避免扩容。4.5 现象潜艇爆炸动画只播一帧立刻消失原因爆炸动画用BufferedImage数组逐帧绘制但render()中未按时间戳切换帧而是每帧都画第一帧或Explosion实体alivefalse后被立即回收来不及播完。解决Explosion类维护currentFrame和frameDurationMspublic class Explosion extends GameEntity { private final BufferedImage[] frames; private int currentFrame 0; private long startTime; private static final long FRAME_DURATION_MS 80; // 每帧80ms public Explosion(double x, double y, BufferedImage[] frames) { super(x, y, 64, 64); this.frames frames; this.startTime System.currentTimeMillis(); } Override public void update(double deltaTimeMs) { long elapsed System.currentTimeMillis() - startTime; currentFrame (int) (elapsed / FRAME_DURATION_MS); if (currentFrame frames.length) { kill(); // 播完才销毁 } } }5. 进阶技巧用 JUnit 5 Mockito 为潜艇逻辑写可验证的单元测试5.1 为什么游戏逻辑必须可测试一个真实翻车案例去年我帮团队重构一款军事模拟游戏潜艇的“声呐探测范围”逻辑写在Submarine.update()里// 错误示范逻辑与渲染、输入混在一起 if (input.isSonarActive()) { for (EnemyShip e : world.getEnemies()) { double dist distance(e, this); if (dist 150 e.isSubmerged()) { // 150px 探测半径 e.setDetected(true); } } }结果 QA 发现当潜艇在 149px 距离时敌舰有时被探测、有时不被探测。排查 3 天发现是distance()计算用了Math.sqrt()浮点误差导致dist 150结果不稳定。如果这个逻辑单独抽成boolean canDetect(EnemyShip enemy)并用 JUnit 测试10 分钟就能定位。5.2 测试潜艇运动状态机验证状态切换的确定性用Test验证Submarine.setState()是否按预期改变行为class SubmarineTest { private Submarine submarine; private InputState input; BeforeEach void setUp() { submarine new Submarine(100, 100); input new InputState(); // 模拟输入状态 } Test void shouldSwitchToDivingWhenDownPressed() { // 给定潜艇在 IDLE 状态 assertEquals(SubmarineState.IDLE, submarine.getState()); // 当按下下潜键 input.setDownPressed(true); // 执行处理输入 submarine.handleInput(input); // 验证状态变为 DIVING assertEquals(SubmarineState.DIVING, submarine.getState()); } Test void shouldStopDivingWhenSurfaceReached() { // 给定潜艇在 DIVING 状态y400深水区 submarine.setState(SubmarineState.DIVING); submarine.y 400; submarine.vy -50; // 快速下潜 // 执行更新 10 帧模拟下潜过程 for (int i 0; i 10; i) { submarine.update(16); // 16ms/帧 } // 验证y 应小于 50水面vy0状态回 IDLE assertTrue(submarine.y 50); assertEquals(0, submarine.vy, 0.1); assertEquals(SubmarineState.IDLE, submarine.getState()); } }测试价值shouldStopDivingWhenSurfaceReached用真实物理参数vy-50,10*16ms验证边界条件比人工点击更可靠assertEquals(..., 0.1)允许浮点误差避免测试脆弱。5.3 测试碰撞系统用 Mock 隔离外部依赖GameEntity.collidesWith()依赖Rectangle2D.intersects()但我们要测的是逻辑不是 Java2D 库。用 Mockito Mock 敌舰验证潜艇是否正确标记碰撞Test void shouldMarkCollisionWhenBoundingBoxesIntersect() { // 给定潜艇在 (100,100)尺寸 40x20 Submarine sub new Submarine(100, 100); sub.width 40; sub.height 20; // Mock 一个敌舰让它返回预设的 boundingBox EnemyShip enemy mock(EnemyShip.class); Rectangle2D.Double mockBox new Rectangle2D.Double(120, 110, 30, 25); // 与潜艇重叠 when(enemy.getBoundingBox()).thenReturn(mockBox); // 执行检测碰撞 boolean result sub.collidesWith(enemy); // 验证返回 true assertTrue(result); } Test void shouldNotMarkCollisionWhenNoOverlap() { Submarine sub new Submarine(100, 100); sub.width 40; sub.height 20; EnemyShip enemy mock(EnemyShip.class); Rectangle2D.Double mockBox new Rectangle2D.Double(200, 200, 30, 25); // 完全分离 when(enemy.getBoundingBox()).thenReturn(mockBox); assertFalse(sub.collidesWith(enemy)); }为什么用 Mock避免测试依赖真实EnemyShip的构造逻辑如路径加载、AI 初始化聚焦在collidesWith()的布尔逻辑when().thenReturn()精确控制输入覆盖边界场景刚好相切、一角重叠。5.4 测试数据驱动用ParameterizedTest覆盖所有鱼雷发射角度鱼雷发射方向由潜艇朝向决定需验证不同角度下Torpedo的初始速度分量是否正确ParameterizedTest CsvSource({ 0, 120, 0, // 朝右vx120, vy0 90, 0, -120, // 朝上vx0, vy-120y轴向上为负 180, -120, 0, // 朝左vx-120, vy0 270, 0, 120 // 朝下vx0, vy120 }) void shouldCalculateTorpedoVelocityCorrectly(int angleDegrees, double expectedVx, double expectedVy) { // 给定潜艇朝向 angleDegrees Submarine sub new Submarine(100, 100); sub.setDirection(angleDegrees); // 执行发射鱼雷 Torpedo torpedo sub.fireTorpedo(); // 验证速度分量匹配 assertEquals(expectedVx, torpedo.vx, 0.1); assertEquals(expectedVy, torpedo.vy, 0.1); }参数表意义CsvSource用表格驱动测试4 组数据覆盖 4 个正交方向0.1的 delta 容忍浮点计算误差这种测试比写 4 个独立Test更易维护新增角度只需加一行 CSV。我坚持给每个GameEntity子类写测试不是为了覆盖率数字好看——是当需求变更比如“鱼雷现在要受水流影响”我能mvn test一键确认没破坏原有逻辑。当年重构潜艇 AI 时正是靠这套测试3 天内安全上线了新声呐算法没让 QA 报一个回归 bug。希望帮到你。本文还有配套的精品资源点击获取