这篇文章讲的是工程决策,不是 API 用法。凡是「为什么这么选」的地方都写了代价,凡是「效果如何」的地方都标了怎么测。
一、一页速览
| 项 | 内容 |
|---|---|
| 项目定位 | 教学向 Unity 客户端基础框架,新工程可以直接拿去做骨架 |
| 我的角色 | 独立完成:问题拆解、模块边界划分、编码实现、文档编写 |
| 核心模块 | 单例基类、缓存池、事件中心、Mono 桥接、资源加载 |
| 技术栈 | Unity、C#、泛型与反射、设计模式 |
| 验证方式 | Demo 场景手测 + Unity Profiler 的 GC Alloc 对比 + 边界用例自测 |
| 当前状态 | 持续维护,每补一个模块同步一篇笔记 |
二、背景:为什么要自己写一套
做小 Demo 和练手工程时,同样的问题每次都要重新解一遍:
- 全局访问点:Manager 类越写越多,
Awake里互相GetComponent,初始化顺序全靠猜; - 频繁创建销毁:子弹、飘字、列表项反复
Instantiate/Destroy,Profiler 里 GC Alloc 一路涨,低端机上出现可感知的卡顿; - 模块耦合:UI 直接调战斗逻辑,改一个需求要顺着调用链改五处;
- MonoBehaviour 依赖:工具类、数据类想要
Update或协程,只能被迫继承MonoBehaviour,凭空多出一堆空挂载脚本。
这四件事单独看都不难,难的是每次新工程都要重来一遍,且每次做法都不一样。所以目标很明确:把这部分固定成骨架,让后续项目只需要关心业务。
三、我的职责
- 拆边界:先确定哪些是「所有工程都要用」的通用能力,哪些必须留给业务层,避免框架一口吃成引擎;
- 定接口:每个模块只暴露最小方法集(如对象池只给
Get/Release/Clear),把生命周期钩子收敛到基类; - 实现与文档:五个模块全部手写,同时输出可复制的代码片段与使用说明,保证「换个工程能直接落地」;
- 验证:为每个模块准备最小 Demo 与边界用例,重点测重复注册、忘记归还、异常销毁这类常见误用。
四、技术选择与理由
| 面临的问题 | 我的选择 | 为什么 | 代价 / 取舍 |
|---|---|---|---|
| 全局唯一访问点 | 泛型单例基类 + 统一生命周期钩子 | 子类不必重复写 Awake 与 DontDestroyOnLoad,初始化时机统一 | 静态引用不利于单测,需要保留手动清理入口 |
| 频繁创建销毁 | 按 Prefab 分桶的对象池 | 复用实例,削掉运行时的 GC 峰值 | 归还时必须重置状态,漏了会出「幽灵数据」 |
| 模块间通信 | 事件中心(以类型为 key 的注册表) | UI 与逻辑解耦,新增监听方不需要改发送方 | 调用链变隐式,需要额外的调试输出才能追踪 |
非 Mono 类要 Update / 协程 | Mono 桥接:单例驱动 Update 队列 + 协程宿主 | 工具类、数据类不用继承 MonoBehaviour | 执行顺序由桥接统一调度,必须显式约定优先级 |
| 资源加载 | 统一的加载封装(同步 + 异步) | 调用方不直接碰 Resources,方便日后替换实现 | 当前实现基于 Resources,不适用于热更新场景 |
一个具体的取舍例子:事件中心最初打算用 string 作事件名,好处是调试时能直接打印名字;最后改成以类型为 key,因为字符串拼错编译器不会报错——这类错误往往上线才暴露。代价是需要在事件中心里维护一张映射表,用来输出可读的事件名。
五、结构
分层上刻意保持扁平:单例与桥接在底层提供运行环境,缓存池与事件中心是中层的通用设施,资源加载负责与外部资源打交道。业务代码只允许依赖中层的接口,不允许反过来让框架知道业务。
六、验证结果
验证不是「跑起来没报错」,而是每条主张都有对应测法:
| 主张 | 怎么测 | 结果 |
|---|---|---|
| 对象池能消掉运行时 GC 峰值 | 同一 Prefab 连续生成/销毁 1000 次,对比开启与关闭对象池时 Profiler 的 GC Alloc | 待补:采集截图与数值 |
| 单例不会重复实例化 | 场景内故意挂两个同类 Manager,检查是否有告警且只保留一个 | 待补:补充日志截图 |
| 事件中心不会泄漏 | 注册后销毁对象,检查注册表长度是否回落 | 待补:补充数据 |
| 非 Mono 类能拿到逐帧更新 | 用一个纯 C# 计时器类驱动 UI 倒计时,与 MonoBehaviour.Update 结果对比 | 待补:补充对比结论 |
这一节的数值目前是空的。没有实测数据就不写结论——填上实测截图与数字之后再改这段说明。
七、复盘
- 对象池的归还重置最容易漏:早期只重置位置和速度,结果复用的对象带着上一次的血条和动画状态,排查花的时间比写池子还长。后来把重置收敛成一个虚方法,由使用方强制实现。
- 事件中心缺少注销约定:一开始靠使用方自觉,实际几乎没人记得,于是把注销挂到对象的销毁流程里。
- 框架的边界比功能更重要:最初想把音频、存档也塞进来,后来发现这些在不同项目里差异太大,塞进框架反而变成负担,最终只保留其中最通用的五个模块。
八、相关文章
这个框架的每个模块都对应一篇笔记,按顺序读能看清演进过程:
- 【Unity】Unity 程序基础小框架:模块总览与代码片段
- 【Unity】Unity YAML:场景与 Prefab 的序列化格式,排查资源问题时用得上
- 【Unity】UGUI 优化:UI 侧的同类取舍
也可以从 Unity 基础与工具系列 顺着读下去。