☰
Java Swing潜艇大战:可调试可扩展的2D游戏工程实践
2026/10/9 9:08:58 网站建设 项目流程

简介:这是一份面向Java初学者与GUI编程实践者的潜艇大战游戏完整源码项目,聚焦面向对象设计、Swing图形界面开发及游戏逻辑实现,帮助开发者通过实战掌握事件处理、多线程控制、碰撞检测与资源管理等核心技能。压缩包共78个文件,含20个Java源文件(涵盖Submarine、Torpedo、Ocean等核心类)、36个编译后class文件、12张PNG游戏素材(如战舰、鱼雷、爆炸效果等UI资源),以及XML配置、IDEA工程配置文件(iml、project、classpath)等,整体401KB,结构规范,便于导入IDE直接运行调试。已有953人学习下载,项目目录层次清晰,src与bin分离,imgs资源集中管理,附带完整可执行逻辑——从主界面启动、键盘操控潜艇移动发射,到实时得分更新与碰撞判定,所有功能模块均开源可溯,是理解Java桌面游戏开发全流程的优质教学案例。

1. 用 Java 写一个能跑起来、能对战、能改代码的潜艇大战游戏:不是玩具 Demo,是可调试、可扩展、可面试讲清楚的完整工程

你在网上搜“Java 潜艇大战游戏源码”,大概率会撞上两类东西:一类是十年前某高校课程设计压缩包,解压后只有SubmarineGame.java一个文件,main方法里塞了 800 行 Swing 绘图逻辑,连个类都没分;另一类是 GitHub 上 Star 数过百的“开源项目”,点进去发现 README 只有“运行需 JDK8+”,src目录下com.example.game包里全是GamePanel.java、Bullet.java、EnemySub.java这种命名,但没注释、没单元测试、没资源加载说明,ImageIO.read()一报NullPointerException就卡死——这根本不是源码,是黑匣子。
真正值得花时间啃的 Java 潜艇大战,得满足三个硬指标:第一,能本地一键启动(不依赖 IDE);第二,核心逻辑清晰分层(输入/渲染/物理/状态机分离);第三,留了明确的扩展钩子(比如换 AI 策略、加新关卡、接网络对战)。它不是让你抄完交作业就扔的玩具,而是你能在简历里写“独立实现基于 Swing 的双人实时对抗游戏,支持碰撞检测、血量系统、音效反馈与关卡进度持久化”的技术凭证。本文就带你从零搭起这个版本:不用 JavaFX,不碰 Java Web,纯 JDK 自带 API + 标准 Swing + 资源文件规范管理,所有代码可复制、可调试、可讲清每行在干什么。


2. 用标准 Swing 构建游戏主循环:为什么不用 GameLoop 库,而用Timer+repaint()手动控帧

2.1 游戏主窗口与线程模型:Swing 的 EDT 限制就是你的安全边界

Java 潜艇大战这类 2D 实时游戏,最常翻车的不是逻辑写错,而是线程乱套。新手常犯的错误是:在KeyListener里直接开新线程调moveSub(),结果Graphics2D对象被多线程并发调用,画面撕裂、NullPointerException频发。正确做法是把所有 UI 更新锁死在 Event Dispatch Thread(EDT)内,用javax.swing.Timer做主循环驱动器——它天然运行在 EDT 上,actionPerformed回调里调repaint()安全,更新游戏状态也安全。

// GameFrame.java public class GameFrame extends JFrame { private final GamePanel gamePanel; private final Timer gameTimer; public GameFrame() { this.gamePanel = new GamePanel(); this.gameTimer = new Timer(16, e -> { // 16ms ≈ 60FPS gamePanel.update(); // 更新游戏逻辑(位置、碰撞、状态) gamePanel.repaint(); // 触发 paintComponent() }); initUI(); } private void initUI() { setTitle("Java 潜艇大战"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); add(gamePanel); pack(); setLocationRelativeTo(null); gameTimer.start(); // 启动主循环 } }

提示:Timer的 delay 设为16是经验参数,对应理论 60 FPS。实际帧率由update()+paintComponent()总耗时决定。若单帧超 16ms,Timer会自动跳过未执行的 tick(避免 backlog),这是 Swing 游戏循环的默认节流机制,比手动Thread.sleep()更稳。

2.2 游戏面板的双缓冲绘制:解决闪烁问题的唯一正解

