15.Unity基础框架总结

  1. 15.总结
    1. 15.1 知识点
      1. 知识点回顾
      2. 回顾概述内容
      3. 总结
    2. 15.2 核心要点速览
      1. 模块总览
      2. 单例
      3. MonoMgr
      4. 对象池 PoolMgr
      5. 事件中心 EventCenter
      6. 资源管理
        1. ResMgr(Resources)
        2. 其它管线
      7. 音效 MusicMgr
      8. UI:BasePanel 与 UIMgr
        1. BasePanel
        2. UIMgr
      9. 场景 SceneMgr
      10. 输入 InputMgr
      11. 计时器 TimerMgr
      12. 加密 EncryptionUtil
      13. 文本 TextUtil
      14. 数学 MathUtil
    3. 15.3 面试题精选
      1. 基础题
        1. 1. 泛型单例为什么要用反射创建实例,而不是 new() 约束后 new T()?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. MonoMgr 解决的是什么问题?注册回调时要注意什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 事件中心为什么用 EventInfoBase 字典配合 EventInfo / EventInfo<T>?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. 切场景时为什么要先 ClearSound 再清对象池?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. 双重检查锁定里为什么要判断两次 instance == null?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. GameObject 池空闲区用 Stack、池满时又从在用链表头抢占,这两套逻辑各解决什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. UIMgr 里 PanelInfo.isHide 主要挡的是哪种异步竞态?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 纯 C# 单例加锁、Mono 单例不加锁,工程上怎么判断?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 客户端里做内存异或加密,边界在哪里?还要配什么手段?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

15.总结


15.1 知识点

知识点回顾

回顾概述内容






总结


15.2 核心要点速览

模块总览

  • 单例与 Mono 调度打底,往上叠对象池、事件、资源,再接到音效、UI、场景、输入、计时器;文本、加密、数学是横切工具。
类别 模块
基础设施 单例、MonoMgr
运行时常用 对象池、事件中心、资源、音效、UI、场景、输入、计时器
数据与工具 文本、加密、数学

单例

  • 目标只有一个:外部只通过 Instance 拿对象,不要在外面再 new 出第二份。教程里三条线:Singleton<T>SingletonMono<T>SingletonAutoMono<T>
基类 典型场景 实现上别漏
Singleton 纯 C# 管理器,不挂场景 子类 private 无参构造;基类反射 NonPublic 构造;多线程首次访问就加双检锁
SingletonMono 想先在场景里摆承载物 FindObjectOfType,没有就建;Awake 抢实例、DontDestroyOnLoad、重复则 Destroy(gameObject)
SingletonAutoMono 需要 Mono 生命周期又不想手摆 写法接近上者,语义是懒创建、自动挂物体
  • new() 约束若配公开构造,谁都能 new T(),单例名存实亡;教程走私有构造 + 反射。
  • Mono 单例默认只在主线程碰引擎 API,一般不加锁;若有子线程抢 Instance,要单独看,别背「Mono 永不加锁」当教条。
// 纯 C# 懒初始化:外判空省锁,内判空防并发下重复创建
if (instance == null)
{
    lock (LockObject)
    {
        if (instance == null)
        {
            // 反射调用 T 的私有无参构造
        }
    }
}

MonoMgr

  • 不想每个管理器各挂一个 MonoBehaviour 只为蹭 Update:Hierarchy 难看,引擎更新登记也多。MonoMgr 自己跑三个生命周期,外面注册 UnityAction 分发,相当于项目里自管的一小段帧循环。
  • 协程就是宿主上的 StartCoroutine,没有额外封一层「MonoMgr 协程 API」。
说明
注册 AddUpdateListener / RemoveUpdateListener 成对;只加不移除会变成野回调,对象逻辑上不用了仍被每帧调用
协程 起停要和模块生命周期对齐;StopAllCoroutines 会断掉所有挂在本宿主上的协程,通常只用于关游戏或整包卸载

对象池 PoolMgr

  • 抽屉模型:一个 key 一个池,高频 Instantiate / Destroy 换成借还,省分配和 GC 压力。
