news 2026/10/18 9:41:43

Java Swing游戏开发实战:大鱼吃小鱼教学骨架解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing游戏开发实战:大鱼吃小鱼教学骨架解析

简介:这是一份基于Java Swing开发的‘大鱼吃小鱼’经典小游戏完整源码工程,面向Java初学者与GUI编程入门者,帮助理解事件驱动、图形绘制、集合管理与简单游戏逻辑设计。资源包含43个文件,涵盖14个核心Java源码(含Ui主窗口、Entity实体类、用户登录注册模块)、15个编译后class文件、8张PNG/JPG游戏素材(如fish*.png、bg.jpg、start.png)及Eclipse项目配置文件(.project、.classpath、.prefs等),整体压缩包仅1.97MB,轻量易导入。已有273人学习下载,代码结构清晰,分层明确:src目录组织业务逻辑,images集中管理资源,User.text模拟简易用户数据存储。读者可直接运行体验完整游戏流程——从用户注册登录,到键盘控制己方鱼移动、碰撞检测、吞食/被吞判定,再到多鱼AI引导与动态集合渲染,是掌握Swing绘图、线程基础与面向对象设计的优质实践案例。

1. 这不是怀旧彩蛋,而是一套可跑、可调、可嵌入教学系统的 Java Swing 游戏骨架

某高校实训课上,导师让 A 同学三天内交一个“能体现面向对象设计+事件驱动+图形渲染”的 Java 小项目。他翻遍 GitHub 和 CSDN,最后选中这个「大鱼吃小鱼」——不是因为画风多炫,而是它把 Swing 的核心能力全摊开了:窗口生命周期管理、双缓冲绘图、键盘事件分发、对象集合动态渲染、碰撞判定与状态切换,连用户登录注册都用最朴素的文件存储做了闭环。它不依赖 Spring Boot、不碰数据库连接池、不走 REST API,纯 Java SE + AWT/Swing 原生 API 实现,JDK 8 即可编译运行。新手照着跑通后,能立刻看清「一个 Swing 程序从 main() 到 paintComponent() 再到 keyPressed() 的完整脉络」;熟手拆开 src 目录,三分钟就能定位到鱼群移动逻辑、碰撞检测边界、用户凭证校验入口——所有代码都在眼皮底下,没有黑匣子,也没有玄学配置。如果你正卡在「Swing 怎么组织 MVC 结构」「如何避免 repaint() 导致画面撕裂」「Swing Timer 和线程安全怎么平衡」这类问题上,这个项目就是你该打开的第一个 .jar。


2. 项目结构与技术栈解剖:从 .classpath 到 images/ 下的 10 张 PNG

这个压缩包表面看是「大鱼吃小鱼.rar」,但解压后你会看到一个典型的 Eclipse 工程结构,没有 Maven pom.xml,没有 Gradle 配置,靠 .classpath 和 .project 文件维持 IDE 兼容性。它不是玩具 Demo,而是一个具备生产级模块划分意识的 Swing 应用:UI 层、实体层、资源层、持久化层全部物理隔离。我把它重排成标准目录树后,发现其结构比多数教学代码更干净——连图片命名都带语义(fish03.png 是第三种敌方小鱼,bg.jpg 是唯一背景图),这种细节对后续扩展意义重大。

2.1 工程根目录:Eclipse 元数据与资源路径的隐含约定

大鱼吃小鱼/ ├── .classpath ← 指定源码路径为 src/,输出目录为 bin/ ├── .project ← Eclipse 项目标识,含 buildSpec(Java Builder) ├── .settings/ │ ├── org.eclipse.jdt.core.prefs ← 编码为 UTF-8,源码兼容 JDK 1.8 │ └── org.eclipse.core.resources.prefs ├── myFile/ ← 用户数据目录(注意:不是系统临时目录!) │ └── User.text ← 明文存储用户名+密码(SHA-256 加密前的原始拼接) ├── images/ ← 所有资源集中地,共 10 张 PNG + 1 张 JPG │ ├── bg.jpg ← 800×600 背景图,paintBackground() 中 drawImage() │ ├── fish.png ← 玩家主鱼(蓝色大鱼),宽高约 64×48 │ ├── fish03.png ~ fish17.png ← 7 种敌方小鱼(红/黄/绿/紫等色系,尺寸递减) │ ├── start.png ← 开始界面按钮贴图(非 Swing JButton,而是 drawImage 绘制) │ └── fish12.png ← 特殊体型鱼(头部偏大,用于测试碰撞半径容错) ├── src/ ← Java 源码根目录,包结构严格按功能分层 │ └── com/ │ └── entity/ ← 核心实体类:Fish(抽象基类)、PlayerFish(继承)、EnemyFish(继承)、User(登录凭证) ├── bin/ ← 编译输出目录,含 class 文件和 resources(如 User.text 被复制至此) └── 大鱼吃小鱼.java ← 主类,含 main(),new Ui().setVisible(true)