Swing 默认绘制是直接画到屏幕,repaint()会触发paint()→update()→paintComponents()链,中间经历多次重绘,潜艇移动时会出现明显拖影。解决方案是启用双缓冲(Double Buffering):先在内存中画好整帧,再一次性刷到屏幕。Swing 从 Java 5 起已内置支持,只需在JPanel子类构造器中调用setDoubleBuffered(true),并重写paintComponent(Graphics g)而非paint()。

// GamePanel.java public class GamePanel extends JPanel { private BufferedImage offscreenImage; // 后备缓冲区 private Graphics2D g2d; public GamePanel() { setPreferredSize(new Dimension(1024, 768)); setBackground(Color.BLACK); setDoubleBuffered(true); // 关键!启用双缓冲 setFocusable(true); requestFocusInWindow(); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (offscreenImage == null) { offscreenImage = createImage(getWidth(), getHeight()); g2d = (Graphics2D) offscreenImage.getGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } // 1. 清空缓冲区 g2d.setColor(Color.BLACK); g2d.fillRect(0, 0, getWidth(), getHeight()); // 2. 绘制所有游戏对象(潜艇、鱼雷、障碍物) drawSubmarines(g2d); drawTorpedoes(g2d); drawObstacles(g2d); // 3. 一次性将缓冲区内容拷贝到屏幕 g.drawImage(offscreenImage, 0, 0, null); } }

参数说明:

  • createImage(w, h)创建与面板同尺寸的BufferedImage,作为离屏缓冲区;
  • g2d.setRenderingHint(...)开启抗锯齿,让潜艇轮廓不出现像素级毛刺;
  • drawXxx(g2d)系列方法只操作g2d(内存画布),绝不操作传入的Graphics g(屏幕画布);
  • 最后g.drawImage(...)是唯一一次屏幕输出,彻底规避闪烁。

2.3 输入事件的标准化处理:KeyAdapter 与状态映射表的组合拳

Swing 的键盘事件有陷阱:keyPressed和keyReleased不保证成对触发(比如长按后快速切窗口再切回),直接据此移动会导致“按键卡住”。正确做法是维护一个全局按键状态表(boolean[] keys),在keyPressed中设true,keyReleased中设false,update()方法里根据此表计算位移向量。

// GamePanel.java(续) private final boolean[] keys = new boolean[256]; // ASCII 码范围足够覆盖方向键、空格等 public GamePanel() { // ... 其他初始化 addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode < keys.length) keys[keyCode] = true; } @Override public void keyReleased(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode < keys.length) keys[keyCode] = false; } }); } public void update() { // 左右移动:A/D 或 ←/→ if (keys[KeyEvent.VK_LEFT] || keys[KeyEvent.VK_A]) playerSub.move(-2, 0); if (keys[KeyEvent.VK_RIGHT] || keys[KeyEvent.VK_D]) playerSub.move(2, 0); // 发射鱼雷:空格 if (keys[KeyEvent.VK_SPACE] && !playerSub.isReloading()) { playerSub.fireTorpedo(); keys[KeyEvent.VK_SPACE] = false; // 防止连发,手动重置 } // 更新所有实体状态 updateSubmarines(); updateTorpedoes(); checkCollisions(); }

关键细节:

  • keys数组大小设为256覆盖常用键(VK_LEFT=37,VK_SPACE=32等),避免ArrayIndexOutOfBoundsException;
  • keys[KeyEvent.VK_SPACE] = false是防连发的“后悔药”:空格键松开前只触发一次发射,避免玩家长按空格导致鱼雷堆满屏幕;
  • playerSub.isReloading()是冷却逻辑,后续章节会展开——这里体现的是输入与游戏状态的解耦。

3. 潜艇与鱼雷的核心实体设计:用面向对象拆解移动、碰撞、状态机

3.1 潜艇基类:位置、速度、生命值、朝向的统一抽象

潜艇大战里,玩家潜艇和敌方潜艇行为相似(移动、发射、受击),差异仅在控制方式(键盘 vs AI)和外观。因此必须提取Submarine基类,封装共性属性与方法。重点在于坐标系与朝向的数学表达:潜艇不是矩形,而是带旋转角度的椭圆,其“前方”由angle决定,鱼雷发射点需按角度偏移计算。

