
简介这是一份面向Unity3D开发者和模拟经营游戏爱好者的完整农场经营游戏源码包基于Unity4.6.1与C#脚本开发。玩法覆盖从小型农场到大型农场的全成长路径包含多种动物饲养、资源积累与市场交易等核心经营环节并设计了熊闯入农场的随机事件玩家通过连续点击将其捕获并贩卖增添操作互动乐趣。压缩包共2000个文件大小约160.18MB其中包含807个C#脚本、217个预制体、122个动画控制器、121个动画片段以及大量PNG场景与UI贴图项目目录结构清晰可直接导入Unity运行和二次修改。目前已有4417人学习下载。通过学习这套源码可以掌握模拟经营游戏的系统拆解、场景组织、动画控制、点击交互和随机事件触发等实战技巧适合用于个人学习、毕业设计或独立游戏项目的基础框架。1. Unity3D农场模拟经营一个「种收养卖」闭环的完整工程在编辑器里选中一个作物预制体把生长时间从 20 改成 120回到游戏界面农田里的作物立刻按新数值生长全程不用改一行代码——这是打开这个 Unity3D《farm business》农场模拟经营游戏源码后最先感觉到的部分。它不是一个只渲染了几块田的演示 Demo而是把种植、浇水、收获、动物喂养、售卖、时间流转、存档读取这一整条经营链路都做成了可复用的系统模块。这个项目比较适合两类人一类是刚开始做 Unity3D 游戏、想要一份能跑通完整玩法循环的参考工程的人另一类是打算做课设或练手作品想在经营模拟题材里快速拥有一个可持续扩展的底子的开发者。它的价值不在画面表现而在于状态机的设计、数据与表现分离的思路、以及一套不需要第三方插件就能完成的数据存档方案。接下来照着工程结构一层层拆开看。2. 技术选型与工程结构不依赖第三方插件的轻量架构2.1 技术栈与选型理由这个工程的技术栈非常收敛运行起来的主路径上几乎看不到任何第三方插件的身影Unity3D 自带的 UGUI 做界面层C# 脚本负责所有游戏系统ScriptableObject 充当数据配置的载体JsonUtility 完成存档落地。这种选型的直接好处是导入工程以后不需要解决插件版本冲突也不会遇到某些资源包因为许可证或平台限制而报错的问题。我一般会建议手里有这种工程的时候先按住冲动不急着升级渲染管线或换 UI 框架而是把它原本的这套轻量组合吃透。游戏里涉及的交互不算复杂核心无非是点击地块、选择种子、等待成熟、收获、卖给商店所以 UGUI 的 Button、Text、Image 加上 ScrollRect 已经完全够用。ScriptableObject 在这类经营模拟游戏里简直是天然契合的数据容器——一种作物可以是一份独立的资产成熟时长、出售价格、生长阶段图标全部配置在 Inspector 里策划改数值不会误伤代码逻辑。2.2 目录结构Assets 下的功能分区工程内部的资源组织是标准的 Unity 功能分区没有出现所有脚本堆在一个大文件夹里的情况。打开 Assets 目录之后大概能看到如下结构Assets/ |-- Scenes/ // 游戏主场景、启动场景 |-- Scripts/ | |-- Core/ // 游戏入口、单例基类、管理器注册 | |-- Farming/ // 种植、土地、作物状态相关脚本 | |-- Animal/ // 动物数据、饲养、产出逻辑 | |-- Economy/ // 金币、商店、背包系统 | |-- Save/ // 存档实体类与读写器 | |-- UI/ // 面板控制器与事件监听 |-- Data/ // ScriptableObject 资产作物配置、动物配置、商店价格表 |-- Prefabs/ // 作物预制体、动物预制体、UI 控件预制体 |-- Art/ // 图片、动画、字体等静态资源 |-- Plugins/ // 默认无第三方依赖这种分区方式让「改配置」和「改代码」天然隔离。Data 目录下存放的是各种 ScriptableObject 资产文件它是设计数据和逻辑实现之间的桥梁Prefabs 里是挂在场景中带表现组件的预制体。这样在设计上就形成了一条单向依赖Prefab 引用 ScriptableObjectScript 读取 ScriptableObject而 Script 不反向依赖任何具体预制体。2.3 数据驱动ScriptableObject 作为核心配置层项目的很多逻辑都依赖数据配置与代码逻辑的分离。以作物为例每种作物都有一个对应的 ScriptableObject 资产里面定义生长阶段数量、各阶段时长、成熟后的售价。代码里只依赖一个统一的CropData类型不用关心具体是哪种作物。// CropData.cs using UnityEngine; [CreateAssetMenu(fileName NewCrop, menuName Farm/Crop Data)] public class CropData : ScriptableObject { [Header(基础信息)] public string cropName; public Sprite[] phaseSprites; // 每个生长阶段的图标 [Header(生长参数)] public int totalPhases 4; // 一共几个生长阶段 public float phaseDuration 20f; // 每个阶段持续秒数 [Header(经济参数)] public int buyPrice; // 种子价格 public int sellPrice; // 成熟后售出价格 }这段定义是整个种植系统的数据基础。CreateAssetMenu让开发者可以在 Unity 编辑器里通过右键菜单直接创建新的作物资产不用编写额外的编辑器脚本。phaseSprites是每个生长阶段的贴图序列配合生长阶段索引既能驱动 Sprite 变化也能为以后加生长动画留好接口。总阶段数和单阶段时长共同决定了作物从播下到可收获的绝对时间这也让调整游戏节奏变得非常直观。这里想强调一个参数设计层面的细节把buyPrice和sellPrice拆在两个字段里而不是写死一个差价是为了以后做商店价格波动、节日特卖或者商人系统预留空间。到时候只需要在购买流程里加一个价格系数运算而不需要改动数据资产本身的结构。3. 核心模块拆解种植、动物、时间、存档四段关键代码3.1 种植系统从播种到收获的状态机种植模块是这个工程里最值得读的部分。它没有把每棵作物做成一个复杂的个体而是让地块FarmSlot承担状态管理地块上的作物表现只是一个随状态切换的 Sprite。简化后的核心逻辑是这样的// FarmSlot.cs using UnityEngine; public class FarmSlot : MonoBehaviour { public enum SlotState { Empty, Planted, Growing, Mature, Wilted } [SerializeField] private SpriteRenderer cropSprite; [SerializeField] private CropData currentCrop; private SlotState state SlotState.Empty; private int currentPhase 0; private float phaseTimer 0f; public bool CanPlant state SlotState.Empty; // 外部调用在地块上种下指定作物 public void PlantCrop(CropData data) { if (!CanPlant) return; currentCrop data; currentPhase 0; phaseTimer 0f; state SlotState.Planted; UpdateVisual(); } // 由 TimeManager 每帧/每固定间隔驱动而非自行累加时间 public void Tick(float delta) { if (state ! SlotState.Planted state ! SlotState.Growing) return; phaseTimer delta; if (phaseTimer currentCrop.phaseDuration) { phaseTimer 0f; AdvancePhase(); } } private void AdvancePhase() { currentPhase; if (currentPhase currentCrop.totalPhases) { state SlotState.Mature; } else { state SlotState.Growing; } UpdateVisual(); } private void UpdateVisual() { if (currentCrop null) return; if (currentPhase currentCrop.phaseSprites.Length) { cropSprite.sprite currentCrop.phaseSprites[currentPhase]; } } }FarmSlot的状态机用枚举划分了五个阶段其中Planted和Growing的区分是为了以后能在种植初期做「不可被动物踩踏」或「浇水后跳过此阶段」之类的玩法留接口。Tick方法不从自身聚合 Time.deltaTime而是等待外部统一驱动——这是整个工程时间系统设计的核心约定后面讲 TimeManager 的时候还会提到。这样做的最大好处是暂停游戏、倍速经营、跨天结算都由上层统一控制地块不需要知道自己处于哪个时间上下文。参数上phaseTimer清零时机的选择要注意它是在到达阈值时归零而不是在开始生长时记录目标时刻这样可以避免用DateTime或较大数值做比较时产生精度问题。对于单阶段时长 20 秒级别的模拟游戏来说这个精度绰绰有余。3.2 动物系统喂食、心情与产出的关联动物模块在工程里是和种植并列的第二个资源产出途径。动物的设计比作物多了一层交互反馈需要玩家主动喂食喂食后动物在下一产出周期才能给出产物心情值影响产出数量。核心代码大致如下// AnimalController.cs using UnityEngine; public class AnimalController : MonoBehaviour { [SerializeField] private AnimalData animalData; [SerializeField] private int mood 80; // 心情值 0-100 [SerializeField] private int feedCount 0; // 本周期已喂食次数 [SerializeField] private float produceTimer 0f; public bool CanProduce feedCount animalData.requiredFeed; public void Feed() { if (feedCount animalData.requiredFeed) return; feedCount; mood Mathf.Min(100, mood animalData.moodGainPerFeed); // 播放吃食动画、显示飘字等表现由外部事件接管 } public void Tick(float delta) { if (!CanProduce) return; produceTimer delta; if (produceTimer animalData.produceInterval) { produceTimer 0f; Produce(); } } private void Produce() { int amount Random.Range(animalData.minYield, animalData.maxYield 1); if (mood 70) amount; // 心情好时额外 1 产量 // 通过事件把产物发给 Inventory 系统 GameEvents.OnAnimalProduce?.Invoke(animalData.productId, amount); } }这段逻辑里有一个值得注意的经济设计requiredFeed和produceInterval是两个不同维度的约束条件。requiredFeed是每周期必须凑够的喂食次数produceInterval是产出计时周期两者共同作用才能让「喂食→等待→产出」形成可感知的操作节奏。如果只做计时玩家就会放着不管如果只做喂食次数就成了纯点击器的套路。工程把两个条件叠在一起玩家既要在时间窗口内喂食又要等待产出计时模拟经营的「操心感」就出来了。mood参数对产出的影响在代码里是以固定阈值 70 判断的实际运行时阈值也可以做成动物数据里的一个可配置字段这样不同品种的动物对心情的敏感度就能差异化。我在自己做经营类项目时一般会更激进一点把minYield和maxYield也做成心情的线性插值函数产量随心情变化而不是跳变。3.3 时间系统驱动一切模拟的统一时钟前面反复提到Tick(delta)是外部驱动的这个外部就是TimeManager。它不是一个简单的累加器而是整个模拟世界的心跳源。它对外暴露统一的 Tick 委托所有需要随时间推进的系统只需注册这个委托不需要各自持有计时逻辑// TimeManager.cs using UnityEngine; using System; public class TimeManager : MonoBehaviour { public static TimeManager Instance { get; private set; } public event Actionint OnMinuteChanged; public event Actionint OnDayChanged; [SerializeField] private int minute 6 * 60; // 从早上 6:00 开始 [SerializeField] private int day 1; [SerializeField] private float realSecondsPerGameMinute 1f; private float timer 0f; private void Awake() { Instance this; } private void Update() { timer Time.deltaTime; if (timer realSecondsPerGameMinute) { timer - realSecondsPerGameMinute; AdvanceMinute(); } } private void AdvanceMinute() { minute; OnMinuteChanged?.Invoke(minute); if (minute 24 * 60) { minute 0; day; OnDayChanged?.Invoke(day); } } }这个实现把游戏时间和真实时间做了一次线性映射realSecondsPerGameMinute 1f意味着现实 1 秒走游戏内 1 分钟那么现实 24 分钟就是游戏内一天。这个数值在调试阶段可以临时调成 0.2f 来快速验证跨天逻辑调成 5f 则变成慢节奏休闲经营。它的巧妙之处在于所有下游系统只需要依赖OnMinuteChanged和OnDayChanged两个事件自身完全不需要感知真实时间的流速。这里有一个很容易踩的坑会在避坑章节展开如果用Time.deltaTime直接驱动那么游戏在后台切出去再回来时Time.deltaTime会是一个很大的跳跃值作物可能瞬间从种子长成大树。比较稳妥的做法是在OnApplicationFocus(false)的时候暂停模拟或者丢弃累积时间增量工程里的结构已经为这种修复留好了位置。3.4 存档系统JsonUtility 结合中转列表的读写方案存档是经营模拟游戏的后悔药所在。这个工程使用 JsonUtility 作为序列化方案它能力有限——比如无法直接序列化Dictionary也无法处理多态类型。因此工程的做法是定义一套纯数据结构的存档实体再用中转列表解决字典类型的序列化问题。核心实现如下// SaveManager.cs using UnityEngine; using System.Collections.Generic; [System.Serializable] public class CropSaveData { public string slotId; // 地块唯一标识 public string cropId; // 作物 ID 或 ScriptableObject 名称 public int phase; public float phaseTimer; } [System.Serializable] public class InventorySaveData { public string itemId; public int amount; } [System.Serializable] public class GameSaveData { public int day; public int minute; public int coin; public ListCropSaveData crops new ListCropSaveData(); public ListInventorySaveData inventory new ListInventorySaveData(); } public class SaveManager : MonoBehaviour { private const string SaveKey farm_save_v1; public void SaveGame() { GameSaveData data new GameSaveData(); data.day TimeManager.Instance.GetDay(); data.minute TimeManager.Instance.GetMinute(); data.coin EconomyManager.Instance.Coin; // 把字典结构展开成 List 再交给 JsonUtility foreach (KeyValuePairstring, CropSaveData pair in FarmManager.Instance.GetAllCropSave()) { data.crops.Add(pair.Value); } string json JsonUtility.ToJson(data); PlayerPrefs.SetString(SaveKey, json); PlayerPrefs.Save(); } public void LoadGame() { string json PlayerPrefs.GetString(SaveKey, string.Empty); if (string.IsNullOrEmpty(json)) return; GameSaveData data JsonUtility.FromJsonGameSaveData(json); // 将 List 还原成字典并分发到各个管理器 } }这套方案的序列化实体是完全独立于运行时对象的好处是以后换存档格式比如换成 JSON.NET 或 SQLite时只需要动 SaveManager 和序列化实体类不需要在 FarmSlot 里塞一堆存档字段。slotId作为字符串而非整数是为了兼容将来地图扩展后的命名规则。cropId存储的是 ScriptableObject 资源的对象名而不是引用因为直接序列化对象引用在不同平台和资源打包环境下极不稳定。存档时机上工程选择在关键节点触发自动保存比如跨天瞬间、玩家主动打开暂停菜单时。手动存档的入口一般放在设置界面或者背包界面角落避免玩家误触导致回档。4. 场景与数据组织预制体、UI、ScriptableObject 怎么串成一条线4.1 场景层级管理器的挂载与加载顺序打开游戏主场景后Hierarchy 窗口里可以看到一套非常典型的管理器挂载结构。根节点下一共挂了四个比较核心的物体TimeManager统一时钟、EconomyManager金币与订单经济、FarmManager农田地块容器、UIManager所有面板的挂载点和切换控制。四个管理器在 Awake 里完成自身单例注册在 Start 里按依赖顺序完成互相绑定。FarmManager 需要拿到 TimeManager 的 Tick 事件因此它的 Init 方法必须在 TimeManager 之后执行。这种结构的控制流是单向的UIManager 只通过公开方法调用 EconomyManager 和 FarmManager 的方法不直接访问具体地块FarmManager 只做地块收集和转发事件TimeManager 只负责发时间事件不知道也没有必要知道谁在听它的声音。这样改动的容错性有很大提升比如以后要加一个天气系统只需要让天气管理器也监听同一个时钟事件再在 UI 上增加一个表现层切图不需要侵入种植逻辑。4.2 UI 面板与事件绑定从按钮点击到游戏行为UIManager 管理着店铺面板、背包面板、主界面工具栏和设置面板。关键交互集中在三个地方购买种子、选择种子种到地块、收获产物放入背包。工程里大量使用了 Unity 的 EventTrigger 与公开方法绑定例如在种子商店的物品条目预制体上挂了一个ShopItemUI脚本它把当前商品的数据引用保存在自己的字段里// ShopItemUI.cs 的核心片段 public Button buyButton; private CropData boundCrop; public void BindCrop(CropData data) { boundCrop data; icon.sprite data.phaseSprites[0]; priceText.text data.buyPrice.ToString(); } public void OnBuyButtonClicked() { EconomyManager.Instance.TryPurchase(boundCrop); }按钮的点击事件在 Inspector 的 OnClick 面板里直接绑定到了OnBuyButtonClicked方法而不是在代码里动态注册。这样做的优点是团队成员可以在不打开脚本的情况下重新绑定 UI 事件缺点则是重构时如果改了方法签名所有引用事件都会失效并在 Inspector 里显示 Missing避坑环节会说到这个现象。背包面板的数据展示也走了类似路径背包系统内部是一个以 itemId 为键、数量为值的字典UI 层每次打开面板时请求一份快照列表并刷新格子而不是维护一个常驻的 UI 数据副本。这样保证 UI 不会出现「逻辑上有一百个物品界面只显示八十个」的脏数据情况。4.3 预制体结构表现与逻辑分离的复用单元作物预制体的结构很轻一个根节点挂FarmSlot脚本下面有一个子物体持有SpriteRenderer所有生长阶段的贴图都不放在预制体身上而是从CropData里读取。这意味着同一套预制体可以种小麦、种萝卜、种南瓜只需要在游戏运行时把不同的CropData资产传入即可。这种设计带来一个操作上的改变做新作物时正确的流程是先创建一张作物数据资产然后拖预制体到场景地块并指定数据资产而不是复制一个新预制体出来改贴图。开发久了就会形成肌肉记忆——打开编辑器先确认数据文件再考虑要不要动场景。动物预制体和商店条目都是同一思路数据放进资源表现留在场景。Prefab 变体配合数据的组合方式在项目中期扩建的时候优势明显。比如新增一个需要浇水才会继续生长的作物不需要新建一整棵预制体只需要在原有作物预制体的基础上增加一个浇水状态图再在数据资产里加一个needWater布尔字段修改FarmSlot的AdvancePhase让它检查这个字段即可。5. 跑起来与避坑导入工程到稳定运行的实操记录5.1 从打开 Unity Hub 到点击 Play拿到工程压缩包后第一件事不是急着解压双击而是先打开 Unity Hub在「项目」标签页里点击「添加」并将解压出来的文件夹直接选中。等待 Unity 编译脚本期间留意控制台有没有红色报错。工程目标平台建议保持默认的 Windows/Mac 独立平台在 PC 环境下直接跑最省事。第一次进入场景时如果看到画面是纯色或相机没有对准农场需要检查 Hierarchy 里的 Main Camera 是否启用了Clear Flags中的 Solid Color并且把FarmManager节点下的地块根物体拉入视野。之后在 Game 视图设置 16:9 或 4:3 分辨比点击 Play 就能开始游戏。跑通一轮完整流程的建议做法开局不急着买高级种子先种三块基础作物等第一波成熟后卖掉积累一点金币再购买动物。通过这个流程把种植计时、收获入包、售卖扣钱、背包刷新四个节点全部验证一遍确认数据流转没有断裂。5.2 避坑记录导入与运行阶段的现场还原下面问题全部是实跑工程或改代码时最容易遇到的按「现象 → 原因 → 解决」的顺序记录每条都是实际开发中容易反复横跳的地方。问题一导入工程后 Console 报大量 API 兼容错误现象打开工程后编译阶段出现十余条以Obsolete或TypeLoadException开头的报错场景里所有预制体组件丢失图标。原因本地 Unity 编辑器版本与工程原始版本之间存在 API 变动尤其是输入系统或渲染管线相关的层。最典型的是工程原本用的是旧版输入管理器而本地编辑器把 Active Input Handling 切成了新版 Input System。解决在 Edit → Project Settings → Player → Active Input Handling 里切换为Input Manager (Old)或Both等待重新编译。其余过期 API 可以执行Edit → Project Settings → Editor → Auto-Referenced之外的API Updater自动升级但升级前建议备份整个工程目录。问题二作物在后台切回后瞬间成熟现象游戏最小化一段时间后切回来所有作物直接跳过中间状态进入成熟或枯死。原因TimeManager.Update使用Time.deltaTime累加而游戏挂起期间 Unity 返回的单帧 delta 值远高于正常帧间隔导致计时器一次性跨过多个生长阶段。解决在TimeManager中监听Application.focusChanged或OnApplicationPause为 false 时记录暂停标志恢复后的第一帧不累计时间。最小改动是给Update加一个条件判断如果focus为 false 则跳过累加逻辑。问题三商店购买种子成功扣款但背包数量不增加现象点击购买按钮后货币数量正确减少但打开背包面板货物列表仍为空。原因经济系统在扣款后调用背包系统的AddItem时传入了新的临时对象而背包 UI 展示时依据 ID 匹配到的是ScriptableObject资源引用两边 ID 不是同一个实例。背包内部字典认为这是两个不同物品查询时就漏掉了。解决统一物品数据入口购买或拾取后在背包里维护DictitemId, amount金额展示与逻辑匹配都要通过同一份ItemDatabase资产查表不要直接引用商店面板传入的运行时对象。问题四存档写入成功但重新加载后部分地块数据为空现象Save 时是有数据的退出重进后很多slotId命中的地块没有作物。原因JsonUtility序列化Dictionary时会直接失败或产生空对象而工程内如果使用了字典存放cropSaveDict并且直接 ToJson则输出 JSON 里会缺失字段。解决严格使用存档章节展示的「字典转 List」方案自定义可序列化类承接字典的键值对反序列化后再还原成字典分发给 FarmManager。这是 JSON.NET 之外最省事的原生方案也是这个工程选择的路线。问题五改完脚本后预制体 Inspector 里出现「Missing Script」警告现象某个脚本改了类名或移动到别的命名空间场景中的预制体组件仍然指向旧脚本在 Inspector 里显示为红色缺失项。原因Unity 以 GUID 和文件相对路径识别脚本引用类名改变不会自动更新场景引用。解决尽量避免改脚本类名如果必须改重新拖一次预制体组件并复制原参数或者用文本编辑器全局搜索旧脚本 GUID 替换为新脚本 GUID。修改完以后务必打开所有场景检查一遍没有 Missing Script 残留再提交版本。6. 二次开发落点加作物、调经济、改存档的三个方向这个工程给了不错的底子但它更多像一张骨架肉还得自己长。最容易上手的改造是从数据层切入新建一个新作物资产复制CropData资产后重新命名在 Inspector 里替换phaseSprites的生长阶段贴图把成熟时长设置为 60、售价调成 35再把它加进商店的售卖列表即可。整个过程中不需要动一行 C# 代码因为种植逻辑完全读取数据资产来驱动运行。如果想让新作物表现特殊比如成熟后需要额外点击一次才能收获则在FarmSlot的Mature状态里增加一个交互判定分支。第二个方向是做经济系统上的调整让「种什么赚钱」成为一个动态问题。目前的sellPrice是静态配置可以给EconomyManager加一个每日价格波动模块每天早上根据昨日销售数量生成一个价格系数乘以基础价得到当日售价。这个改动只需要新增一个PriceFluctuationManager并监听TimeManager.OnDayChanged在 UI 商店价格文本刷新时查询实时价格即可不需要重构现有购买链路。存档系统的改造我建议优先考虑从 PlayerPrefs 迁移到本地文件。原因很简单随着动物数量、仓库物品数量增长JSON 字符串会越写越长PlayerPrefs 在 PC 上处理较长的字符串没有性能问题但跨平台尤其是 Web 端时会有写入上限的隐患。可以保留现有GameSaveData结构把SaveManager里的PlayerPrefs.SetString替换为File.WriteAllText路径放在Application.persistentDataPath下。升级时记得做旧存档兼容读不到新文件就回退到 PlayerPrefs防止已有玩家进度丢失。验证每一次改动有没有做坏原有玩法我常用的办法是给项目定一条「一日冒烟脚本」手动操作顺序固定为种地 → 快速调时间到第二天 → 喂食 → 收获 → 售卖 → 存档 → 重进。全程不写自动化测试就是自己盯着控制台和 UI 数字过一遍。只要这条链路不断核心体验就还稳着。很多次改完数据配置觉得自己逻辑天衣无缝结果冒烟测试直接在收获环节翻车——后来养成了习惯每次改完哪怕只是加一个图标都会强制走一遍完整的游戏循环确认不是只改了个「看起来能跑」的假模块。这些都是某次调式到半夜之后的血泪经验希望帮到你。本文还有配套的精品资源点击获取