提示:myFile/User.text是程序首次运行时自动创建的,内容格式为username:password(冒号分隔,无空格)。它被硬编码在com.entity.User.login()方法中,路径拼接为"myFile" + File.separator + "User.text"。这不是最佳实践,但对教学场景足够透明——你想改存储方式?直接进 User.java 改两行就行。

2.2 src/com/entity/:面向对象建模的真实落地

整个游戏逻辑围绕Fish抽象类展开,它不是空接口,而是定义了所有鱼共有的行为契约:

// src/com/entity/Fish.java public abstract class Fish { protected int x, y; // 当前坐标(像素) protected int width, height; // 宽高(用于碰撞矩形) protected int speedX, speedY; // 水平/垂直速度(每帧偏移量) protected boolean isAlive; // 生存状态(被吃后设为 false,避免重复绘制) public abstract void move(); // 子类必须实现移动逻辑 public abstract Rectangle getBounds(); // 返回碰撞检测矩形(AABB) public abstract void draw(Graphics g); // 绘制自身(传入 Graphics 对象) }

PlayerFish和EnemyFish分别继承它,但实现天差地别:

  • PlayerFish.move()是空方法——因为它的移动由键盘事件实时驱动(keyPressed()中直接改x/y);
  • EnemyFish.move()则是真·移动:随机生成方向向量,叠加speedX/speedY,并做边界反弹(if (x < 0 || x > 750) speedX = -speedX);
  • getBounds()在PlayerFish中返回new Rectangle(x, y, 64, 48),而在EnemyFish中根据fishType(构造时传入 0~7)返回不同尺寸,比如fishType=3时返回new Rectangle(x, y, 32, 24)。

这种设计让「同一种操作(move)」在不同角色上有完全不同的语义,正是 Swing 游戏里多态价值的直观体现。

2.3 images/ 资源规范:为什么 fish12.png 不是随便编号?

你可能觉得fish03.png到fish17.png的编号很乱,但实际它们对应EnemyFish构造参数int type。查看Ui.java中鱼群初始化代码:

// src/Ui.java 片段 for (int i = 0; i < 15; i++) { int type = (int)(Math.random() * 8); // 0~7 共 8 种类型 EnemyFish enemy = new EnemyFish(100 + i*30, 100, type); enemyList.add(enemy); }

而EnemyFish构造器中:

// src/com/entity/EnemyFish.java public EnemyFish(int x, int y, int type) { this.x = x; this.y = y; this.type = type; switch (type) { case 0: this.img = loadImage("images/fish.png"); break; // 主鱼同款 case 1: this.img = loadImage("images/fish03.png"); break; // 小一号 case 2: this.img = loadImage("images/fish06.png"); break; // 更小+变色 case 3: this.img = loadImage("images/fish08.png"); break; // 扁平化体型 case 4: this.img = loadImage("images/fish12.png"); break; // 头部放大(重点!用于测试碰撞半径) case 5: this.img = loadImage("images/fish13.png"); break; case 6: this.img = loadImage("images/fish17.png"); break; default:this.img = loadImage("images/fish.png"); } // 根据 type 设置 width/height/speed,确保小鱼游得更快、更难抓 setDimensionsByType(); }