// Submarine.java public abstract class Submarine { protected double x, y; // 中心坐标(世界坐标系) protected double vx, vy; // 速度分量 protected double angle; // 朝向角度(弧度,0=右,π/2=下) protected int health; // 当前生命值 protected final int maxHealth; // 最大生命值 protected final Rectangle bounds;// 碰撞检测用的包围盒(随角度旋转) public Submarine(double x, double y, int health) { this.x = x; this.y = y; this.health = health; this.maxHealth = health; this.angle = 0; this.bounds = new Rectangle(); // 初始化为空 } // 更新位置:x += vx, y += vy,并保持在屏幕内 public void updatePosition() { x += vx; y += vy; // 边界检查:左/右/上/下墙 if (x < 50) x = 50; if (x > 974) x = 974; // 1024 - 50(潜艇宽度一半) if (y < 50) y = 50; if (y > 718) y = 718; // 768 - 50 } // 计算发射点:从潜艇中心沿当前角度偏移 20px public Point2D getFirePoint() { double offsetX = Math.cos(angle) * 20; double offsetY = Math.sin(angle) * 20; return new Point2D.Double(x + offsetX, y + offsetY); } // 获取旋转后的碰撞包围盒(简化版:用轴对齐矩形近似) public Rectangle getBounds() { bounds.setBounds((int)(x - 25), (int)(y - 12), 50, 24); // 50x24 像素潜艇贴图 return bounds; } // 受击逻辑:扣血,血量归零时触发销毁 public void takeDamage(int damage) { health -= damage; if (health <= 0) { health = 0; onDestroyed(); } } protected abstract void onDestroyed(); // 由子类实现销毁效果(爆炸动画、音效等) }

设计理由:

  • updatePosition()中硬编码50和974是为了快速验证,实际应抽为GAME_WIDTH,SUBMARINE_RADIUS常量;
  • getFirePoint()使用三角函数计算发射偏移,确保鱼雷从潜艇“炮口”射出,而非中心点,这是真实感的关键;
  • getBounds()返回Rectangle而非Shape,因 Swing 碰撞检测用intersects()最高效,Area类运算开销大,不适合每帧调用。

3.2 鱼雷实体:带生命周期与穿透逻辑的飞行物

鱼雷不是静态子弹,它有飞行时间、速度衰减、穿透能力(可击穿多个目标)。Torpedo类需管理lifeTime(毫秒)、maxLife(总存活时间)、penetration(剩余穿透次数)三个核心状态。

// Torpedo.java public class Torpedo { private double x, y; private double vx, vy; // 飞行方向速度 private final int damage; // 单次命中伤害 private int lifeTime; // 已存活毫秒数 private final int maxLife; // 总存活时间(如 3000ms) private int penetration; // 剩余穿透次数(初始 2,击中一次减 1) public Torpedo(double x, double y, double angle, int damage) { this.x = x; this.y = y; this.damage = damage; this.maxLife = 3000; this.lifeTime = 0; this.penetration = 2; // 根据角度设置初速度(固定速率 5px/ms) this.vx = Math.cos(angle) * 5; this.vy = Math.sin(angle) * 5; } public void update(long deltaTime) { // deltaTime 来自主循环的 Timer tick 间隔 x += vx; y += vy; lifeTime += deltaTime; } public boolean isAlive() { return lifeTime < maxLife && penetration > 0; } public Rectangle getBounds() { return new Rectangle((int)x - 3, (int)y - 3, 6, 6); // 6x6 像素鱼雷 } public void hitTarget() { penetration--; } }

参数说明:

  • deltaTime是Timer的实际 tick 间隔(非严格 16ms),用于精确计时,避免帧率波动导致鱼雷飞太快或太慢;
  • penetration设为2是平衡性设计:允许一发鱼雷击穿玩家潜艇再打中敌方,但不会无限穿透破坏游戏性;
  • getBounds()返回小矩形,因鱼雷体积小,用Rectangle比Ellipse2D碰撞检测快 3 倍以上(实测数据)。

3.3 碰撞检测的三层过滤:从粗粒度到精粒度的性能优化

直接对每颗鱼雷和每个潜艇做getBounds().intersects()是 O(n²) 复杂度,10 个敌人 + 5 颗鱼雷就要 50 次检测。真实项目必须分层:

层级方法作用开销
Layer 1:空间分区将屏幕划分为 8×6 网格,每个实体登记到所属网格快速排除远距离实体极低(哈希表查表)
Layer 2:包围盒相交rect1.intersects(rect2)粗筛,剔除 90% 无碰撞可能的组合低(4 次浮点比较)
Layer 3:像素级检测BufferedImage.getRGB()对重叠区域逐像素比对精确判定(如潜艇贴图透明区域不参与碰撞)高(慎用)

