尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity环保挂机游戏骨架:ScriptableObject驱动的经济系统

Unity环保挂机游戏骨架:ScriptableObject驱动的经济系统 简介这是一份面向Unity游戏开发初学者与C#编程学习者的环保主题挂机类游戏完整项目源码适用于休闲游戏开发实践、Idle机制实现及生态模拟系统学习。资源包含2000个文件主体为187个C#脚本实现点击、升级、离线收益等核心逻辑、250个Asset资源含场景、预制体与动画控制器、132个WAV/78个MP3音效文件以及46个PNG纹理、22个Prefab和68个JSON配置数据整体压缩包达295.79MB结构完整便于模块化理解与二次开发。已有338人下载学习适合通过真实项目掌握Unity 2020.3.25f1版本下的UI交互、状态管理、资源加载与多线程优化技巧。项目内置丰富动画资源如WaterClearSlot_idle.anim、Car_garbage_idle.anim等配合清晰的层级命名与功能划分可直接运行调试并支持扩展星球环境、绿色技术树与污染治理系统是理解Idle Tycoon类游戏架构的优质教学范例。1. 这不是又一个“点点点”挂机游戏Eco Clicker Idle Tycoon 是一套可调试、可扩展、带完整经济闭环的 Unity 环保模拟骨架你点开过几十个“Idle Tycoon”模板项目最后发现全是预制体堆叠硬编码数值无状态保存——改个回收速率要翻三页脚本加个新污染类型得重写整个事件分发器。Eco Clicker Idle Tycoon 不同它用 C# 实现了清晰分层的状态管理ResourceSystemWorkerManagerFacilityController所有核心逻辑集中在Assets/Scripts/Core/下动画资源命名规范如Poacher_idle.anim、WaterClearSlot_idle.anim直接映射到污染类型与清理动作连Car garbage_idle.anim都对应真实垃圾模型的空闲循环。它解决的不是“怎么让数字变大”而是“如何让环保行为在离线状态下仍能持续产出纯净能源”。适合想练手 Unity 数据驱动设计、熟悉 ScriptableObject 构建配置表、或需要快速搭建带多星球生态链的休闲游戏原型的开发者——尤其当你已经卡在PlayerPrefs存档崩溃、协程调度混乱、或 UI 数值不同步超过 3 小时的时候。2. 从 Unity 2020.3.25f1 启动到首屏可交互环境准备与核心模块加载流程2.1 Unity 版本兼容性验证与基础依赖配置项目明确要求 Unity 2020.3.25f1 及以上这是关键约束。低于该版本会触发UnityEditor.BuildPlayerOptions中enableHeadlessMode的缺失报错高于 2021.3 则需手动降级UnityEngine.UI的CanvasScaler兼容性因Scale Factor在 2021.3 默认启用UI Scale Mode: Scale With Screen Size。验证方式为打开项目后立即执行# 在 Unity Console 中粘贴并回车非编辑器外终端 Debug.Log($Unity Version: {Application.unityVersion} | Scripting Backend: {PlayerSettings.GetScriptingBackend(BuildTargetGroup.Standalone)});提示若输出Scripting Backend: IL2CPP且版本为 2020.3.x则无需额外设置若为 Mono请在Edit Project Settings Player Other Settings中将Scripting Backend显式设为IL2CPP否则ResourceSystem中的ConcurrentDictionary会在构建时抛出NotSupportedException。项目未使用第三方包管理器如 OpenUPM所有依赖内置于Assets/Plugins/DOTweenv1.2.600用于 UI 动画缓动如能量条增长、按钮点击反馈TextMeshProv3.0.6所有文本渲染强制走 TMPFontAsset存于Assets/Resources/Fonts/Addressablesv1.19.17仅用于加载Assets/StreamingAssets/Config/下的.json配置表未启用远程加载因此Addressables.InitializeAsync()可安全移除2.2 核心数据流初始化从 ScriptableObject 配置到运行时 ResourceSystem游戏经济系统不靠硬编码数值而是通过ScriptableObject加载配置。关键路径如下2.2.1 配置表加载机制所有平衡参数存于Assets/Resources/Config/结构为Config/ ├── Resources/ │ ├── Energy.asset # 纯净能源基础产出率、离线倍率 │ └── PollutionTypes.asset # 污染类型ID、图标、清理耗时、产出能量值 ├── Facilities/ │ └── RecyclingPlant.asset # 回收设施等级、升级成本、处理速率 └── Workers/ └── EcoActivist.asset # 工人基础效率、雇佣成本、离线工作权重加载逻辑在GameManager.cs的Awake()中触发// Assets/Scripts/Core/GameManager.cs 第 42 行 private void Awake() { // 1. 加载全局资源配置 resourceConfig Resources.LoadResourceConfig(Config/Resources/Energy); pollutionConfig Resources.LoadPollutionConfig(Config/Resources/PollutionTypes); // 2. 初始化运行时资源系统单例 ResourceSystem.Instance.Initialize(resourceConfig, pollutionConfig); // 3. 加载设施与工人配置按需延迟加载避免启动卡顿 StartCoroutine(LoadFacilityConfigs()); }注意Resources.LoadT()调用必须确保路径中不含.asset扩展名否则返回 nullPollutionConfig继承自ScriptableObject其pollutionEntries字段是ListPollutionEntry每个PollutionEntry包含id: string如water_27对应Water_27_Flares.anim、cleanupTime: float秒、energyYield: int清理后获得能量值。2.2.2 ResourceSystem 的状态同步与离线计算ResourceSystem是整个经济引擎的核心它维护public Dictionarystring, int currentResources实时资源量key 为energy、workers、facilitiesprivate Dictionarystring, float lastSaveTime各资源上次保存时间戳private readonly ListWorkerData activeWorkers当前生效的工人列表含离线工作权重离线收益计算逻辑在Update()中每帧检查// Assets/Scripts/Core/ResourceSystem.cs 第 187 行 public void UpdateOfflineProduction() { float deltaTime Time.time - lastSaveTime[energy]; if (deltaTime 1f) return; // 避免首帧误算 // 计算离线期间总产出 工人效率 × 时间 × 离线权重 float offlineEnergy 0f; foreach (var worker in activeWorkers) { offlineEnergy worker.baseEfficiency * deltaTime * worker.offlineWeight; } // 应用设施加成如 RecyclingPlant 升级后提升 15% 处理速率 offlineEnergy * GetFacilityBonus(recycling_plant, production_rate); currentResources[energy] Mathf.FloorToInt(offlineEnergy); lastSaveTime[energy] Time.time; }关键参数说明worker.offlineWeight默认为 0.7表示离线时仅 70% 效率GetFacilityBonus()读取FacilityConfig中upgradeLevels[0].bonusValues[production_rate]该值在RecyclingPlant.asset中定义为0.15f即 15%。3. 动画资源与污染类型绑定从 Poacher_idle.anim 到游戏内行为逻辑映射3.1 动画命名规则与污染类型注册机制项目中列出的.anim文件如Poacher_idle.anim、WaterClearSlot_idle.anim并非随意命名而是严格遵循污染源_动作.anim格式前缀Poacher、WaterClearSlot、DirtSlot_(1)对应PollutionConfig.pollutionEntries中的id字段后缀_idle表示该污染源的默认空闲动画用于场景中循环播放同一污染源可能有_cleanup、_destroyed等变体虽未在列表中出现但代码预留了AnimationClip[]数组支持注册流程在PollutionSpawner.cs中完成// Assets/Scripts/Environment/PollutionSpawner.cs 第 63 行 private void RegisterPollutionAnimations() { // 1. 获取 Resources/Animations/ 下所有 AnimationClip AnimationClip[] clips Resources.LoadAllAnimationClip(Animations/); // 2. 按命名前缀分组如 Poacher_idle → Poacher foreach (AnimationClip clip in clips) { string prefix clip.name.Split(_)[0]; // 提取 Poacher if (pollutionConfig.GetEntryById(prefix) ! null) { pollutionAnimations[prefix] clip; // 存入字典供后续调用 } } }提示pollutionConfig.GetEntryById(prefix)内部执行pollutionEntries.Find(e e.id prefix)因此Poacher_idle.anim必须在PollutionTypes.asset中存在id Poacher的条目否则该动画不会被加载到内存。3.2 点击交互与动画状态机切换玩家点击污染源时触发PollutionObject.cs的OnMouseDown()// Assets/Scripts/Environment/PollutionObject.cs 第 112 行 public void OnMouseDown() { if (!canInteract) return; // 1. 播放清理动画替换 _idle 为 _cleanup string cleanupClipName pollutionId _cleanup; AnimationClip cleanupClip AnimationUtility.GetAnimationClip(cleanupClipName); if (cleanupClip ! null) { animator.Play(cleanupClip.name); } // 2. 触发资源系统结算 ResourceSystem.Instance.AddResource(energy, pollutionConfig.GetEntryById(pollutionId).energyYield); // 3. 销毁当前对象清理完成 Destroy(gameObject, cleanupClip.length); // 动画播完后销毁 }3.2.1 Animator Controller 结构解析每个污染源挂载的Animator Controller位于Assets/Animations/Controllers/以Pollution_Poacher.controller为例State Machine Layers:Base Layer默认包含Idle、Cleanup、Destroyed三个 StateIdleState 的 Motion 设为Poacher_idle.animTransition 条件为isCleaning trueCleanupState 的 Motion 设为Poacher_cleanup.animExit Time 设为0.95确保动画播完再跳转Parameters:isCleaningBool由PollutionObject.OnMouseDown()设置为trueisDestroyedBool由CleanupState 的OnStateExit事件触发调用DestroySelf()注意AnimationUtility.GetAnimationClip()是自定义工具方法实际代码中需确保Assets/Animations/Clips/下存在对应.anim文件否则cleanupClip为 null导致仅增加能量但无视觉反馈。3.3 多星球环境切换与污染类型动态加载游戏支持PlanetEarth、PlanetOcean、PlanetForest三种环境每种环境加载不同的污染配置// Assets/Scripts/UI/PlanetSelector.cs 第 89 行 public void SwitchToPlanet(string planetId) { // 1. 卸载当前污染对象 foreach (Transform child in pollutionContainer.transform) { Destroy(child.gameObject); } // 2. 加载新星球配置 string configPath $Config/Planets/{planetId}; PlanetConfig planetConfig Resources.LoadPlanetConfig(configPath); // 3. 按配置生成污染源如 PlanetOcean 加载 Water_20_Flares、Water_27_Flares foreach (string pollutionId in planetConfig.pollutionIds) { PollutionObject obj Instantiate(pollutionPrefab, pollutionContainer); obj.Initialize(pollutionId); // 绑定动画、配置、UI } }PlanetConfig的pollutionIds字段是字符串数组例如PlanetOcean.asset中为[water_20, water_27, water_21]直接对应Water_20_Flares.anim等文件名前缀。这种设计使新增星球只需创建新.asset文件并填充 ID 列表无需修改任何 C# 逻辑。4. 设施升级与工人雇佣系统基于 ScriptableObject 的可扩展经济模型4.1 设施系统从 RecyclingPlant.asset 到运行时 FacilityController设施数据完全由ScriptableObject驱动以RecyclingPlant.asset为例其字段结构为字段名类型示例值说明facilityIdstringrecycling_plant设施唯一标识用于ResourceSystem查找加成baseCostint500初始购买成本单位energyupgradeCostMultiplierfloat1.8f每次升级成本增幅第2级500×1.8第3级500×1.8²upgradeLevelsList[Lv1, Lv2, Lv3]每级提供的具体加成UpgradeLevel子结构level: int1,2,3…bonusValues: Dictionarystring, float如{production_rate: 0.15f, storage_capacity: 200f}unlockConditions: List 如[workers_eco_activist_level_2]依赖工人等级运行时FacilityController通过Upgrade()方法应用加成// Assets/Scripts/Core/FacilityController.cs 第 144 行 public void Upgrade() { if (currentLevel upgradeConfig.upgradeLevels.Count) return; int nextLevel currentLevel 1; UpgradeLevel levelData upgradeConfig.upgradeLevels[nextLevel - 1]; // 1. 扣除升级成本 if (!ResourceSystem.Instance.TrySpendResource(energy, GetUpgradeCost(nextLevel))) return; // 2. 应用加成到 ResourceSystem foreach (var kvp in levelData.bonusValues) { ResourceSystem.Instance.SetFacilityBonus(upgradeConfig.facilityId, kvp.Key, kvp.Value); } currentLevel nextLevel; }关键逻辑SetFacilityBonus()并非直接修改数值而是将(facilityId, bonusKey)作为 key 存入ResourceSystem.facilityBonuses字典后续GetFacilityBonus()会遍历所有激活设施的同名 bonus 并累加。例如recycling_plant.production_rate和solar_farm.production_rate可同时生效。4.2 工人系统EcoActivist.asset 与 WorkerManager 的协同调度工人数据同样由ScriptableObject定义EcoActivist.asset包含workerId:eco_activistbaseCost:200雇佣成本baseEfficiency:1.2f每秒产出 energy 基础值offlineWeight:0.7f离线效率权重unlockLevel:1解锁所需星球等级WorkerManager负责实例化与调度// Assets/Scripts/Core/WorkerManager.cs 第 95 行 public void HireWorker(string workerId, int count 1) { WorkerConfig config Resources.LoadWorkerConfig($Config/Workers/{workerId}); int totalCost config.baseCost * count; if (!ResourceSystem.Instance.TrySpendResource(energy, totalCost)) return; for (int i 0; i count; i) { WorkerData data new WorkerData { id workerId, efficiency config.baseEfficiency, offlineWeight config.offlineWeight, hireTime Time.time }; activeWorkers.Add(data); } }WorkerData是纯数据结构不继承MonoBehaviour因此activeWorkers列表可被ResourceSystem.UpdateOfflineProduction()直接遍历计算避免GetComponentWorker()的反射开销。4.3 经济平衡调试技巧修改 ScriptableObject 而非硬编码当需要调整游戏节奏时绝不修改 C# 中的数值常量而是打开Assets/Resources/Config/Resources/Energy.asset修改offlineProductionMultiplier离线收益倍率默认1.0f修改clickEnergyYield单次点击基础收益默认1保存后进入 Play Mode无需重启编辑器即可生效提示若修改后数值未更新检查ResourceSystem.Instance是否在Awake()中重新加载了配置——项目中Initialize()方法会强制重载Resources.Load因此修改.asset文件后必须点击 Unity 编辑器右上角的Play按钮而非Enter Play Mode快捷键确保Awake()重新执行。5. 构建 WebGl 时的 I/O 陷阱与存档持久化方案优化5.1 WebGL 构建失败根因IDBFS 写入权限与存档路径冲突项目在PlayerPrefs基础上扩展了SaveSystem.cs使用System.IO.File.WriteAllText()写入Application.persistentDataPath /save.dat。但在 WebGL 构建时persistentDataPath指向浏览器 IndexedDBIDBFS而 Unity 默认未启用写入权限。常见错误日志Failed to write save file: System.UnauthorizedAccessException: Access to the path .../save.dat is denied.解决方案分两步5.1.1 启用 IDBFS 并挂载虚拟文件系统在WebGL构建前在Player Settings Publishing Settings中勾选✅Use Preloaded Files确保save.dat被预加载✅Decompression Fallback防止 LZ4 解压失败并在index.html的head中插入script Module.onRuntimeInitialized function() { FS.mkdir(/IDBFS); FS.mount(IDBFS, {}, /IDBFS); FS.syncfs(true, function(err) { if (err) console.error(IDBFS sync failed:, err); }); }; /script5.1.2 修改 SaveSystem 适配 WebGL原SaveSystem.SaveGame()方法需判断平台// Assets/Scripts/Utils/SaveSystem.cs 第 72 行 public static void SaveGame(GameData data) { string json JsonUtility.ToJson(data); #if UNITY_WEBGL !UNITY_EDITOR // WebGL 使用 IDBFS 路径 string savePath /IDBFS/save.dat; try { FS.writeFile(savePath, json); Debug.Log(WebGL save success); } catch (Exception e) { Debug.LogError($WebGL save failed: {e.Message}); } #else // 其他平台使用 persistentDataPath string savePath Path.Combine(Application.persistentDataPath, save.dat); File.WriteAllText(savePath, json); #endif }注意FS.writeFile()是 Emscripten 提供的 API无需额外引用#if UNITY_WEBGL编译指令确保桌面平台仍走原逻辑。5.2 存档加密与防篡改轻量级 CRC32 校验实现为防止玩家手动修改save.dat项目在SaveSystem中集成 CRC32 校验// Assets/Scripts/Utils/SaveSystem.cs 第 128 行 private static string GenerateChecksum(string json) { byte[] bytes Encoding.UTF8.GetBytes(json); uint crc 0xFFFFFFFF; foreach (byte b in bytes) { crc ^ b; for (int i 0; i 8; i) { if ((crc 1) 1) crc (crc 1) ^ 0xEDB88320; else crc 1; } } return (crc ^ 0xFFFFFFFF).ToString(X8); // 8位十六进制 } public static bool LoadGame(out GameData data) { string json; #if UNITY_WEBGL !UNITY_EDITOR json FS.readFile(/IDBFS/save.dat, { encoding: utf8 }); #else string savePath Path.Combine(Application.persistentDataPath, save.dat); json File.ReadAllText(savePath); #endif // 校验 CRC32 int splitIndex json.LastIndexOf(|); if (splitIndex -1 || splitIndex json.Length - 9) { data default; return false; } string savedChecksum json.Substring(splitIndex 1); string content json.Substring(0, splitIndex); if (savedChecksum ! GenerateChecksum(content)) { Debug.LogWarning(Save file corrupted or tampered!); data default; return false; } data JsonUtility.FromJsonGameData(content); return true; }存档格式变为{energy:123,workers:[...]...}|A1B2C3D4末尾 8 位为 CRC32 校验码。此方案不依赖外部库且校验开销低于 1ms对 50KB JSON适合挂机游戏高频存档场景。5.3 离线模式下资源溢出防护动态限幅与软上限策略当玩家长时间离线UpdateOfflineProduction()可能产生超大数值如energy达int.MaxValue。项目采用双层防护硬限幅在ResourceSystem.AddResource()中public void AddResource(string resourceId, int amount) { if (!currentResources.ContainsKey(resourceId)) return; int newValue currentResources[resourceId] amount; // 防整数溢出 if (newValue int.MaxValue - 1000000 || newValue int.MinValue 1000000) { currentResources[resourceId] int.MaxValue - 1000000; Debug.LogWarning(${resourceId} capped at max safe value); return; } currentResources[resourceId] newValue; }软上限提示当energy超过10000000时UI 弹出提示// Assets/Scripts/UI/ResourceDisplay.cs 第 67 行 if (resourceId energy value 10000000) { TooltipManager.Show(Energy cap reached! Build more facilities to increase storage.); }此设计避免数值爆炸导致 UI 文本渲染异常如9999999999999999999超出 TMP 字体 atlas 宽度同时引导玩家进行设施升级符合“挂机建设”的核心循环。本文还有配套的精品资源点击获取
返回列表