概念 行为
GameObjPool 空闲区 StackPop / Push,刚还的先被拿走,和官方 ObjectPool 习惯一致
GetObj 栈非空 Pop,否则 Instantiate,再 SetActive(true);开布局时取出后 SetParent(null)
ReturnObj SetActive(false)Push;开布局时挂回池根
容量上限 _usedList 记在用;满时从链表头拿最早借出的实例抢占;归还要从链表摘掉
  • ObjPool<T>Queue 管引用;new T() 复用会带脏字段,在 OnGetObject / OnReturnObject 里洗掉。
  • ObjPoolMgrDictionary<Type, object>,支持 RegisterPool 注入自定义池。
  • GameObjPoolMgr.isOpenLayout:调试把失活物体归到 GameObjPoolRoot;发包可关,少做 SetParent
  • LoadPrefab 抽象掉;默认 Resources.Load,线上换 AB、Addressables 等按项目键名约定。

事件中心 EventCenter

  • 用枚举 E_EventType 比裸字符串好重构;字典里用 EventInfoBase 装桶,里面实际是无参 EventInfo 或有参 EventInfo<T>
结构 作用
无参 / 有参两条包装 无参侧挂 UnityAction;有参侧挂带类型参数的 UnityAction,由泛型包装类承载
EventInfoBase 同一字典值类型;触发时按 T 转回具体 EventInfo<T>Invoke
  • 同一枚举成员别混挂不同 Tas 失败容易空引用;约定一成员对应一种载荷。
  • Clear() 清全表;Clear(E_EventType) 清单路。监听挂在 MonoBehaviour 上要在 OnDestroyRemove
  • 后续可演进:struct / 接口载荷、订阅 Token,减轻「必须同一委托引用才能 -=」的负担。

资源管理

  • 大量资源长期放 Resources 会撑包体、难控常驻内存;教程用它讲同步、异步、合并请求、引用计数。上线多走 AB、Addressables、YooAsset。

ResMgr(Resources)

能力 要点
同步 Resources.Load
异步 MonoMgr 协程里 yield returnResourceRequest
合并请求 path + "_" + typeof(T).Name 作 key,同路径同类型共一条协程,结束统一回调
引用计数 Load / LoadAsync 加引用,UnloadAsset 减;到 0 且允许再 UnloadAsset;异步未完成别硬卸
同步打断异步 StopCoroutine 后同步 Load:已跑异步作废,实现简单,CPU 可能白跑

其它管线

备注
EditorResMgr 仅编辑器 AssetDatabase.LoadAssetAtPath,路径从 Assets/
ABMgr abDic[name] == null 表示异步占位;协程里等到非 null;失败必须删占位打日志,否则死等
Android / WebGL StreamingAssets 常在包内压缩;示例 UnityWebRequestAssetBundle,或首启拷到 persistentDataPathLoadFromFile
ABResMgr 编辑器 isDebug 可走 EditorResMgr,少打 AB 也能调
UWQResMgr 按类型分支拉数据;用完在对应路径 Dispose

音效 MusicMgr

  • BGM 单独一个 AudioSourceloop 和音量单独管。音效列表在 FixedUpdate 里扫:非循环且停了就回收;池化路径里 clip = nullReturnGameObj,比反复 Destroy 少 GC 尖峰。
  • soundList.Contains 防同一音源重复进表。循环音要玩法或 StopSound 显式关。3D 在回调里设 spatialBlendmin/maxDistancerolloffMode;跟单位就 SetParent
顺序 原因
ClearSound,再清对象池 soundList 指着池里物体;先 PoolMgr.Clear 会把物体销毁,列表变悬垂,后面遍历必炸

UI:BasePanel 与 UIMgr

BasePanel

  • AwakeGetComponentsInChildren<T>(true),用节点名做 Dictionary<string, UIBehaviour> 的 key。
  • defaultNameList 把纯装饰 Image、TMP 文本滤掉,避免当成交互控件。
  • Button / Slider / Toggle 自动绑到 ClickBtnSliderValueChangeToggleValueChange,子类按控件名 switch

UIMgr