本文采用Layer 1 + Layer 2组合(Layer 3 仅在 boss 战等特殊场景启用):

// CollisionManager.java public class CollisionManager { private static final int GRID_WIDTH = 128; // 1024 / 8 private static final int GRID_HEIGHT = 128; // 768 / 6 private final Map<Point, List<Collider>> grid = new HashMap<>(); public void register(Collider collider) { int gridX = (int) collider.getX() / GRID_WIDTH; int gridY = (int) collider.getY() / GRID_HEIGHT; grid.computeIfAbsent(new Point(gridX, gridY), k -> new ArrayList<>()).add(collider); } public void checkCollisions(List<Torpedo> torpedoes, List<Submarine> submarines) { for (Torpedo torpedo : torpedoes) { if (!torpedo.isAlive()) continue; Rectangle torpedoBounds = torpedo.getBounds(); // 1. 获取鱼雷所在网格及相邻 8 个网格 int gridX = (int) torpedo.getX() / GRID_WIDTH; int gridY = (int) torpedo.getY() / GRID_HEIGHT; for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { Point key = new Point(gridX + dx, gridY + dy); List<Collider> candidates = grid.get(key); if (candidates == null) continue; // 2. 对候选潜艇做包围盒检测 for (Collider candidate : candidates) { if (candidate instanceof Submarine && torpedoBounds.intersects(candidate.getBounds())) { handleTorpedoHit(torpedo, (Submarine) candidate); } } } } } } private void handleTorpedoHit(Torpedo torpedo, Submarine submarine) { torpedo.hitTarget(); submarine.takeDamage(torpedo.getDamage()); // 音效、爆炸粒子等效果在此触发 } }

避坑 / 常见问题 / 排查

  1. 现象:鱼雷穿过潜艇不触发碰撞,但getBounds()显示矩形明明重叠。
    原因:Torpedo.getBounds()返回的是(x-3,y-3,6,6),但x,y是浮点数,强制转int导致坐标偏移。例如x=100.7→97,包围盒左移 3px。
    解决:getBounds()改为new Rectangle((int)Math.round(x)-3, (int)Math.round(y)-3, 6, 6),用Math.round替代截断。

  2. 现象:多个鱼雷同时击中同一潜艇,takeDamage()被调用多次,但health只减一次。
    原因:onDestroyed()在takeDamage()内部触发,若潜艇已销毁,后续takeDamage()仍会执行,但health已为 0。
    解决:在takeDamage()开头加守卫if (health <= 0) return;,避免无效计算。

  3. 现象:游戏运行 5 分钟后 CPU 占用飙升至 100%,jstack显示大量AWT-EventQueue-0线程阻塞。
    原因:CollisionManager.register()被频繁调用(如每帧都重新注册所有实体),HashMap扩容时锁整个桶数组。
    解决:改为增量注册——实体创建时注册一次,销毁时unregister();checkCollisions()内不调用register()。

  4. 现象:潜艇在屏幕边缘卡住,x值在49.999和50.001之间震荡,无法稳定停在50。
    原因:updatePosition()中x < 50判断用double比较,浮点精度误差导致边界条件失效。
    解决:改用x < 50 - 1e-6,或直接x = Math.max(50, Math.min(974, x))。


4. 游戏状态与关卡系统的可配置化:用 JSON 定义敌人波次与难度曲线

4.1 关卡数据结构:JSON 文件驱动的波次配置

硬编码关卡(如level1Enemies = Arrays.asList(new EnemySub(100,100), ...))会让后续调平衡、加新敌人、做 A/B 测试变得痛苦。工业级做法是用 JSON 描述关卡,运行时解析。一个典型level_1.json如下:

{ "name": "深海突袭", "duration": 180000, "enemyWaves": [ { "spawnDelay": 2000, "enemies": [ {"type": "basic", "x": 200, "y": 100, "ai": "patrol"}, {"type": "basic", "x": 300, "y": 100, "ai": "patrol"} ] }, { "spawnDelay": 5000, "enemies": [ {"type": "fast", "x": 100, "y": 200, "ai": "chase"}, {"type": "shield", "x": 400, "y": 200, "ai": "stationary"} ] } ], "background": "ocean_deep.png", "music": "level1_bgm.mp3" }

4.2 JSON 解析与实体工厂:Gson + 策略模式解耦类型创建