setDimensionsByType()方法里,type=4(即fish12.png)对应的width=40, height=32, speed=3,比type=0的width=64, height=48, speed=1快 3 倍且碰撞框更紧凑——这说明作者在用资源编号承载业务逻辑,不是随手打的标签。你要新增一种鱼?只需加一张图、在 switch 里加 case、在setDimensionsByType()里配参,不用动任何其他模块。

2.4 用户系统:文件存储的极简主义实现

登录注册没用 JDBC,没用 SQLite,就靠User.text一行一用户。User.java里两个关键方法:

// src/com/entity/User.java public static boolean login(String username, String password) { try (BufferedReader reader = new BufferedReader( new FileReader("myFile" + File.separator + "User.text"))) { String line; while ((line = reader.readLine()) != null) { String[] parts = line.split(":"); if (parts.length == 2 && parts[0].equals(username)) { // 注意:这里明文比对!教学用途,非生产环境 return parts[1].equals(password); } } } catch (IOException e) { e.printStackTrace(); } return false; } public static boolean register(String username, String password) { try (FileWriter writer = new FileWriter( "myFile" + File.separator + "User.text", true)) { // true 表示追加 writer.write(username + ":" + password + "\n"); return true; } catch (IOException e) { e.printStackTrace(); return false; } }

注意:FileReader和FileWriter默认使用平台默认编码(Windows 是 GBK),而 Eclipse 项目设置为 UTF-8。如果用户输入中文用户名,此处会乱码。这是本项目第一个真实踩坑点,我们放在第 4 章统一讲。


3. 运行与调试:从 JDK 8 到双缓冲防闪烁的实操链路

这个项目不需要 Tomcat、不依赖任何外部 JAR,但要跑起来,你得亲手过一遍从环境准备到画面稳定的全流程。很多教程只说「右键 Run As Java Application」,却不说为什么有时画面撕裂、有时键盘失灵、有时图片加载为空——这些恰恰是 Swing 新手最常翻车的地方。

3.1 环境准备:JDK 8 是底线,Eclipse 配置是关键

  • JDK 版本:必须 JDK 8u202 或更高(低版本SwingUtilities.invokeLater()行为有差异)。JDK 11+ 也能跑,但javax.swing.Timer在高 DPI 屏幕下需额外处理缩放,建议先用 JDK 8。
  • Eclipse 配置:
    • 右键项目 → Properties → Java Build Path → Libraries → 确认 JRE System Library 是JavaSE-1.8;
    • 同页面 → Source → 双击src→ 确认 Output folder 是bin(不是默认的bin/子目录);
    • 最关键一步:Properties → Java Compiler → 将 Compiler compliance level 设为1.8,并勾选Enable project specific settings。

提示:如果解压后直接 Import → Existing Projects into Workspace,Eclipse 可能因.project文件缺失或路径错误报红。此时手动新建 Java Project,名称随意(如FishGame),然后将解压后的src/拖入新项目的src文件夹,images/拖入项目根目录,myFile/同理。.classpath和.project文件可删掉,Eclipse 会自动生成。

3.2 启动流程:main() → Ui 构造 → paintComponent() 的完整调用栈

主类大鱼吃小鱼.java只有一行有效代码:

// 大鱼吃小鱼.java public class 大鱼吃小鱼 { public static void main(String[] args) { SwingUtilities.invokeLater(() -> new Ui().setVisible(true)); } }

SwingUtilities.invokeLater()是 Swing 线程安全的基石。它确保new Ui()和setVisible(true)都在 Event Dispatch Thread(EDT)中执行。Ui类继承JFrame,其构造器干了四件事:

  1. setTitle("大鱼吃小鱼")、setSize(800, 600)、setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  2. setResizable(false)锁死窗口大小(避免 resize 导致paintComponent()重绘异常);
  3. add(new GamePanel())——GamePanel是核心绘图面板,继承JPanel;
  4. addKeyListener(new KeyHandler())—— 注册全局键盘监听(注意:必须在setVisible(true)前添加,否则监听无效)。

GamePanel的paintComponent(Graphics g)是整个游戏的渲染心脏:

