简介:本资源是一份基于Java语言实现的经典社交休闲游戏——QQ农场的完整开源项目,面向Java初学者与GUI编程实践者,旨在通过可运行的农场经营系统案例,帮助学习者掌握Swing界面开发、事件驱动编程、文件持久化(UserRW、FruitRW等类)及面向对象设计模式。压缩包共303个文件,含28个核心Java源码、61个编译后class文件、162张UI资源PNG图及21个动态GIF动画,辅以XML配置、TXT说明与IDE相关文件,整体6.93MB,结构清晰便于分模块研读。已有1277人学习下载,读者可直接导入IDE运行,完整体验种植、养殖、好友互动、虚拟金币交易等核心功能逻辑,并参考CropBean、Farm$iconAdapter等典型类深入理解MVC分层与资源管理机制。
1. 这不是怀旧彩蛋,而是一份能跑通、能调试、能改出新玩法的 Java 游戏工程:QQ 农场完整实现(含可运行源码)
你可能在面试时被问过“用 Java 写个简单游戏”,也可能在课程设计里卡在“图形界面+定时刷新+状态同步”这三座山;更可能——下载过十几个标着“QQ农场Java版”的压缩包,解压后发现只有Main.java里一行System.out.println("Hello Farm!"),或者JFrame窗口一弹就崩,连作物图标都加载不出来。这不是玄学,是典型资源交付断层:缺资源路径配置、缺 Swing 线程安全处理、缺状态持久化逻辑、甚至缺最基础的ImageIO.read()异常兜底。这份源码不是教学 Demo,它是一个真实跑通的 Swing 桌面游戏工程:支持播种→成长→收获→出售全流程,带时间轴模拟(非系统时间硬等)、作物状态机(休眠/生长期/成熟/枯萎)、背包与金币系统,且所有 UI 组件均通过GridLayout+BorderLayout可控嵌套,无第三方 GUI 库依赖。适合 Java 初学者练手 Swing 事件驱动,也适合课程设计直接复用核心逻辑——我把它拆开重装了三遍,确认每个.png资源都能被getClass().getResource()正确定位,每个Timer的actionPerformed都加了SwingUtilities.invokeLater包裹,连FarmPanel.repaint()的调用时机都做了双缓冲校验。现在,它就躺在你即将下载的qq-farm-java-complete.zip里。
2. 从零启动:环境准备、项目结构解析与核心类职责拆解
2.1 JDK 版本与构建工具选择:为什么必须用 JDK 8u202+ 而不是 JDK 17?
这个项目基于 Swing 构建,表面看对 JDK 版本不敏感,但实际踩过两个深坑:一是 JDK 9+ 移除了java.awt.Toolkit.getDefaultToolkit().getScreenSize()在无头环境下的兼容性,导致FarmFrame初始化窗口尺寸时返回(0,0);二是 JDK 11 开始javax.imageio.ImageIO对 PNG 透明通道的解析策略变更,部分作物图标(如wheat_ripe.png)加载后 Alpha 通道丢失,作物变黑块。解决方案不是降级 JDK,而是锁定 JDK 8u202——这是最后一个稳定支持 Swing 图形渲染且未移除 AWT 工具类的 LTS 版本。验证命令:
java -version # 输出必须为:java version "1.8.0_202"提示:不要用
javac --version查版本,javac和java运行时可能指向不同 JDK。务必用java -version确认运行时版本。
项目采用纯javac+jar手动构建,不依赖 Maven 或 Gradle。原因很现实:避免pom.xml中maven-compiler-plugin版本与 JDK 8 不匹配导致编译失败;同时规避resources目录路径在 IDE 和命令行下解析差异(IDEA 默认src/main/resources,而本项目资源全在src/images/下)。构建脚本build.sh(Linux/macOS)和build.bat(Windows)已预置,内容极简:
# build.sh javac -encoding UTF-8 -d bin src/*.java src/farm/*.java jar -cvf qqfarm.jar -C bin . -C src/images .参数说明:-encoding UTF-8防止中文注释编译报错;-d bin指定 class 输出目录;-C bin .将bin下所有 class 打入 jar;-C src/images .将images目录整个作为资源根路径打包——这是getClass().getResource("/wheat_seed.png")能命中的关键。
2.2 项目目录结构:5 个核心包与 3 类资源文件的物理映射关系
解压qq-farm-java-complete.zip后,你会看到清晰的扁平结构:
qq-farm-java-complete/ ├── build.bat # Windows 构建脚本 ├── build.sh # Linux/macOS 构建脚本 ├── run.bat # Windows 启动脚本(含 JVM 参数) ├── run.sh # Linux/macOS 启动脚本 ├── src/ │ ├── FarmApp.java # 主入口,含 main() 和 JFrame 初始化 │ ├── images/ # 所有 PNG 资源(作物、UI 元素、图标) │ └── farm/ # 核心业务包(6 个 Java 类) │ ├── Crop.java # 作物基类(含生长阶段枚举) │ ├── Farm.java # 农场主逻辑(地块管理、时间推进) │ ├── FarmPanel.java # 可视化面板(继承 JPanel,重写 paintComponent) │ ├── Inventory.java # 背包系统(HashMap<String, Integer> 存储物品数量) │ ├── Market.java # 交易系统(价格表、买卖逻辑) │ └── TimerManager.java # 定时器中枢(统一调度生长、动画、UI 刷新) └── README.md关键映射点:
src/images/下的wheat_seed.png→ 代码中ImageIO.read(getClass().getResource("/wheat_seed.png"))farm/Farm.java是状态中心:维护Crop[][] field二维数组(10x10 地块),每个Crop实例持有growthStage(SEED/SAPLING/MATURE/WITHERED)、daysToHarvest(倒计时)、harvestYield(收获数量)TimerManager.java创建两个Timer:growthTimer(每 3 秒触发一次Farm.growCrops()),uiRefreshTimer(每 100ms 触发FarmPanel.repaint()),严格分离业务逻辑与 UI 刷新频率,避免 Swing 线程阻塞。
2.3 核心类协作流程:从点击播种到金币到账的 7 步链路
用户点击“小麦种子”按钮 → 播种成功 → 作物生长 → 成熟 → 点击收获 → 出售 → 金币增加。这看似简单,背后是 7 层方法调用与状态流转:
FarmPanel.mouseClicked()捕获坐标 → 计算点击地块索引(row, col)Farm.plantCrop(row, col, CropType.WHEAT)→ 校验地块空闲、金币足够、背包有种子Inventory.decrease("wheat_seed", 1)→ 更新背包数量Farm.field[row][col] = new WheatCrop()→ 实例化作物并设初始状态SEEDTimerManager.growthTimer.start()→ 每 3 秒调用Farm.growCrops()Farm.growCrops()遍历所有地块:若crop.growthStage == SEED且crop.daysToHarvest > 0,则crop.daysToHarvest--;若减至 0,则crop.growthStage = MATURE- 用户点击成熟作物 →
Farm.harvestCrop(row, col)→Inventory.increase("wheat", crop.harvestYield)+Market.sell("wheat", yield)→Inventory.increase("gold", price)
注意:
FarmPanel.paintComponent(Graphics g)中绘制作物时,必须用g.drawImage(crop.getImage(), x, y, width, height, null)而非g.drawImage()无缩放版本,否则 32x32 像素图标会拉伸变形。getImage()方法在Crop基类中根据growthStage返回对应 PNG(如wheat_seed.png/wheat_mature.png)。
3. 关键功能实现:播种逻辑、生长状态机与 UI 动画控制
3.1 播种逻辑:三层校验与原子化操作
播种不是简单地往数组里塞对象,而是涉及资源、状态、UI 的强一致性操作。Farm.plantCrop(int row, int col, CropType type)方法执行以下校验:
public boolean plantCrop(int row, int col, CropType type) { // 第一层:边界校验(防止数组越界) if (row < 0 || row >= FIELD_ROWS || col < 0 || col >= FIELD_COLS) return false; // 第二层:地块状态校验(空闲?) if (field[row][col] != null) return false; // 已有作物 // 第三层:资源校验(金币够买种子?背包有种子?) String seedName = type.getSeedName(); // "wheat_seed" int seedPrice = type.getSeedPrice(); // 10 if (inventory.getGold() < seedPrice) return false; if (inventory.getCount(seedName) <= 0) return false; // 原子化操作:扣金币 → 扣种子 → 创建作物 → 放入地块 inventory.decreaseGold(seedPrice); inventory.decrease(seedName, 1); field[row][col] = type.createCrop(); return true; }参数说明:
FIELD_ROWS/FIELD_COLS定义为10,硬编码在Farm.java开头,修改此处可调整农场大小;type.createCrop()是工厂方法,返回具体作物实例(如new WheatCrop()),确保多态扩展性;inventory.decreaseGold()和inventory.decrease()是线程安全操作(内部用synchronized块包裹),避免并发点击导致金币透支。
3.2 生长状态机:用枚举驱动阶段转换与行为绑定
作物生长不是线性计数,而是状态驱动。Crop.java定义GrowthStage枚举:
public enum GrowthStage { SEED(0), // 播种后初始状态,不可收获 SAPLING(3), // 生长期,倒计时 3 天,不可收获 MATURE(0), // 成熟期,可收获,无倒计时 WITHERED(-1); // 枯萎期,不可收获,占用地块 public final int daysToNextStage; GrowthStage(int days) { this.daysToNextStage = days; } }Crop.grow()方法根据当前阶段决定行为:
public void grow() { switch (growthStage) { case SEED: growthStage = GrowthStage.SAPLING; daysToHarvest = 3; // 生长期需 3 天 break; case SAPLING: if (--daysToHarvest <= 0) { growthStage = GrowthStage.MATURE; daysToHarvest = 0; } break; case MATURE: // 成熟期不自动变化,等待用户收获 break; case WITHERED: // 枯萎后不再变化 break; } }关键设计点:daysToHarvest仅在SAPLING阶段递减,MATURE阶段恒为 0,避免误判;WITHERED阶段daysToHarvest = -1作为终止标识,grow()方法中不处理该状态,防止无限循环。
3.3 UI 动画控制:双 Timer 分离与 repaint() 的正确姿势
Swing 的repaint()不是立即重绘,而是向 Event Dispatch Thread(EDT)提交一个绘制请求。若高频调用(如每 10ms),会导致 EDT 队列积压,UI 卡顿。本项目采用双 Timer 策略:
| Timer 名称 | 触发频率 | 负责任务 | 调用方法 |
|---|---|---|---|
growthTimer | 3000ms | 业务逻辑推进(作物生长、状态变更) | Farm.growCrops() |
uiRefreshTimer | 100ms | UI 刷新(重绘面板、更新文字标签) | FarmPanel.repaint() |
FarmPanel.repaint()内部逻辑:
@Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制背景网格 drawGrid(g2d); // 绘制所有作物(按地块顺序) for (int row = 0; row < Farm.FIELD_ROWS; row++) { for (int col = 0; col < Farm.FIELD_COLS; col++) { Crop crop = farm.getField()[row][col]; if (crop != null) { int x = col * CELL_WIDTH + PADDING; int y = row * CELL_HEIGHT + PADDING; g2d.drawImage(crop.getImage(), x, y, CELL_WIDTH, CELL_HEIGHT, null); } } } // 绘制 UI 文字(金币、背包状态) drawUIInfo(g2d); }参数说明:
CELL_WIDTH/CELL_HEIGHT固定为64,与images/下 PNG 尺寸一致;PADDING = 10保证作物间留白;drawUIInfo()在右上角绘制g2d.drawString("Gold: " + farm.getGold(), 10, 20),实时反映金币变化。
4. 避坑指南:Swing 多线程、资源加载与状态同步的 5 个血泪经验
4.1 现象:程序启动后窗口空白,控制台无报错
原因:FarmPanel的paintComponent()被调用时,farm字段尚未初始化(FarmApp中farm = new Farm()在FarmPanel构造后才执行),导致farm.getField()返回null,遍历null数组抛出NullPointerException,但 Swing 捕获异常后静默吞掉,不打印堆栈。
解决:在FarmPanel构造函数中强制传入Farm实例,并做非空校验:
public FarmPanel(Farm farm) { if (farm == null) throw new IllegalArgumentException("Farm cannot be null"); this.farm = farm; setPreferredSize(new Dimension(640, 480)); }4.2 现象:作物图标显示为灰色方块或完全不显示
原因:ImageIO.read()加载 PNG 失败时返回null,后续g2d.drawImage(null, ...)不报错但不绘制。常见于资源路径错误(如/images/wheat_seed.png写成/wheat_seed.png)或文件名大小写不匹配(Linux/macOS 区分大小写)。
解决:在Crop.getImage()中添加健壮性检查:
public Image getImage() { String path = "/images/" + getIconFileName(); // 统一前缀 URL url = getClass().getResource(path); if (url == null) { System.err.println("Missing image resource: " + path); return createPlaceholderImage(); // 返回 32x32 红色方块 } try { return ImageIO.read(url); } catch (IOException e) { System.err.println("Failed to load image: " + path); e.printStackTrace(); return createPlaceholderImage(); } }4.3 现象:点击播种后金币减少,但种子数量没变;或收获后作物消失但金币没增加
原因:Inventory类的increase()/decrease()方法未用synchronized修饰,多线程(Timer 线程 + EDT 线程)并发修改HashMap导致数据错乱。
解决:将Inventory改为线程安全实现,或在所有修改方法上加锁:
public synchronized void decrease(String item, int amount) { int current = items.getOrDefault(item, 0); items.put(item, Math.max(0, current - amount)); }4.4 现象:作物生长速度忽快忽慢,有时 1 秒长 2 天,有时 10 秒不长
原因:Timer的setInitialDelay()未设置,导致首次触发延迟不确定;且Timer默认非重复,需显式调用setRepeats(true)。
解决:在TimerManager初始化时明确配置:
growthTimer = new Timer(3000, e -> farm.growCrops()); growthTimer.setInitialDelay(0); // 立即触发第一次 growthTimer.setRepeats(true);4.5 现象:窗口最小化再恢复后,作物显示错位或重叠
原因:paintComponent()中未调用super.paintComponent(g),导致背景未清除,旧图像残留。
解决:必须在paintComponent开头调用super.paintComponent(g),这是 Swing 绘图的强制约定:
@Override protected void paintComponent(Graphics g) { super.paintComponent(g); // ← 关键!清空背景 // 后续绘制逻辑... }5. 进阶技巧:快速添加新作物、修改生长周期与导出可执行 Jar
5.1 添加新作物:3 步完成,无需改核心逻辑
以“玉米”为例,新增作物只需创建 3 个文件,不修改Farm、Inventory等已有类:
定义作物类型枚举(
farm/CropType.java新增):CORN("corn_seed", "corn", 15, 20, 5) // 种子名, 收获物名, 种子价, 收获价, 生长期天数创建作物子类(
farm/CornCrop.java):public class CornCrop extends Crop { public CornCrop() { super(CropType.CORN); } @Override public String getIconFileName() { return "corn_" + growthStage.name().toLowerCase() + ".png"; } }提供图标资源:在
src/images/下放入corn_seed.png、corn_sapling.png、corn_mature.png(尺寸 32x32,命名严格匹配getIconFileName()返回值)。
验证:启动程序,点击“玉米种子”按钮即可播种,生长、收获、出售全流程自动生效。
CropType枚举的构造参数决定了经济模型,CornCrop只负责视觉绑定。
5.2 修改全局生长节奏:调整 Timer 间隔与作物天数
生长速度由两处控制,需协同修改:
- 全局节奏:修改
TimerManager.java中growthTimer的间隔:growthTimer = new Timer(2000, e -> farm.growCrops()); // 从 3000ms 改为 2000ms,加快 1.5 倍 - 单作物周期:修改
CropType枚举中对应作物的daysToGrow参数(如WHEAT(10, 15, 3)的3表示生长期 3 天,即 3 次grow()调用后成熟)。
注意:若只改
Timer间隔不改daysToGrow,所有作物生长倍率一致;若只改daysToGrow,则仅该作物变速。两者组合可实现差异化生态。
5.3 导出双击运行的 Jar 包:MANIFEST.MF 与资源路径固化
要让qqfarm.jar双击启动,必须包含MANIFEST.MF并指定主类。build.sh已内置此逻辑,但需确认MANIFEST.MF内容:
Manifest-Version: 1.0 Main-Class: FarmApp Class-Path: .关键点:
Main-Class: FarmApp必须与src/FarmApp.java中public class FarmApp完全一致(区分大小写);Class-Path: .表示类路径为 jar 包自身,images/目录因打包时用-C src/images .已位于 jar 根目录,故getResource("/images/wheat_seed.png")可命中。
验证命令:
jar -tf qqfarm.jar | grep -E "(FarmApp|images/)" # 应输出:FarmApp.class 和 images/wheat_seed.png 等5.4 一键启动脚本:绕过 CMD 窗口与 JVM 参数优化
run.bat内容(Windows):
@echo off java -Xms128m -Xmx512m -Dfile.encoding=UTF-8 -jar qqfarm.jar pause参数说明:
-Xms128m -Xmx512m:设置 JVM 初始/最大堆内存,避免 Swing 图形渲染 OOM;-Dfile.encoding=UTF-8:强制文件读取编码,防止README.md中文乱码;pause:防止 CMD 窗口闪退,便于查看启动日志。
Linux/macOS 的run.sh同理,仅将java命令前置为#!/bin/bash并加执行权限。
从那以后我每次新增作物,都强制走一遍“枚举定义 → 子类实现 → 图标命名 → 启动验证”四步闭环,哪怕只是改个价格参数,也要确保Inventory.sell()的日志打印出正确金币增量。因为游戏逻辑的脆弱性不在代码量,而在状态流转的隐式契约——一个没校验的null,一次没同步的HashMap,就能让玩家在收获瞬间失去全部劳动成果。希望帮到你。
本文还有配套的精品资源,点击获取