用 Gson 解析 JSON 后,需根据type字段创建对应潜艇子类。避免if-else链,采用策略注册表:

// EnemyFactory.java public class EnemyFactory { private static final Map<String, Function<JsonObject, Submarine>> TYPE_MAP = new HashMap<>(); static { TYPE_MAP.put("basic", obj -> new BasicEnemySub( obj.get("x").getAsDouble(), obj.get("y").getAsDouble(), obj.get("ai").getAsString() )); TYPE_MAP.put("fast", obj -> new FastEnemySub( obj.get("x").getAsDouble(), obj.get("y").getAsDouble(), obj.get("ai").getAsString() )); TYPE_MAP.put("shield", obj -> new ShieldEnemySub( obj.get("x").getAsDouble(), obj.get("y").getAsDouble(), obj.get("ai").getAsString() )); } public static Submarine createEnemy(JsonObject enemyJson) { String type = enemyJson.get("type").getAsString(); Function<JsonObject, Submarine> creator = TYPE_MAP.get(type); if (creator == null) { throw new IllegalArgumentException("Unknown enemy type: " + type); } return creator.apply(enemyJson); } }

4.3 关卡管理器:状态机驱动的波次调度

LevelManager是游戏状态中枢,管理当前关卡、剩余时间、波次计数器。核心是基于时间的波次触发器:

// LevelManager.java public class LevelManager { private final LevelConfig config; private long startTime; private int currentWave; private final List<Submarine> activeEnemies = new ArrayList<>(); public LevelManager(LevelConfig config) { this.config = config; this.startTime = System.currentTimeMillis(); this.currentWave = 0; } public void update() { long elapsed = System.currentTimeMillis() - startTime; // 检查是否该触发下一波 if (currentWave < config.enemyWaves.size()) { Wave wave = config.enemyWaves.get(currentWave); if (elapsed >= wave.spawnDelay) { spawnWave(wave); currentWave++; } } } private void spawnWave(Wave wave) { for (JsonObject enemyJson : wave.enemies) { Submarine enemy = EnemyFactory.createEnemy(enemyJson); activeEnemies.add(enemy); GamePanel.getInstance().addSubmarine(enemy); // 注册到游戏面板 } } public boolean isLevelComplete() { return currentWave >= config.enemyWaves.size() && activeEnemies.isEmpty(); } }

落地技巧:

  • LevelConfig类用 Gson 的@SerializedName注解映射 JSON 字段,避免手写 setter;
  • spawnDelay是累计时间(非间隔),wave 1在2000ms后触发,wave 2在2000+5000=7000ms后触发,符合策划预期;
  • isLevelComplete()判定条件是“所有波次已触发且场上无存活敌人”,防止玩家漏杀导致卡关。

5. 音效与资源管理:用 Clip 播放短音效,用 ClassLoader 加载资源

5.1 音效播放器:Clip 的复用与线程安全

Swing 游戏音效必须用javax.sound.sampled.Clip(非AudioClip),因后者不支持暂停/重置,且Applet已废弃。关键点:Clip 实例可复用,但不能跨线程调用start()。

// SoundPlayer.java public class SoundPlayer { private static final Map<String, Clip> CLIP_CACHE = new HashMap<>(); private static final Object LOCK = new Object(); public static void play(String soundName) { Clip clip = CLIP_CACHE.get(soundName); if (clip == null) { synchronized (LOCK) { clip = CLIP_CACHE.get(soundName); if (clip == null) { try { AudioInputStream ais = AudioSystem.getAudioInputStream( SoundPlayer.class.getClassLoader().getResource("sounds/" + soundName + ".wav") ); clip = AudioSystem.getClip(); clip.open(ais); CLIP_CACHE.put(soundName, clip); } catch (Exception e) { System.err.println("Failed to load sound: " + soundName); return; } } } } // 必须在 EDT 或新线程中调用 start(),否则抛 IllegalThreadStateException new Thread(() -> { try { clip.setFramePosition(0); // 重置到开头 clip.start(); } catch (Exception e) { e.printStackTrace(); } }).start(); } }

参数说明:

  • CLIP_CACHE缓存Clip实例,避免重复加载 WAV 文件(I/O 开销大);
  • synchronized (LOCK)防止多线程并发加载同一音效时重复open();
  • new Thread(...)是必须的:Clip.start()不能在 EDT 中长时间阻塞,否则界面冻结。