// src/Ui.java 内部类 GamePanel @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 双缓冲开始:创建内存图像 if (offscreenImage == null) { offscreenImage = createImage(getWidth(), getHeight()); } Graphics g2 = offscreenImage.getGraphics(); // 1. 绘制背景 g2.drawImage(bgImage, 0, 0, null); // 2. 绘制玩家鱼 playerFish.draw(g2); // 3. 绘制所有敌方鱼 for (EnemyFish enemy : enemyList) { if (enemy.isAlive()) { enemy.draw(g2); } } // 4. 绘制 UI 文字(分数、提示) g2.setColor(Color.WHITE); g2.setFont(new Font("微软雅黑", Font.BOLD, 16)); g2.drawString("分数:" + score, 20, 30); // 双缓冲结束:将内存图像一次性刷到屏幕 g.drawImage(offscreenImage, 0, 0, null); }

这段代码解释了为什么不用repaint()会闪烁:repaint()请求 EDT 异步重绘,若paintComponent()中直接drawImage()到屏幕,每一帧都是「画一半→清屏→画一半」的撕裂过程。而双缓冲强制先画到内存(offscreenImage),再整帧drawImage()到屏幕,视觉上就是丝滑动画。

3.3 键盘事件:KeyHandler 如何把 WASD 映射为鱼的位移

KeyHandler是Ui的内部类,实现KeyListener接口。它不处理keyTyped()(字符输入),只用keyPressed()和keyReleased():

// src/Ui.java 内部类 KeyHandler private class KeyHandler implements KeyListener { @Override public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_W: playerFish.setDirection(0, -1); break; // 上 case KeyEvent.VK_S: playerFish.setDirection(0, 1); break; // 下 case KeyEvent.VK_A: playerFish.setDirection(-1, 0); break; // 左 case KeyEvent.VK_D: playerFish.setDirection(1, 0); break; // 右 case KeyEvent.VK_SPACE: // 空格暂停/继续 isPaused = !isPaused; break; } } @Override public void keyReleased(KeyEvent e) { // 松开按键时,不重置方向!方向由最后一次按键决定(长按移动) // 这是游戏手感的关键:W 按住不放,鱼持续上移 } }

PlayerFish.setDirection(dx, dy)并不直接改x/y,而是更新directionX/directionY字段,真正的位移发生在GamePanel的gameLoop中:

// GamePanel.run() 方法(在 Timer 动作中调用) private void gameLoop() { if (!isPaused && playerFish.isAlive()) { // 根据方向向量和速度计算新坐标 playerFish.x += playerFish.directionX * playerFish.speed; playerFish.y += playerFish.directionY * playerFish.speed; // 边界检测:不能游出窗口 playerFish.x = Math.max(32, Math.min(playerFish.x, 750)); playerFish.y = Math.max(32, Math.min(playerFish.y, 550)); } }

这里Math.max/min的边界值(32 和 750)不是随便写的:playerFish.width=64,所以x=32保证鱼头不贴左边界,x=750(800-64+14)留出右侧安全区。这种硬编码数值在教学项目里很常见,但也是你二次开发时最先要参数化的点。

3.4 游戏循环:javax.swing.Timer 的精度与陷阱

GamePanel使用javax.swing.Timer驱动游戏逻辑,而非Thread.sleep():

// GamePanel 构造器中 timer = new Timer(16, e -> { // 16ms ≈ 60FPS gameLoop(); // 更新逻辑(移动、碰撞) checkCollision(); // 检测吃鱼/被吃 repaint(); // 触发 paintComponent() }); timer.start();

Timer(16, ...)的 16ms 是理论值,实际间隔受 EDT 负载影响。如果paintComponent()耗时超过 16ms(比如加载新图片),Timer会累积事件,导致「一帧执行多次gameLoop()」,鱼突然瞬移。这就是为什么gameLoop()里要加isPaused判断——暂停时Timer仍在触发,但逻辑被跳过,避免状态爆炸。

提示:想验证帧率?在gameLoop()开头加System.out.println(System.nanoTime()),观察时间戳间隔。若发现大量 32ms、48ms 输出,说明paintComponent()过重,需优化图片加载或减少绘制对象。


4. 避坑指南:5 个真实发生过的崩溃现场与修复方案

