用QFramework构建Unity类幸存者金币升级系统:从数据到UI的完整架构
在类幸存者游戏里金币升级系统是中期节奏的核心击杀掉金币关卡结束进升级商店买完属性再进下一关。这套循环做得顺不顺直接决定玩家的投入感。代码组织上如果还是想在哪写就在哪写金币字段散落在 HUD、敌人脚本、存档脚本里等再过两周加新武器、新升级项时就会开始补丁叠补丁。这次我们来看 QFramework 在 Unity 游戏开发里怎么支撑“类幸存者”项目的金币升级系统设计。这套系统会涉及数据存储、升级逻辑、界面刷新、事件通信和存档恢复正好适合拿 QFramework 的 MVC 分层、事件系统和 UIKit 面板来整体收口。本文会按“需求拆解 - 数据层设计 - 逻辑层 System - 事件通信 - UIKit UI 实现 - 存档恢复 - 性能观察 - 常见排错”的顺序展开并且会给出可直接套用的代码框架。如果你在用 Unity 做独立游戏尤其是类幸存者、轻度肉鸽这种“局内成长 局外积累”的项目这篇文章建议收藏备用。1. 核心能力速览在写金币升级系统前先明确 QFramework 这套方案在本文里的能力边界。能力项说明项目类型Unity 客户端架构框架开源项目适合中小型游戏快速搭建本篇主题使用 QFramework 搭建类幸存者金币升级系统核心模块MVCModel / View / Controller、事件系统、UIKit、代码生成主要功能金币增删改查、升级项解锁、升级价格计算、UI 自动刷新、存档写入适配游戏类幸存者、肉鸽、射击、RPG 内购属性加点等语言环境C#Unity 2019.4 以上建议具体以 QFramework 仓库说明为准启动方式通过 Unity Package Manager 或 Git URL 导入 QFramework是否支持批量任务不涉及游戏内升级项配置适合用 ScriptableObject 批量管理是否提供接口 API提供 C# 层系统接口支持编辑器工具链适合读者已熟悉 Unity 基础想用框架约束项目结构的开发者这里强调一个原则QFramework 不是 UI 插件也不是资源服务器它是把项目分层、事件通信、模块调用约束到固定轨道上的架构框架。金币升级系统这种强数据、强交互、强持久化的玩法模块用它做会让后续扩展更可控。2. 适用场景与使用边界金币升级系统适合用 QFramework 来实现但不是所有项目都必须用它。适合的场景类幸存者、类吸血鬼幸存者、轻度肉鸽项目存在局内金币和局外成长两条线。团队多人协作需要统一文件结构避免“一个人一套写法”。中小型项目不想引入过大架构但仍需要清晰的 MVC 分层。后续会频繁加升级项、加角色、加敌人需要把数据、逻辑和表现分开。不适合的场景纯 Demo、纯美术验证项目全部都写在场景脚本里可能更快。已经稳定运行且不需要维护的老项目周边重构风险偏高。团队完全没有分层概念又不打算学习框架约束导入后反而会觉得“束缚”。使用边界方面金币升级系统涉及存档数据QFramework 层面的架构只解决模块解耦不替代具体的加密、防修改和用户鉴权。独立游戏金币存档如果只做本地玩家体验可以不做高强度加密但针对排行榜、联网对战类玩法金币数据必须放到服务端校验不能信任客户端。另外素材和角色引用需要注意授权涉及角色立绘、音效、UI 图标时确认资源拥有合法授权。3. 环境准备与前置条件在动手前先把环境整理好。3.1 Unity 版本与开发环境从 QFramework 仓库和社区常见用法看推荐使用 Unity 2019.4 LTS 以上的版本。实际新项目建议直接用 Unity 2021 LTS 或 2022 LTS稳定性更好UI Toolkit 和 Addressables 的支持也更完整。操作系统方面Windows / macOS 均可。Windows 下要注意安装 Unity Hub。安装对应 Unity 版本时勾选 Windows Build SupportIL2CPP。安装 Visual Studio 或 Rider并用“外部脚本编辑器”指向它。如果遇到类似 “No valid Unity Editor license found. Please activate your license.” 的提示说明 Unity 许可证没有激活先打开 Unity Hub 登录并激活许可证再回来继续开发。3.2 导入 QFrameworkQFramework 可以通过 Unity Package Manager 的 Git URL 导入这是目前最常用的方式。在 Unity 中操作打开Window - Package Manager。点击左上角号选择Add package from git URL...。输入 QFramework 的 Git 仓库地址。等待解析完成。解析过程中需要联网。如果国内网络访问 GitHub 较慢可以尝试先克隆仓库到本地再通过Add package from disk导入。导入完成后菜单栏会出现 QFramework 相关菜单项说明框架安装成功。3.3 目录规划建议在 Assets 下规划如下目录Assets/ Scripts/ Game/ Architecture/ Model/ System/ Utility/ UI/ UIPanel/ UIWidget/ Data/ Config/ Save/ Art/ Resources/金币升级系统的脚本主要放在Game/Model、Game/System、UI/UIPanel三块。4. 金币升级系统的需求拆解先不写代码把类幸存者游戏中的金币升级系统拆成几个基础问题。4.1 类幸存者中的经济循环类幸存者的核心循环是玩家击杀敌人。敌人掉落金币。玩家拾取金币金币数量变化。局内可以消耗金币升级武器、恢复生命、购买临时能力。关卡结束或玩家死亡后根据表现获得局外货币用来永久提升基础属性。这里至少有两套货币体系局内金币本局临时资源死后清零。局外货币跨局积累用于永久升级。设计时不要把两套数据混在同一个类里否则局内重置和存档写入会互相干扰。4.2 升级项的分类升级项可以粗分为三类全局永久升级进入任何一局前就生效例如初始攻击力 5%。局内即时升级在局内通过金币购买当前角色立即生效例如伤害提升 10%。被动条件升级通过某种条件触发例如击杀数累积后自动回血。金币升级系统重点管理第一类和第二类。被动条件升级通常挂在敌人或战斗流程的监听事件上不直接在商店面板里配置。4.3 升级项的数据结构升级项至少需要这些字段字段说明id升级项唯一标识名称显示名描述显示在商店面板的文本类型全局永久 / 局内即时基础价格第一次购买价格价格增长系数每次购买后价格按倍率增长最大等级0 表示无限当前等级存档中记录效果数值每级提供的具体数值把数据结构定义成 ScriptableObject 或者普通类都可以重点是不要散落在一个巨大的配置类中。5. 数据层Model 设计QFramework 的架构中Model 负责持有数据数据变化后通过事件通知表现层。金币升级系统这里可以拆出两个 ModelCoinModel 和 UpgradeModel。5.1 Model 的基础接口QFramework 中 Model 通常实现 IModel 接口并在 Architecture 中注册。下面写出通用结构public interface ICoinModel : IModel { BindablePropertyint CoinCount { get; } void AddCoin(int count); bool TrySpendCoin(int count); }使用BindablePropertyint而不是普通int是为了让“金币数量变化”这件事可以被 UI 订阅。UI 不需要主动轮询金币数量数据一变绑定属性自动通知订阅者刷新。5.2 CoinModel 实现public class CoinModel : AbstractModel, ICoinModel { public BindablePropertyint CoinCount { get; } new BindablePropertyint(0); protected override void OnInit() { // 可以在初始化时设置初始值例如从存档恢复 } public void AddCoin(int count) { if (count 0) return; CoinCount.Value count; } public bool TrySpendCoin(int count) { if (count 0) return true; if (CoinCount.Value count) return false; CoinCount.Value - count; return true; } }CoinModel 只负责金币数值变化不管金币来自哪个敌人、金币飞到哪里的动画。动画属于表现层可以由事件驱动生成飞行金币效果但不要把钱数加减写死在特效脚本里。5.3 UpgradeModel 设计升级项的当前等级和价格计算可以沿用同一个 Model 管理。public interface IUpgradeModel : IModel { Dictionarystring, UpgradeState UpgradeStates { get; } int GetCurrentLevel(string upgradeId); int GetUpgradePrice(string upgradeId); bool IsMaxLevel(string upgradeId); void LevelUp(string upgradeId); }UpgradeState 定义当前等级的本地状态[Serializable] public class UpgradeState { public string id; public int level; }这里不把基础配置放进来。基础配置属于静态数据用 ScriptableObject 表承载UpgradeModel 只存“玩家当前拥有哪些等级”。这样做的目的是配置表可以随意调整数值不会破坏存档存档里只记录 id 和 level版本升级后配置表变了旧存档也能兼容。5.4 全局升级与局内升级的区分全局永久升级和局内即时升级在 Model 层可以共用一套 UpgradeModel但保存时机不同。全局升级购买后立即写入持久存档。局内升级购买后只修改当前局的内存状态玩家死亡时丢弃。实现时可以在 UpgradeState 上加一个isGlobal字段并由升级配置表决定。[Serializable] public class UpgradeState { public string id; public int level; public bool isGlobal; }局内升级购买时走内存更新全局升级则额外标记脏数据关卡结算时统一保存。6. 逻辑层System 设计Model 只存数据真正的业务规则放在 System 中。金币升级系统的业务规则主要有敌人死亡掉金币。玩家拾取金币。购买升级项。价格计算。升级效果应用到角色属性。6.1 CoinSystemCoinSystem 负责“金币从战斗产生到进入 CoinModel”的完整链路。public interface ICoinSystem : ISystem { void OnEnemyDie(Vector3 position, EnemyConfig config); void OnCoinPickedUp(int pickUpAmount); }public class CoinSystem : AbstractSystem, ICoinSystem { private ICoinModel _coinModel; protected override void OnInit() { _coinModel this.GetModelICoinModel(); } public void OnEnemyDie(Vector3 position, EnemyConfig config) { if (config null) return; int dropCount config.coinDropCount; // 生成掉落物表现实际金币数值等拾取后再结算 this.SendEvent(new EnemyCoinDroppedEvent(position, dropCount)); } public void OnCoinPickedUp(int pickUpAmount) { if (pickUpAmount 0) return; _coinModel.AddCoin(pickUpAmount); this.SendEvent(new CoinChangedEvent(_coinModel.CoinCount.Value)); } }把事件从OnEnemyDie抛出去是希望表现层和数值层解耦。掉落物由表现系统生成真正加钱只发生在拾取瞬间。6.2 UpgradeSystem升级行为的核心逻辑集中在 UpgradeSystem 中public interface IUpgradeSystem : ISystem { bool TryUpgrade(string upgradeId); }public class UpgradeSystem : AbstractSystem, IUpgradeSystem { private ICoinModel _coinModel; private IUpgradeModel _upgradeModel; protected override void OnInit() { _coinModel this.GetModelICoinModel(); _upgradeModel this.GetModelIUpgradeModel(); } public bool TryUpgrade(string upgradeId) { if (_upgradeModel.IsMaxLevel(upgradeId)) return false; int price _upgradeModel.GetUpgradePrice(upgradeId); if (_coinModel.TrySpendCoin(price) false) { this.SendEvent(new UpgradeFailedEvent(upgradeId, 金币不足)); return false; } _upgradeModel.LevelUp(upgradeId); this.SendEvent(new UpgradeSucceededEvent(upgradeId, _upgradeModel.GetCurrentLevel(upgradeId))); return true; } }从这段可以看出UI 点击购买时不需要直接操作 CoinModel也不需要自己判断价格。UI 只向 System 发起请求由 System 完成校验、扣钱、升级、发送事件。6.3 升级效果应用升级效果应用可以在 UpgradeSucceededEvent 的监听中完成。例如提升伤害public class DamageUpgradeHandler : ICanRegisterEvent { public void OnUpgradeSucceeded(UpgradeSucceededEvent e) { if (e.UpgradeId ! damage_up) return; // 读取配置表把对应攻击力加成应用到 PlayerAttribute } }这种设计进一步把“金币系统”和“战斗属性”解耦。金币系统不知道伤害计算细节伤害系统也不直接依赖金币。7. 事件驱动架构解耦的关键金币升级系统的数据流是战斗逻辑产生金币 - 金币数量变化 - 多个 UI 面板刷新同时可能触发音效、飘字、特效。如果这些模块都直接引用同一个静态类后期维护会非常痛苦。QFramework 事件系统把“通知”和“处理”拆开。发送方不需要知道自己被谁监听监听方也不需要在发送方代码里注册。7.1 定义事件类在金币升级系统里建议定义以下事件public struct CoinChangedEvent { public int CurrentCoin; public int ChangeAmount; } public struct UpgradeSucceededEvent { public string UpgradeId; public int NewLevel; } public struct UpgradeFailedEvent { public string UpgradeId; public string Reason; }用 struct 定义事件对象分配压力低于 class在频繁触发的战斗场景更友好。7.2 发送与监听在 QFramework 里事件的发送可以使用this.SendEvent监听使用this.RegisterEvent。CoinModel 中的数据变化其实已经通过 BindableProperty 被 UI 订阅但业务层事件的意义是传递更完整的上下文例如携带 ChangeAmount、原因、目标 UI 等。建议的使用方式是BindableProperty 负责基础数据绑定UI 直接刷新文本。业务事件负责跨模块通知例如升级成功后刷新角色属性面板、播放音效、飘字。两者并存互不冲突。7.3 避免事件滥用事件系统容易走向一个极端所有方法调用都用事件代替。这会带来两个问题事件流难以阅读IDE 里搜不到调用链。某个事件到处都是监听者性能开销和调试成本上升。针对金币升级系统我的建议是跨模块、跨 UI 面板用事件。同模块内部、Controller 调用 System 方法用普通方法调用。每个事件至少写清楚触发时机不要在 Update 里高频发事件。8. UI 层UIKit 面板与 HUD金币 UI 主要包括局内 HUD 右上角的金币数量。商店/升级面板。飘字提示获得金币、金币不足。升级成功后的属性变化反馈。QFramework 的 UIKit 可以统一管理面板的加载、显示、隐藏和释放。8.1 绑定金币数量文本在 HUD 面板中金币文本可以直接订阅 CoinModel 的 BindablePropertypublic class CoinHUDWidget : MonoBehaviour { public Text coinText; private ICoinModel _coinModel; private void Start() { _coinModel this.GetModelICoinModel(); _coinModel.CoinCount.Register(OnCoinChanged); OnCoinChanged(_coinModel.CoinCount.Value); } private void OnDestroy() { if (_coinModel ! null) { _coinModel.CoinCount.UnRegister(OnCoinChanged); } } private void OnCoinChanged(int coin) { if (coinText ! null) { coinText.text coin.ToString(); } } }这里注意面板销毁时一定要 UnRegister否则会因持有已销毁对象造成空引用或内存泄漏。8.2 升级商店面板升级商店面板会有升级项列表、购买按钮、金币余额显示。每个列表项建议单独做成 UIWidget每个 Widget 负责绑定一个升级项的状态。点击购买按钮时调用 UpgradeSystem.TryUpgradepublic class UpgradeItemWidget : MonoBehaviour { public Text nameText; public Text levelText; public Text priceText; public Button buyButton; private string _upgradeId; public void Setup(string upgradeId) { _upgradeId upgradeId; Refresh(); } public void OnBuyButtonClicked() { this.GetSystemIUpgradeSystem().TryUpgrade(_upgradeId); } public void Refresh() { var upgradeModel this.GetModelIUpgradeModel(); int level upgradeModel.GetCurrentLevel(_upgradeId); int price upgradeModel.GetUpgradePrice(_upgradeId); levelText.text $Lv.{level}; priceText.text price.ToString(); } }购买成功后可以通过刷新所有 Widget 来同步显示。比较简单的实现是打开商店时刷新一次收到 UpgradeSucceededEvent 时再刷新当前列表。8.3 面板打开关闭控制UIKit 打开面板UIKit.OpenPanelUpgradeShopPanel();关闭面板UIKit.ClosePanelUpgradeShopPanel();面板的加载路径和预设体配置在 UIKit 的配置表中管理。新项目建议把面板 Prefab 放在Resources/UI下或按 UIKit 推荐的路径目录放置先用默认模式跑通再考虑 Addressables。9. 存档与全局升级金币升级系统中存档问题绕不开。建议拆成两部分局内数据不存档退出即丢。全局数据金币总额、全局永久升级等级、解锁状态。9.1 存档数据结构[Serializable] public class PlayerSaveData { public int totalCoin; public ListUpgradeState globalUpgrades; }序列化时直接存 List 和基础类型不要直接序列化 Model 对象避免格式耦合。9.2 保存时机全局升级购买成功后保存。关卡结算时保存。应用切后台时保存。关键节点手动调用刷新。不要每局每帧都写存档。类幸存者游戏一局往往十几分钟甚至几十分钟频繁写文件会造成卡顿实际也没必要。9.3 加载时机游戏启动时先创建 Architecture 并注册 Model然后从存档文件读取数据恢复到 Model 中。QFramework 的 Architecture 初始化代码大致是public class GameApp : ArchitectureGameApp { protected override void Init() { this.RegisterModelICoinModel(new CoinModel()); this.RegisterModelIUpgradeModel(new UpgradeModel()); this.RegisterSystemICoinSystem(new CoinSystem()); this.RegisterSystemIUpgradeSystem(new UpgradeSystem()); } }在游戏入口处调用GameApp.Interface.LoadSaveData(data)把存档数据注入 Model 即可。10. 资源占用与分析虽然这是纯游戏逻辑代码但性能问题仍然值得注意。10.1 内存占用金币升级系统本身的代码量很小。主要内存开销来自升级项配置表如果使用 ScriptableObject每个升级项占用较小。升级商店逐帧刷新时可能产生的字符串拼接。避免的方式文本拼接用string.Format或字符缓冲避免高频创建小字符串。不要在 Update 中每帧刷新金币文本只在数据变化事件中刷新。面板关闭后及时释放避免 UIKit 面板常驻。10.2 GC 压力战斗场景中敌人死亡会频繁触发生成掉落物、加点、飘字。每帧都创建大量临时对象会带来 GC Alloc长时间战斗后会出现卡顿。建议掉落物使用对象池金币数值变化时复用 UI 文本组件。事件对象用 struct 而不是 class。飘字效果限制同时存在的数量超出数量的回收复用。10.3 框架层开销QFramework 的模型和系统注册本身是一次性开销。事件发送是小开销但如果一帧发送上千个事件仍会形成压力。例如掉落物逐个拾取时可以考虑把多次拾取合并成一次 CoinChangedEvent。11. 常见问题与排查方法开发金币升级系统过程中常见问题大致如下问题现象可能原因排查方式解决方案金币 UI 不刷新UI 未订阅 CoinCount或订阅后未 UnRegister打断点在 OnCoinChanged检查是否进入在 Start 中 Register在 OnDestroy 中 UnRegister价格计算不对基础价格或增长系数设置错误打印 GetUpgradePrice 返回值核对配置表字段购买后没有效果升级效果未监听 UpgradeSucceededEvent搜索升级 id 的监听代码注册对应 Handler面板打开空白UIKit 面板未放入 Resources 或路径配置缺失检查控制台报错检查面板 Prefab 路径存档恢复后金币丢失加载顺序在 Model 注册之前检查调用顺序先注册 Architecture再加载数据反复打开商店卡顿面板每次都重新加载资源打开 Profiler 查看加载耗时开启 UIKit 面板复用或缓存退后台后进度丢失未在 OnApplicationPause 保存添加监听在暂停时触发保存敌人掉金币后数值不增加拾取判定没走 CoinSystem 方法检查触发链确保拾取逻辑调用 OnCoinPickedUpIL2CPP 打包后存档读取异常序列化类型支持问题真机日志查看异常检查存档类是否可序列化、字段类型是否支持Unity 报许可证错误Unity 许可证未激活打开 Unity Hub 查看许可证状态登录并激活许可证12. 最佳实践与扩展方向12.1 配置优先把升级项做成数据驱动而不是硬编码。新加一个升级项时只需要新增一条配置并实现对应的效果处理函数。金币系统的核心代码不需要改动。12.2 效果抽象升级效果建议抽象成接口public interface IUpgradeEffect { void Apply(PlayerAttribute playerAttribute); }例如伤害升级、移速升级、最大生命升级各自实现一个 IUpgradeEffect 类升级 System 根据 upgradeId 到效果容器里拿实例。12.3 批量扩展如果升级项数量很多可以用编辑器工具批量生成配置#if UNITY_EDITOR using UnityEditor; public static class UpgradeConfigTool { [MenuItem(Tools/金币系统/批量创建默认升级配置)] public static void BatchCreateDefaultConfig() { // 读取一个 ScriptableObject 配置表批量生成对应资产 } } #endif这里的批量扩展侧重开发期效率不影响运行时性能。12.4 合规与安全提醒金币系统一旦接入支付、排行榜或联机交易必须注意客户端金币不能作为权威数据。涉及用户真实资产时必须在服务端校验。使用他人素材前确认授权。测试时不要把测试数据误当正式存档。独立游戏建议先做本地单机经济循环再把数据接入服务端。12.5 下一步当前这套金币升级系统已经覆盖了“敌人掉落 - 金币积累 - 商店购买 - 效果生效 - 存档写入”的完整链路。后面可以继续扩展局外科技树做成树形节点解锁。广告复活/双倍金币通过事件接口插入。局内随机升级三选一与现有价格模型互补。多角色存档隔离按角色维度保存全局升级。如果你刚接触 QFramework先不要急着把所有模块都迁过去。用一个金币升级系统做试点跑通一次完整数据流再逐步把战斗、角色、背包都纳入同一套架构里。这套流程走完类幸存者项目后续加内容时你会发现最花时间的已经不是“怎么改代码”而是“怎么才能不破坏已经跑通的经济循环”。