行为 说明
初始化 UI 相机、Canvas、EventSystemDontDestroyOnLoad;子层 Bottom / Middle / Top / System
ShowPanel 类名与预制体名一致;panel == null 表示加载中,重复 Show 只叠回调;HideisHide 并清回调,若加载完已隐藏则不实例化并删字典项
HidePanel true 销毁并移出字典
HidePanel false 仅失活,下次打开省 Awake,占常驻内存,按机型和界面频率用 Profiler 权衡
  • AddCustomEventListener:每控件一个 EventTrigger,动态加 Entry,给 Image 等补指针类事件。

场景 SceneMgr

路径 注意
异步 MonoMgr 协程 yield return AsyncOperation;循环发 ao.progress,结束前再手动发 1f,减轻条卡在 0.9x(和 allowSceneActivation 有关);「加载完成」仍建议业务单独收口
同步 LoadScene 当前场景立刻卸掉;同脚本后续代码若依赖旧场景物体,往往已无效
预加载 LoadSceneAsync + allowSceneActivation = false,配合 priority、RT 转场,要和相机、UI 一起设计
示例骨架 LoadSceneWithCleanup 若调 HideAllPanels,工程里没有就自实现或遍历 HidePanel

输入 InputMgr

  • CheckKeyCode 挂在 MonoMgr 的 Update,集中读键鼠后 EventTrigger;改键逻辑也集中在一处。
  • ChangeKeyboardInfo / ChangeMouseInfo 先按旧类型从字典 RemoveAdd,避免同一事件键鼠两条记录互踩。
  • GetInputInfo 首帧返回 null:用 isStart 跳过第一帧,减轻改键当帧读写交错;不能接受空窗就改成立即刷新字典或返回缓存配置。
// 首帧跳过:改键当帧字典可能处于中间态
public InputInfo GetInputInfo(E_EventType type)
{
    if (!isStart)
    {
        isStart = true;
        return null;
    }
    // 再查 Dictionary …
}

计时器 TimerMgr

  • 固定步长(示例 0.1f)的 while(true) 协程,轮询「游戏时间」「真实时间」两套字典;到期先把项记入 delList,本轮结束再 Remove,避免遍历字典时改集合。
  • TimerItem 可走对象池减分配。游戏时间用 WaitForSeconds(吃 timeScale),真实时间用 WaitForSecondsRealtime
  • 类里 const float intervalTime 必须写在引用它的 WaitForSeconds 字段初始化之前,否则字段初始化顺序直接编译报错。
  • 两套循环共用一个 delList 容易在写入和消费上打架;上线更干净是每字典独立延迟删除列表,或单协程顺序处理两个字典。
说明
驱动 固定间隔 tick + 双字典
删除 延迟删除列表,避免 foreach 中 Remove
暂停 游戏时间 vs 真实时间两套语义不要混用

加密 EncryptionUtil

  • 定位是抬高内存修改器搜明文成本,不是密码学意义上的安全。固定几步异或和加减 key,UnLockValue 逆序撤销;GetRandomKey 让搜固定模式难一点。
  • 属性对外像普通字段,get 解密 set 加密。密文 0 直接返回是「槽位未写」的约定;若合法密文也可能为 0,要换哨兵或 bool 标志。
  • 经济、存档仍以服务端校验、签名为准;本地加密只是纵深里的一层。
说明
算法 可逆变换,加解密互逆
密文 0 与「未初始化」混淆风险,设计时要分开
预期 拖慢随手改内存,不能替代服务端权威

文本 TextUtil

  • 静态工具:拆配置、补零、时间串、大数缩写。SplitStr 先把全角分号逗号冒号换成半角再按 type 拆,避免策划表导出全角导致 Split 整行不切开。
  • SplitStrToIntArr 直接 Parse,脏表会抛,线上常加 TryParse。两轮拆时内层不足两段就 continue,防 ints[1] 越界。
  • 静态共享 StringBuilder:主线程顺序、无嵌套调用同一路径时问题不大;嵌套或多线程改局部 builder、[ThreadStatic] 或小池。
场景 常用 API
配表 id,count 形式 SplitStrToIntArrTwice
倒计时 UI SecondToHMS2
战力金币展示 GetBigDataToString(展示用,不是精度计算)