这个项目在 CSDN 博客评论区被问得最多的问题,几乎都集中在以下五个点。它们不是理论缺陷,而是开发者在真实环境(不同 Windows 版本、不同 JDK 补丁、不同显示器 DPI)下亲手踩出的坑。我把现象、根因、修复步骤写成可直接抄作业的清单。

4.1 现象:启动后窗口空白,控制台无报错,paintComponent()从未被调用

原因:GamePanel没有被正确添加到JFrame的内容窗格(content pane),或JFrame未调用pack()/setVisible(true)。
排查:在Ui构造器末尾加System.out.println("Ui constructed, panel added: " + (getContentPane().getComponentCount() > 0));
解决:确认add(new GamePanel())在super()之后、setVisible(true)之前;若用BorderLayout,需显式add(new GamePanel(), BorderLayout.CENTER);检查GamePanel是否重写了getPreferredSize()返回new Dimension(800,600),否则pack()会压缩为 0×0。

4.2 现象:键盘按下无反应,keyPressed()断点不触发

原因:JFrame或GamePanel未获取焦点(focus),Swing 键盘事件只发给当前焦点组件。
排查:在keyPressed()开头加System.out.println("Focus owner: " + KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner());
解决:在Ui构造器末尾加this.requestFocusInWindow();和gamePanel.requestFocusInWindow();;或在GamePanel中重写requestFocusInWindow()并返回true;终极方案:gamePanel.setFocusable(true); gamePanel.requestFocusInWindow();

4.3 现象:fish.png等图片加载失败,img == null,drawImage()报 NPE

原因:ImageIO.read()路径错误。images/fish.png是相对路径,但ImageIO.read()以 classpath 为根,而images/在项目根目录,不在bin/下。
排查:打印getClass().getResource("/images/fish.png"),若为null则路径不对。
解决:将images/文件夹拖入src/下(成为src/images/),或改用new ImageIcon("images/fish.png").getImage()(ImageIcon支持文件系统路径);更健壮写法:ImageIO.read(new File("images/fish.png"))。

4.4 现象:中文用户名注册后,登录时始终失败

原因:FileWriter默认用系统编码(GBK),而FileReader读取时也用系统编码,但 Eclipse 编辑器保存User.text时用 UTF-8,造成编码错位。
排查:用记事本打开myFile/User.text,看中文是否显示为乱码。
解决:统一用InputStreamReader/OutputStreamWriter指定编码:

// login() 中 try (BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("myFile" + File.separator + "User.text"), StandardCharsets.UTF_8))) { // register() 中 try (OutputStreamWriter writer = new OutputStreamWriter( new FileOutputStream("myFile" + File.separator + "User.text", true), StandardCharsets.UTF_8)) {

4.5 现象:游戏运行几分钟后 CPU 占用飙升至 100%,窗口卡死

原因:Timer事件堆积。当paintComponent()因图片加载慢或repaint()频繁而耗时过长,Timer的动作队列不断增长,EDT 被占满。
排查:在Timer动作中加计时:long start = System.nanoTime(); ... long end = System.nanoTime(); System.out.println("Loop time: " + (end-start)/1000000 + "ms");
解决:在Timer动作开头加if (isRunning) return; isRunning = true;,结尾加isRunning = false;;或改用SwingWorker异步加载图片;最简单:将Timer间隔从 16ms 改为 32ms(30FPS),降低压力。


5. 二次开发实战:从「加一条血条」到「接入 MySQL 用户库」的渐进改造

现在你已经跑通、理解了、避开了坑,下一步就是动手改。我不会教你“如何写一个新游戏”,而是给你一条清晰的改造路径:从 UI 层微调(加血条),到逻辑层增强(吃鱼音效),再到架构层升级(MySQL 替代文件存储)。每一步都附可验证的代码、必改的文件、以及我踩过的血泪经验。

5.1 第一步:在 UI 上加血条(Health Bar),理解 JPanel 的 Z-Order 渲染顺序

血条要始终显示在玩家鱼上方,且随生命值变化。PlayerFish类已有一个health字段(初始 100),但没用。我们把它可视化。

修改文件:src/Ui.java中的GamePanel.paintComponent()方法。

原逻辑(绘制玩家鱼):

playerFish.draw(g2);

替换为(先画血条,再画鱼):