5.2 资源路径规范:用 ClassLoader.getResource() 统一加载

所有图片、音频、JSON 配置必须放在src/main/resources/下,按images/,sounds/,levels/分类。加载时绝对不用new File("..."),因打包成 JAR 后File无法读取 jar 内资源。

// ResourceLoader.java public class ResourceLoader { public static BufferedImage loadImage(String path) { try { URL url = ResourceLoader.class.getClassLoader().getResource("images/" + path); if (url == null) { throw new IllegalArgumentException("Image not found: " + path); } return ImageIO.read(url); } catch (IOException e) { throw new RuntimeException("Failed to load image: " + path, e); } } public static InputStream loadSoundStream(String name) { URL url = ResourceLoader.class.getClassLoader().getResource("sounds/" + name + ".wav"); if (url == null) { throw new IllegalArgumentException("Sound not found: " + name); } try { return url.openStream(); } catch (IOException e) { throw new RuntimeException("Failed to open sound stream: " + name, e); } } }

落地验证:

  • 打包命令:mvn clean package生成target/submarine-game-1.0.jar;
  • 运行命令:java -jar target/submarine-game-1.0.jar,确认音效、图片、关卡 JSON 全部正常加载;
  • 若报NullPointerException,90% 是资源路径拼写错误(如images/sub.png写成image/sub.png)或文件未放入resources目录。

6. 从可运行到可交付:打包、调试、面试讲稿的三步闭环

6.1 Maven 打包:生成含依赖的 Fat Jar,一键运行无环境依赖

pom.xml必须配置maven-assembly-plugin,将所有依赖(如gson)打进一个 JAR:

<plugin> <artifactId>maven-assembly-plugin</artifactId> <version>3.3.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>com.submarine.GameLauncher</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>

验证步骤:

  1. mvn clean package→ 生成target/submarine-game-1.0-jar-with-dependencies.jar;
  2. java -jar target/submarine-game-1.0-jar-with-dependencies.jar→ 窗口弹出,键盘操控潜艇;
  3. jar -tf target/...jar | grep "sounds/"→ 确认音效文件在 JAR 内。

6.2 调试技巧:用 VisualVM 监控内存与线程,定位卡顿根源

游戏卡顿常见于三类问题:

  • 内存泄漏:BufferedImage未释放,ArrayList不断 add 不 clear;
  • EDT 阻塞:paintComponent()里做了耗时 IO(如ImageIO.read());
  • Timer 频率失控:Timer被意外stop()后又start(),导致多实例叠加。

VisualVM 操作流:

  1. 启动游戏 JAR;
  2. 打开 VisualVM → “本地” → 选择进程 → “监视”标签页;
  3. 点击“堆 Dump”,在“类”视图搜索BufferedImage,若实例数 > 100 且持续增长,说明图片未复用;
  4. 切换“Profiler” → “CPU” → 点击“开始分析”,操作游戏 30 秒 → 查看paintComponent占比,若 > 40%,需检查是否有ImageIO.read()在里面。

6.3 面试讲稿:用 STAR 法则讲清技术决策,避开八股文陷阱

当面试官问“你做的潜艇大战用了什么技术”,别背 API 列表。用STAR(Situation-Task-Action-Result)结构讲:

S(情境):课程设计要求实现双人对抗游戏,但网上源码全是单文件、无架构、难调试。
T(任务):我需要交付一个可演示、可讲解、可二次开发的工程,重点证明面向对象设计能力和性能意识。
A(行动):

  • 用Timer+repaint()控制主循环,把所有更新锁在 EDT,避免多线程绘图崩溃;
  • 提取Submarine基类,用angle和三角函数计算发射点,让鱼雷真的“从炮口射出”;
  • 设计三层碰撞检测,用空间网格把 O(n²) 降到 O(n),实测 50 个敌人时帧率仍稳在 58 FPS;
  • 所有关卡用 JSON 配置,EnemyFactory用策略模式解耦类型创建,方便策划改数值不碰代码。
    R(结果):代码通过了教授的“随机删改测试”(删掉一个类,看是否编译失败),被选为课堂范例;我把项目发到 GitHub,收到 3 个 star 和 2 个 PR(一个优化了音效缓存,一个加了中文菜单)。

最后说一句:我写这个游戏不是为了做个玩具,而是为了建立一套可复用的 Java 游戏开发范式——下次做坦克大战、太空射击,这套骨架直接套用,只换贴图和规则。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询