数学 MathUtil

  • Deg2Rad / Rad2Deg 包一层 Mathf,少写反乘除。GetObjDistanceXZ / XY 先把 yz 抹零再 Distance,分别是水平面和 2D 平面距离。
  • IsWorldPosOutScreen 只看屏幕矩形不够:目标在相机后方时 z 常为负,x/y 仍可能落在矩形内;要看 screenPos.z 或视锥;多相机别写死 Camera.main
  • IsInSectorRangeXZangle 是整扇开口,和 forward 夹角用半角 angle * 0.5fforward 与坐标同一空间。

射线与 OverlapBox / OverlapSphereRayCast 重载回调可能是 HitGameObject 或组件;没有组件时回调 nullOverlap*typeof(T) 分支 Collider、GameObject、其它组件,统一传 layerMaskQueryTriggerInteraction.Collide

// XZ 扇形:半径 + 相对 forward 的半角
return Vector3.Distance(pos, targetPos) <= radius
    && Vector3.Angle(forward, targetPos - pos) <= angle * 0.5f;

15.3 面试题精选

基础题

1. 泛型单例为什么要用反射创建实例,而不是 new() 约束后 new T()

题目

new() 约束和「禁止外部 new」能不能同时成立?子类构造应该怎么写?

深入解析
  • new() 约束通常要求可访问的无参构造,编译器允许在任何地方 new T(),单例入口和外部构造并存,约束落空。
  • 子类改成 private 无参构造后,基类常规语法调不到,只能用反射取 NonPublic 构造 Invoke 一次。
  • 子类若漏写私有无参构造,GetConstructor 拿不到会直接抛,属于配置错误尽早暴露。
答题示例

目的是封死外部 new,只留 Instancenew() 约束会逼出公开构造,外面还能造第二份。

子类私有构造后,基类用反射调非公开无参构造创建唯一实例。

参考文章
  • 2.单例模块

2. MonoMgr 解决的是什么问题?注册回调时要注意什么?

题目

为什么不建议每个管理器各自挂一个 MonoBehaviourUpdate

深入解析
  • 每个 MonoBehaviour 都会在引擎侧登记更新;管理器一多,Hierarchy 和调度成本都难看。
  • MonoMgr 只跑一份 Update / FixedUpdate / LateUpdate,用委托分发给纯 C# 逻辑;协程也借同一个宿主 MonoBehaviour
  • AddUpdateListenerRemoveUpdateListener 必须成对,否则野回调每帧空转;StopAllCoroutines 会断掉所有挂在本宿主上的协程。
答题示例

把帧循环集中到一个 Mono 上分发,比一堆脚本各自 Update 干净。

注册和注销要成对;协程起停也要和模块生命周期对齐,别用 StopAllCoroutines 当日常手段。

参考文章
  • 3.公共Mono模块

3. 事件中心为什么用 EventInfoBase 字典配合 EventInfo / EventInfo<T>

题目

能不能直接用 Dictionary<E_EventType, Delegate> 存所有事件?

深入解析
  • 无参、有参、不同载荷类型的委托不是同一种静态类型,塞进非泛型 Delegate 会丢具体签名,触发时难以类型安全地 Invoke
  • EventInfoBase 做统一容器,实际装 EventInfoEventInfo<T>,触发时再按 T 转回,监听端可以用 UnityAction<T>,减少全程 object 和装箱。
  • 代价:同一枚举成员不能混挂多种 T,否则 as 失败;项目里宜约定「一成员一种载荷」。
答题示例

一个字典要同时装无参、有参和不同参数类型,用基类指针装桶,触发时再按泛型转回去。

这样监听方法签名在编译期能对上;别在同一枚举值上混用不同载荷类型。

参考文章
  • 5.事件中心模块

4. 切场景时为什么要先 ClearSound 再清对象池?

题目

顺序反了会发生什么?

深入解析
  • soundList 保存的是音效所在的 GameObject;若先 PoolMgr.Clear 把池里物体销毁,列表里引用变悬垂,后续遍历停播或回收会崩。
  • ClearSound 先停播、clip = null、归还并清空列表,让管理器状态和场景里实际物体一致,再动池字典。
答题示例

音效列表还指着池里的物体,先清池等于先把物体干掉,列表变成野指针。

先把音效管干净,再清对象池。

参考文章
  • 7.音效模块

进阶题

1. 双重检查锁定里为什么要判断两次 instance == null

题目