// 1. 计算血条位置:在玩家鱼头顶 10px,宽度随 health 缩放 int barX = playerFish.x + (playerFish.width - 60) / 2; // 居中对齐鱼身 int barY = playerFish.y - 15; int barWidth = (int) (60 * (playerFish.health / 100.0)); // 0~60px int barHeight = 6; // 2. 绘制血条背景(灰色) g2.setColor(Color.GRAY); g2.fillRect(barX, barY, 60, barHeight); // 3. 绘制血条前景(红色,按比例) g2.setColor(playerFish.health > 30 ? Color.GREEN : playerFish.health > 10 ? Color.YELLOW : Color.RED); g2.fillRect(barX, barY, barWidth, barHeight); // 4. 绘制玩家鱼(覆盖在血条下方,但血条已画完,所以显示在上层) playerFish.draw(g2);

关键点:Swing 渲染是「后绘制的组件在上层」。我们先画血条(fillRect),再画鱼(drawImage),所以血条永远在玩家鱼上方。如果你把playerFish.draw(g2)放前面,血条会被鱼图遮住。

验证:运行后,按 W/A/S/D 移动,观察血条颜色变化(绿→黄→红)。想测试归零效果?在checkCollision()中找到被吃逻辑,加一行playerFish.health -= 10;,撞一次小鱼掉 10 血。

5.2 第二步:接入吃鱼音效,用 Clip 实现低延迟播放

Swing 自带AudioClip,但加载慢、不支持并发。我们用javax.sound.sampled.Clip,它可预加载、可循环、可精确控制。

新增文件:src/com/entity/SoundPlayer.java

