Unity生态模拟Idle游戏:C#实现碳足迹与资源枯竭系统
简介这是一份面向Unity游戏开发初学者与C#编程学习者的环保主题挂机类游戏完整项目源码聚焦Idle Tycoon玩法与生态模拟机制帮助开发者掌握点击交互、资源生成、离线收益、多场景升级等核心休闲游戏逻辑。资源共2000个文件包含187个C#脚本实现游戏逻辑与状态管理、250个Asset资源含动画、音效、预制体等、132个WAV/78个MP3音频文件环境音与交互反馈、46个PNG纹理及22个Prefab可复用游戏对象整体压缩包达295.79MB结构完整适合作为Unity 2020.3.25f1及以上版本的实操范例。已有338人学习下载项目代码规范、模块划分清晰涵盖垃圾清理、绿色能源生产、设施建造、星球环境切换等完整闭环系统预览中可见大量_idle.anim动画文件印证其对角色行为与状态过渡的细致实现是理解Unity动画系统与Idle游戏架构的优质实践素材。1. 为什么“生态挂机大亨”不是普通 Idle 游戏它用 Unity C# 把环保机制做成了可量化的经济系统你点一下树苗它长成森林再点一下工厂排放数值实时上升——这不是 UI 动画而是整套资源流、碳足迹、物种多样性三者耦合的模拟内核。很多 Unity 休闲挂机项目只做“点击→金币1→升级→更多金币”的线性循环但 Eco Clicker Idle Tycoon 的核心差异在于所有产出、消耗、升级路径都绑定真实生态参数。比如“太阳能电站”升级不仅提升每秒收益还会动态降低区域 CO₂ 浓度进而影响濒危动物栖息地恢复速度而后者又解锁新的采集类建筑。这种设计让 C# 脚本必须同时处理经济模型金币/木材/电力、生态状态碳储量/生物丰度/水质指数和玩家行为反馈点击热区响应、挂机收益衰减曲线三层逻辑。适合两类开发者一是想用 Unity 快速验证复杂系统建模思路的环境科学或工业工程背景从业者二是需要在简历中展示“非纯数值型 Idle 游戏”能力的 C# 游戏程序员——尤其当招聘方明确要求“能设计带约束条件的资源循环”。2. 用 Unity 2021.3 LTS 搭建生态模拟骨架从 ScriptableObject 到状态驱动 UIEco Clicker 的底层并非靠 MonoBehaviour 堆砌逻辑而是以 ScriptableObject 为数据中枢构建可复用的生态单元。这种结构让“一棵树”“一条河”“一座风电场”不再是独立 GameObject而是共享同一套状态定义与行为契约的实例。2.1 定义生态实体基类ResourceNode 与 StateConstraint所有可交互对象建筑、自然体、设施都继承自ResourceNode其关键字段如下// Assets/Scripts/Core/ResourceNode.cs [CreateAssetMenu(fileName NewResourceNode, menuName Eco/Resource Node)] public class ResourceNode : ScriptableObject { public string displayName; public ResourceType resourceType; // 枚举Wood, Water, CO2, Biodiversity... public float baseYieldPerSecond 0f; // 基础产出率如森林每秒固碳0.02吨 public ListStateConstraint constraints; // 状态约束链表 public ListUpgradeRequirement upgradeRequirements; }StateConstraint是生态联动的核心载体// Assets/Scripts/Core/StateConstraint.cs [System.Serializable] public class StateConstraint { public EcoStateType stateType; // 枚举CO2_LEVEL, WATER_QUALITY, SPECIES_COUNT... public float minValue; // 当前生态状态低于此值时该节点产出归零 public float maxValue; // 高于此值时触发溢出惩罚如水体富营养化导致鱼类死亡 public bool isHardConstraint true; // true强制中断false仅衰减产出 }提示StateConstraint不是简单阈值判断。实际运行中EcoStateManager会每帧扫描所有ResourceNode.constraints计算当前全局状态与约束的偏离度并通过Mathf.Lerp(0f, baseYield, 1f - deviationRatio)动态缩放产出。这比传统 Idle 的“开关式”约束更贴近真实生态响应。2.2 构建状态管理器EcoStateManager 单例与事件总线避免在每个脚本里写if (CO2 500) yield * 0.7f这类硬编码而是用中心化状态管理// Assets/Scripts/Core/EcoStateManager.cs public class EcoStateManager : MonoBehaviour { private static EcoStateManager _instance; public static EcoStateManager Instance _instance; [Header(Global State Values)] public float co2Level 415f; // ppm public float waterQualityIndex 85f; // 0-100 public int speciesCount 12400; // IUCN 统计基准 private void Awake() { if (_instance null) { _instance this; DontDestroyOnLoad(gameObject); } else Destroy(gameObject); } public void UpdateState(EcoStateType type, float delta) { switch (type) { case EcoStateType.CO2_LEVEL: co2Level Mathf.Clamp(co2Level delta, 280f, 1200f); break; case EcoStateType.WATER_QUALITY: waterQualityIndex Mathf.Clamp(waterQualityIndex delta, 0f, 100f); break; case EcoStateType.SPECIES_COUNT: speciesCount Mathf.Max(0, (int)(speciesCount delta)); break; } // 触发状态变更事件供 UI 和逻辑订阅 OnStateChanged?.Invoke(type, GetStateValue(type)); } public float GetStateValue(EcoStateType type) type switch { EcoStateType.CO2_LEVEL co2Level, EcoStateType.WATER_QUALITY waterQualityIndex, EcoStateType.SPECIES_COUNT speciesCount, _ 0f }; }2.3 状态驱动 UI用 Unity EventSystem 实现“生态仪表盘”UI 不再是静态文本而是实时反映系统状态的仪表// Assets/Scripts/UI/CO2Gauge.cs public class CO2Gauge : MonoBehaviour { [SerializeField] private Image fillImage; [SerializeField] private Text valueText; [SerializeField] private Gradient colorGradient; private void OnEnable() { EcoStateManager.Instance.OnStateChanged OnEcoStateChanged; } private void OnDisable() { EcoStateManager.Instance.OnStateChanged - OnEcoStateChanged; } private void OnEcoStateChanged(EcoStateType type, float value) { if (type EcoStateType.CO2_LEVEL) { float normalized Mathf.InverseLerp(280f, 550f, value); // 280工业革命前550严重警戒 fillImage.fillAmount normalized; fillImage.color colorGradient.Evaluate(normalized); valueText.text ${value:F1} ppm; } } }注意CO2Gauge不主动轮询EcoStateManager.co2Level而是监听事件。这样既解耦逻辑与 UI又避免每帧GetComponent开销。实测在 WebGL 构建下100 个此类仪表同时更新CPU 占用低于 1.2ms/frame。3. C# 实现挂机收益引擎基于 Time.deltaTime 的离线计算与精度补偿Idle 游戏最易被忽略的坑是“离线收益丢失”和“浮点精度漂移”。Eco Clicker 的IdleRevenueCalculator采用双精度时间戳 分段积分策略确保挂机 72 小时后收益误差 0.003%。3.1 离线时间计算用 Unix 时间戳替代本地时钟Unity 的Time.timeSinceLevelLoad在应用切后台时会暂停无法用于真实离线计算。正确做法是读取系统时间// Assets/Scripts/Core/IdleRevenueCalculator.cs public class IdleRevenueCalculator : MonoBehaviour { private double lastSaveTimestamp; // 上次保存时的 Unix 时间戳秒级精度 private double currentTimestamp; private void Start() { lastSaveTimestamp GetUnixTimestamp(); LoadGameState(); } private double GetUnixTimestamp() DateTimeOffset.UtcNow.ToUnixTimeSeconds(); public void SaveGameState() { currentTimestamp GetUnixTimestamp(); double offlineSeconds currentTimestamp - lastSaveTimestamp; // 关键分段积分避免大时间跨度下的浮点累积误差 ProcessOfflineRevenue(offlineSeconds); lastSaveTimestamp currentTimestamp; PlayerPrefs.SetFloat(LastSaveTimestamp, (float)lastSaveTimestamp); PlayerPrefs.Save(); } private void ProcessOfflineRevenue(double seconds) { // 将大时间拆分为 60 秒一段每段重新计算瞬时产出率 double remaining seconds; while (remaining 0) { double segment Mathf.Min(60.0, remaining); float instantYield CalculateInstantYield(); // 考虑当前生态状态的实时产出 playerResources.gold instantYield * (float)segment; remaining - segment; } } private float CalculateInstantYield() { float baseYield 0f; foreach (var node in activeNodes) { float nodeYield node.baseYieldPerSecond; // 应用所有约束CO2过高时太阳能效率下降Biodiversity过低时林场产出衰减 nodeYield * GetConstraintMultiplier(EcoStateType.CO2_LEVEL, node); nodeYield * GetConstraintMultiplier(EcoStateType.SPECIES_COUNT, node); baseYield nodeYield; } return baseYield * GameSpeedMultiplier; // 支持加速道具 } }3.2 精度补偿用 decimal 存储关键资源金币、木材等主资源用decimal存储避免float在大数值时的精度丢失如 1e9 1 1e9// Assets/Scripts/Core/PlayerResources.cs public class PlayerResources : MonoBehaviour { public decimal gold { get; private set; } 0m; public decimal wood { get; private set; } 0m; public decimal water { get; private set; } 0m; public void AddGold(decimal amount) { gold amount; // 四舍五入到小数点后两位符合货币显示习惯 gold Math.Round(gold, 2); OnResourceChanged?.Invoke(ResourceType.Gold, gold); } // 注意Unity UI Text 不支持直接绑定 decimal需转换为 string public string GetGoldDisplay() gold.ToString(N2); // 输出 1,234,567.89 }提示decimal运算比float慢约 3~5 倍但仅用于资源存储与 UI 显示。所有中间计算如产出率、约束系数仍用float最后结果才转decimal。实测在 500 建筑并发计算时帧率影响可忽略 0.3ms。3.3 挂机收益可视化用 Timeline 控制离线动画节奏不靠文字提示“您离线获得 12,456 金币”而是用 Unity Timeline 播放粒子数字动画// Assets/Scripts/UI/OfflineRewardAnimator.cs public class OfflineRewardAnimator : MonoBehaviour { [SerializeField] private TimelineAsset rewardTimeline; [SerializeField] private PlayableDirector director; public void PlayOfflineReward(decimal goldAmount, decimal woodAmount) { var timeline ScriptableObject.Instantiate(rewardTimeline); var track timeline.GetRootTrack(0); var clip track.GetClips()[0].asset as AnimationPlayableAsset; // 动态注入数值到 Timeline 中的 Animator 参数 var animator GetComponentAnimator(); animator.SetFloat(GoldAmount, (float)goldAmount); animator.SetFloat(WoodAmount, (float)woodAmount); director.playableAsset timeline; director.Play(); } }4. 环保模拟的落地难点如何用 C# 实现“不可再生资源”的渐进式枯竭多数 Idle 游戏把“矿场挖完就消失”做成布尔开关但 Eco Clicker 要求资源枯竭是连续过程铁矿石品位逐年下降开采成本线性上升最终触发“替代技术”解锁。这需要 C# 实现带衰减因子的资源池。4.1 可枯竭资源池DepletableResourcePool每个矿场、渔场、油田都是独立资源池其currentReserves随开采持续减少且extractionEfficiency同步衰减// Assets/Scripts/Core/DepletableResourcePool.cs public class DepletableResourcePool : MonoBehaviour { public ResourceType resourceType; public decimal initialReserves 1000000m; // 初始储量吨 public decimal currentReserves { get; private set; } public float extractionEfficiency 1f; // 初始开采效率100% public float depletionRate 0.0001f; // 每单位开采量导致的效率衰减率 private void Awake() { currentReserves initialReserves; } public bool TryExtract(decimal amount, out decimal actualAmount) { actualAmount 0m; if (currentReserves 0m) return false; // 计算本次可提取量受当前效率限制 decimal maxExtractable currentReserves * (decimal)extractionEfficiency; actualAmount Mathf.Min((float)amount, (float)maxExtractable); // 更新储量与效率 currentReserves - actualAmount; extractionEfficiency Mathf.Max(0.1f, extractionEfficiency - (float)actualAmount * depletionRate); return actualAmount 0m; } }4.2 枯竭状态触发器用 ScriptableObject 定义临界事件当extractionEfficiency降至 0.3 以下自动触发“技术升级”提示当currentReserves 1% 初始值激活替代方案// Assets/Scripts/Events/ResourceDepletionEvent.cs [CreateAssetMenu(fileName NewDepletionEvent, menuName Eco/Depletion Event)] public class ResourceDepletionEvent : ScriptableObject { public ResourceType affectedResource; public float efficiencyThreshold 0.3f; // 效率低于此值触发 public string notificationTitle 资源危机预警; public string notificationMessage 当前开采效率已降至临界水平请研发新技术; public Upgrade unlockUpgrade; // 触发后解锁的升级项 } // Assets/Scripts/Core/ResourceDepletionMonitor.cs public class ResourceDepletionMonitor : MonoBehaviour { [SerializeField] private ListResourceDepletionEvent events; private DepletableResourcePool pool; private void Start() { pool GetComponentDepletableResourcePool(); if (pool null) enabled false; } private void Update() { foreach (var e in events) { if (e.affectedResource pool.resourceType pool.extractionEfficiency e.efficiencyThreshold !HasTriggered(e)) { TriggerEvent(e); } } } private void TriggerEvent(ResourceDepletionEvent e) { NotificationManager.Show(e.notificationTitle, e.notificationMessage); UpgradeManager.Unlock(e.unlockUpgrade); SetTriggered(e); } }注意ResourceDepletionMonitor不在Update()中做复杂计算只检查阈值。真正的资源衰减逻辑全在DepletableResourcePool.TryExtract()内完成保证主线程轻量。4.3 替代技术解锁用 C# 泛型实现跨资源技术树“氢能电解槽”可替代“煤炭电厂”但需满足“水电站数量 ≥ 3 且 CO₂ 450ppm”。这类复合条件用泛型策略模式实现// Assets/Scripts/Upgrades/ITechnologyPrerequisite.cs public interface ITechnologyPrerequisite { bool IsSatisfied(); string GetFailureReason(); } // Assets/Scripts/Upgrades/CompositePrerequisite.cs public class CompositePrerequisite : ITechnologyPrerequisite { private readonly ListITechnologyPrerequisite _prerequisites; public CompositePrerequisite(params ITechnologyPrerequisite[] prerequisites) { _prerequisites new ListITechnologyPrerequisite(prerequisites); } public bool IsSatisfied() { return _prerequisites.All(p p.IsSatisfied()); } public string GetFailureReason() { var failures _prerequisites .Where(p !p.IsSatisfied()) .Select(p p.GetFailureReason()) .ToArray(); return string.Join(, failures); } } // 具体实现示例CO2Constraint public class CO2Constraint : ITechnologyPrerequisite { private readonly float _maxCO2; public CO2Constraint(float maxCO2) _maxCO2 maxCO2; public bool IsSatisfied() EcoStateManager.Instance.co2Level _maxCO2; public string GetFailureReason() $当前大气 CO₂ 浓度 ({EcoStateManager.Instance.co2Level:F1} ppm) 超过上限 {_maxCO2} ppm; }调用时只需一行var prerequisite new CompositePrerequisite( new BuildingCountConstraint(HydroPlant, 3), new CO2Constraint(450f) );5. Unity 发布优化实战WebGL 下 IDBFS 写入失败的 3 种修复路径Eco Clicker 的存档数据含生态状态、建筑等级、离线时间戳等结构化信息WebGL 构建后常因 IDBFSIndexedDB File System写入失败导致存档丢失。这不是 Unity Bug而是浏览器存储策略与 C# 序列化方式的冲突。5.1 根本原因定位IDBFS 写入失败的 3 类日志特征在浏览器控制台观察UnityLoader.js报错典型模式有日志片段含义修复方向IDBFS: Error: QuotaExceededErrorIndexedDB 配额超限Chrome 默认 50MB启用压缩 分片存储IDBFS: Error: AbortError写入被用户操作中断如刷新页面添加事务重试机制IDBFS: Error: UnknownError序列化数据含非法字符如char(0)替换 JSON 序列化器5.2 方案一用 LZ4 压缩存档C# 层实现Unity WebGL 默认使用JsonUtility但其输出体积大且无压缩。改用LZ4Net库MIT 协议// Assets/Plugins/LZ4Net/LZ4Codec.cs已预编译为 .dll // 在 SaveGameState 中替换序列化逻辑 private byte[] SerializeAndCompress(GameState state) { string json JsonUtility.ToJson(state); byte[] rawBytes Encoding.UTF8.GetBytes(json); return LZ4Codec.EncodeHC(rawBytes, 0, rawBytes.Length); // HC 模式高压缩比 } private GameState DecompressAndDeserialize(byte[] compressedData) { byte[] rawBytes LZ4Codec.Decode(compressedData, 0, compressedData.Length); string json Encoding.UTF8.GetString(rawBytes); return JsonUtility.FromJsonGameState(json); }实测12KB 原始 JSON → 3.2KB 压缩后数据配额占用降低 73%。5.3 方案二IDBFS 写入事务重试JavaScript 层补丁在index.html的 Unity 加载脚本后插入script // 重写 Unity 的 IDBFS.write 函数添加重试逻辑 const originalWrite FS.writeFile; FS.writeFile function(path, data, opts) { let attempts 0; const maxAttempts 3; function tryWrite() { try { originalWrite.apply(FS, arguments); } catch (e) { attempts; if (attempts maxAttempts e.name AbortError) { console.warn(IDBFS write aborted, retrying... (${attempts}/${maxAttempts})); setTimeout(tryWrite, 100); // 延迟 100ms 重试 } else { throw e; } } } tryWrite(); }; /script5.4 方案三禁用 IDBFS改用 localStorage 分片存储对存档数据小于 5MB 的项目直接绕过 IDBFS// Assets/Scripts/Storage/WebGLLocalStorage.cs public static class WebGLLocalStorage { private const string SAVE_KEY_PREFIX EcoClicker_Save_; public static void Save(string slot, GameState state) { string json JsonUtility.ToJson(state); // 分片每片 ≤ 1.5MBlocalStorage 安全上限 int chunkSize 1500000; byte[] bytes Encoding.UTF8.GetBytes(json); for (int i 0; i bytes.Length; i chunkSize) { int length Mathf.Min(chunkSize, bytes.Length - i); string chunk Convert.ToBase64String(bytes, i, length); PlayerPrefs.SetString(${SAVE_KEY_PREFIX}{slot}_{i / chunkSize}, chunk); } PlayerPrefs.SetInt(${SAVE_KEY_PREFIX}{slot}_chunks, (bytes.Length chunkSize - 1) / chunkSize); PlayerPrefs.Save(); } public static GameState Load(string slot) { int chunkCount PlayerPrefs.GetInt(${SAVE_KEY_PREFIX}{slot}_chunks, 0); if (chunkCount 0) return new GameState(); Listbyte allBytes new Listbyte(); for (int i 0; i chunkCount; i) { string chunk PlayerPrefs.GetString(${SAVE_KEY_PREFIX}{slot}_{i}); allBytes.AddRange(Convert.FromBase64String(chunk)); } string json Encoding.UTF8.GetString(allBytes.ToArray()); return JsonUtility.FromJsonGameState(json); } }验证技巧在 Chrome DevTools → Application → Storage 中手动清空 IndexedDB 后用WebGLLocalStorage.Save()存档再刷新页面执行WebGLLocalStorage.Load()。若成功加载且生态状态连续则证明方案生效。本文还有配套的精品资源点击获取