多线程懒初始化单例,少一次判断会怎样?

深入解析
  • 外层判空:实例已存在则不进 lock,减少锁竞争。
  • 内层判空:多个线程可能先后通过外层并在锁外排队;第一个线程创建完后,后续线程拿到锁必须再判空,否则会再执行一次反射创建,破坏唯一性。
  • 双检锁主要服务纯 C# 懒初始化;Mono 单例多数只在主线程访问 Instance,一般不照搬同一套锁策略。
答题示例

外层判空是为了有了实例就别抢锁;内层再判是因为等锁期间可能已经被别的线程创建好了。

纯 C# 懒单例常用双检锁;Mono 单例要另看线程模型。

参考文章
  • 2.单例模块

2. GameObject 池空闲区用 Stack、池满时又从在用链表头抢占,这两套逻辑各解决什么?

题目

空闲区能不能换成 Queue?抢占和「热实例复用」是不是一回事?

深入解析
  • 空闲 Stack 后进先出:刚归还的实例下一轮最先被取走,局部性更好,也和 Unity 官方 ObjectPool 用栈的习惯一致;Queue 会先拿到池里躺最久的实例,多数项目收益不明显。
  • 池满时从 _usedList 链表头拿「最早借出」的对象,是容量上限下的抢占策略,和空闲栈的复用顺序无关;抢占等于可能强行复用仍在业务手里的对象,需要规范或 OnPreemptObject 等配套。
  • 引用类型小对象池仍要在 OnGet / OnReturn 洗字段,否则复用带脏状态。
答题示例

空闲用栈是为了优先复用刚还回来的热实例;链表头抢占是池满时的另一套策略,别和栈混成一句话。

抢占时可能抢还在用的对象,业务要么别长期私自缓存池对象,要么在抢占回调里做重置和通知。

参考文章
  • 4.对象池模块

3. UIMgrPanelInfo.isHide 主要挡的是哪种异步竞态?

题目

加载过程中用户已经关了界面,没有 isHide 会怎样?

深入解析
  • 异步加载未完成时若用户已关闭界面,没有标志位,加载完成回调仍可能 Instantiate 并进字典,浪费资源且可能闪屏。
  • Hide 时置 isHide 并清回调;加载结束若已隐藏则移除字典项、不创建实例。再次 Show 可把 isHide 拉回并叠回调。
答题示例

异步面板最怕「加载完了但用户已经不要了」;isHide 让加载完成分支能直接放弃实例化。

再次打开时把标志和回调接回去即可。

参考文章
  • 8.UI模块

深度题

1. 纯 C# 单例加锁、Mono 单例不加锁,工程上怎么判断?

题目

什么情况下你会给 Mono 单例也加锁,或给纯 C# 单例省锁?

深入解析
  • 判据是是否存在多个线程可能首次访问 Instance 的路径(网络、线程池等);有则纯 C# 侧倾向双检锁等方案。
  • Unity 引擎 API 须在主线程调用,给 Mono 单例加锁并不能解决「工作线程直接调 Unity API」的问题,正确做法是切回主线程执行。
  • 子线程只读、且数据不可变的配置型单例是少数例外;公共框架库往往宁可保守。
答题示例

会不会多线程同时第一次碰 Instance,决定纯 C# 要不要锁。

Mono 单例问题多半在「谁在线程上调引擎 API」,靠加锁治标不治本;该主线程派发就派发。

参考文章
  • 2.单例模块

2. 客户端里做内存异或加密,边界在哪里?还要配什么手段?

题目

能不能靠客户端加密替代服务端校验?

深入解析
  • 算法和内存都在玩家机器上,会逆向的人仍能还原或改调用点;异或层主要提高「搜明文」成本,不是密码学意义上的保密。
  • 经济、排行、关键进度应以服务端权威为准;本地可叠加签名校验、异常检测、关键行为上报等纵深。
  • EncryptionUtil 里密文 0 当哨兵类似:设计时要分清「未初始化」与「合法密文」,避免误判。
答题示例

客户端加密是拖时间和加门槛,挡不住决心够的逆向,更不能当经济系统主防。

值钱的数据服务端验、存档签名;本地加密只是多层防护里的一层。

参考文章
  • 12.加密模块


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