package com.entity; import javax.sound.sampled.*; import java.io.File; public class SoundPlayer { private Clip clip; public SoundPlayer(String filePath) { try { AudioInputStream audioIn = AudioSystem.getAudioInputStream(new File(filePath)); clip = AudioSystem.getClip(); clip.open(audioIn); } catch (Exception e) { e.printStackTrace(); } } public void play() { if (clip != null) { if (clip.isRunning()) clip.stop(); // 防止重复播放卡顿 clip.setFramePosition(0); clip.start(); } } }

在Ui.java中初始化(GamePanel构造器):

private SoundPlayer eatSound; // ... eatSound = new SoundPlayer("sounds/eat.wav"); // 你需准备一个 100KB 以内的 WAV 文件

在checkCollision()中触发(当玩家鱼吃掉敌方鱼时):

if (playerFish.getBounds().intersects(enemy.getBounds()) && enemy.isAlive()) { enemy.setAlive(false); score += 10; if (eatSound != null) eatSound.play(); // 播放音效 }

血泪经验:WAV 文件必须是 PCM 编码、单声道、16bit、44100Hz。用 Audacity 导出时选「WAV (Microsoft) signed 16-bit PCM」。MP3 不支持Clip,会抛UnsupportedAudioFileException。

5.3 第三步:用 MySQL 替代 User.text,重构用户模块

这才是真正考验工程能力的一步。目标:登录注册走 MySQL,User.text彻底废弃。

第一步:建表

CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- 存 SHA-256 加密后字符串 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

第二步:引入 MySQL Connector/J
下载mysql-connector-java-8.0.33.jar,右键项目 → Build Path → Add External Archives,选中该 JAR。

第三步:重写User.java
删除所有FileReader/FileWriter逻辑,新增数据库连接:

// src/com/entity/User.java private static final String URL = "jdbc:mysql://localhost:3306/fishgame?useSSL=false&serverTimezone=UTC"; private static final String USER = "root"; private static final String PASS = "123456"; public static boolean login(String username, String password) { String sql = "SELECT password FROM users WHERE username = ?"; try (Connection conn = DriverManager.getConnection(URL, USER, PASS); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { String dbPass = rs.getString("password"); return dbPass.equals(hashPassword(password)); // hashPassword() 用 MessageDigest 实现 } } } catch (SQLException e) { e.printStackTrace(); } return false; } public static boolean register(String username, String password) { String sql = "INSERT INTO users (username, password) VALUES (?, ?)"; try (Connection conn = DriverManager.getConnection(URL, USER, PASS); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, hashPassword(password)); return ps.executeUpdate() == 1; } catch (SQLException e) { e.printStackTrace(); return false; } }

第四步:修改Ui.java中登录按钮逻辑
找到loginButton.addActionListener(...),将原来的User.login(u, p)替换为新方法。注意:Swing 不允许在 EDT 中执行耗时 DB 操作,必须用SwingWorker包裹:

new SwingWorker<Boolean, Void>() { @Override protected Boolean doInBackground() throws Exception { return User.login(usernameField.getText(), passwordField.getText()); } @Override protected void done() { try { if (get()) { JOptionPane.showMessageDialog(null, "登录成功!"); showGamePanel(); } else { JOptionPane.showMessageDialog(null, "用户名或密码错误"); } } catch (Exception e) { e.printStackTrace(); } } }.execute();

血泪经验:MySQL 8 默认启用 caching_sha2_password 插件,老版 Connector/J 不兼容。解决方案:安装 MySQL 8 时选「Use Legacy Authentication Method」,或在连接 URL 加&allowPublicKeyRetrieval=true&serverTimezone=UTC。

从加血条,到播音效,再到换数据库——这三步不是孤立功能,而是同一套思维:定位入口 → 修改最小单元 → 验证副作用 → 封装隔离。我第一次改这个项目时,在paintComponent()里加了 5 行血条代码,结果发现鱼游太快血条跟不上,又去调Timer间隔;加音效时没关clip.stop(),导致声音堆叠炸耳;换 MySQL 时忘了SwingWorker,界面直接假死 10 秒。从那以后我每次改 Swing 游戏,都强制走一遍「EDT 安全检查 → 资源释放检查 → 线程阻塞检查」三步。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 9:41:16

计算机网络面试高频考点:三次握手、DNS解析与HTTP缓存全梳理

简介&#xff1a;面向技术面试准备的计算机基础知识点 PDF 文档&#xff0c;重点覆盖计算机网络中的高频考点&#xff0c;与“代码随想录知识星球”最强八股文系列的计算机基础篇对应&#xff0c;适合后端、前端开发者跳槽或校招前集中复习。内容系统梳理了TCP三次握手与四次挥…

作者头像 李华
网站建设 2026/10/18 9:41:09

从部署到API对接:工厂AI视频分析智能监测系统实战指南

工厂车间的安全监测、人员行为规范、设备状态巡检、区域闯入预警&#xff0c;这些需求在传统监控方案里基本靠人盯屏&#xff0c;效率低而且漏报率高。这次我们来看一个更直接的解法&#xff1a;工厂AI视频分析智能监测系统。它不是单一算法&#xff0c;而是把视频接入、智能识…

作者头像 李华
网站建设 2026/10/18 9:40:01

Git任务分支开发与合并实战:主分支永葆干净

1. 为什么任务分支是团队开发的标配1.1 从“单线程开发”到“并行任务”的痛点很多刚接触Git的开发者&#xff0c;一开始都在master主分支上直接写代码。项目小、人少的时候&#xff0c;这种“单线程”模式确实能用&#xff1a;拉代码、改文件、提交、推送&#xff0c;一切顺理…

作者头像 李华
网站建设 2026/10/18 9:39:54

OpenMuse 开源解析:个人智能体从对话到干活的工程实现

1. 从 Muse 到 OpenMuse&#xff1a;个人智能体赛道正在发生什么Muse 这个产品在个人智能体圈子里一直是个特殊的存在。它最早以“对话式个人助理”的形态出现&#xff0c;主打的是把日程、笔记、任务、搜索这些日常操作统一到一个自然语言入口里。用过的人都有一个共同感受&am…

作者头像 李华
网站建设 2026/10/18 9:39:48

AI基建底层语言松动:Rust在推理、算子与通信层的实践

1. 从三个信号说起&#xff1a;为什么AI基建的底层语言开始松动过去几年&#xff0c;AI基础设施的默认技术栈几乎被Python和C牢牢占据。训练框架用Python写调度逻辑&#xff0c;底层算子用CUDA C写核函数&#xff0c;这套组合拳打了快十年&#xff0c;大家都觉得理所当然。但最…

作者头像 李华