29.总结
29.1 核心要点速览
概念与演示前提
Model持数据与规则,View把模型画到控件上,Controller处理输入并决定何时调 Model、何时开/关哪块界面。
| 项 | 内容 |
|---|---|
| 案例操作 | M 主界面、N 关闭;点主角进角色;升级写存档并刷新界面 |
| 工程顺序 | 资源进工程 → 场景里摆 Canvas 和预制 → 分辨率和层级确认无误 → 再写脚本 |
非 MVC 胖面板
MainPanel、RolePanel 一类写法:static 自引用、Resources.Load,挂到 GameObject.Find("Canvas")。首次 ShowMe 里 Instantiate,SetActive(true) 后 UpdateInfo;HideMe 多用 SetActive(false)。若改成 Destroy,等于换了一套生命周期,和「隐藏复用」不是一回事。
数据散落在各面板里 PlayerPrefs 读写,UpdateInfo 把值填进 Text。升级后除了当前面板,往往还要 MainPanel.Panel.UpdateInfo() 这种跨面板静态调用——改需求时引用网会一起动,后面拆 MVC 主要针对这个。
手写 MVC 与数据通知
手写版把「改数」收到 PlayerModel,界面只订阅、只刷新:
PlayerModel:字段私有、对外只读;PlayerModel.Data懒单例,Init读档,LevUp后SaveData。MainView/RoleView:Inspector 拖引用,UpdateInfo(PlayerModel data)只负责把字刷上去,不写升级公式、不写存档。MainController/RoleController:ShowMe/HideMe、按钮回调、向 Model 订阅;升级按钮只调PlayerModel.Data.LevUp(),不直接改别的面板的控件。
| 层 | 职责 |
|---|---|
PlayerModel |
数据与持久化唯一入口 |
MainView / RoleView |
纯显示 |
MainController / RoleController |
流程与输入 |
预制体:MVC 演示走 BaseFramework/UI/UGUI/MVC/;前面「常规」示例在 UGUI/Normal/,路径不要交叉。
Controller.Start 里常见顺序:GetComponent 取 View → 先 UpdateInfo(PlayerModel.Data) → 绑按钮(开角色界面调 RoleController.ShowMe() 等)→ PlayerModel.Data.AddEventListener(UpdateInfo),在私有 UpdateInfo 里转发给两个 View。MVCMain 继续用 M / N 切主界面。
// OnDestroy 里成对 Remove,避免 Controller 没了委托还指着它
void OnDestroy()
{
PlayerModel.Data.RemoveEventListener(UpdateInfo);
}
胖面板与 MVC 演示对照
| 对比 | 胖面板 | MVC 演示 |
|---|---|---|
| 改数据 | 各 Panel 内直接 PlayerPrefs |
只通过 PlayerModel 的方法 |
| 跨界面刷新 | MainPanel.Panel.UpdateInfo() 一类静态互调 |
各 Controller 听 Model,只更新自己的 View |
| 预制目录 | UGUI/Normal/ |
UGUI/MVC/ |
| 脚本 | UI、存档、跳转搅在一起 | View / Controller / Model 各管一截 |
多几个类不是目的;目的是数据入口单一、刷新走事件,面板之间少硬编码引用。项目小可以不服,项目一大这套约束会省很多扯皮。
MVX 变体(同一业务换管道)
MVP、课内 MVVM、MVE 共用同一套界面需求,差别在View 要不要直接认识模型类型,以及通知走 C# 委托还是全局事件管理器。
MVP:MVP_PlayerModel 与手写 PlayerModel 同构。View 尽量只剩控件;Presenter 的 UpdateInfo 里给 txtName 等赋值,并订阅 MVP_PlayerModel.Data,OnDestroy 退订。预制在 UGUI/MVP/,入口 MVPMain。
MVVM(课内简化):没有 WPF 那种双向绑定;BaseUGUIPanel + GetUIElement,逻辑仍写在 Panel 里。Init 里 UpdateInfo + AddEventListener,OnButtonInitClick 按按钮名开子界面。入口 MVVMMain 用 ShowPanel / HidePanel。
MVE:在上一套骨架上,Model 改完走 BaseEventManager:AddEventListener / EventTrigger,字符串 key 与 MVE_PlayerModel 泛型载荷配套。路径 UI_SAVE_PATH + "MVE/",入口 MVEMain。
| 写法 | View 与 Model | 谁改控件 | 数据到界面 |
|---|---|---|---|
| MVC | View 有 UpdateInfo(Model) |
View | Model 内 UnityAction |
| MVP | View 弱依赖模型类型 | Presenter | 仍是 Model 的 UnityAction |
| MVVM 演示 | BaseUGUIPanel 合一 |
Panel | 同上 |
| MVE | 同 MVVM 演示 | 同左 | BaseEventManager 广播 |
PureMVC:单核、接入与显隐刷新
教学工程按单核:一个 Facade。真要多个上下文用官方多核,别在单核里 new 出第二套 GameFacade。
接入方式二选一:编译 DLL 丢 Plugins;或把源码里核心 / 接口 / 设计模式三块拷进工程,外面套 PureMVC 文件夹方便改。
通知名:PureNotification 里 const string,与 RegisterCommand、ListNotificationInterests、SendNotification 共用字面量。字符串 key 拼错一位常常编译通过、运行里「没反应」,集中定义能少踩这类坑。
数据:PlayerDataObj 存字段;PlayerProxy 包一层,RetrieveProxy 取,Data 上挂实例。读档、升级、SaveData 在 Proxy;界面不自动跟 Proxy 变,一般由 Command 再 SendNotification(UPDATE_PLAYER_INFO, …) 把各 Mediator 拉起来。
Mediator:ListNotificationInterests 列要听的名字;HandleNotification 里看 Name 和 Body。ViewComponent 在 SetView 前、或 HidePanelCommand 之后可能为空,先判再 as 成具体 View。
Command:RegisterCommand(通知, () => new 某Command());Execute 读 Body,中间可以继续 SendNotification 串步骤。
| 步骤 | 行为 |
|---|---|
| 显示 | 按 Body 面板名注册或取 Mediator;ViewComponent == null 时 Resources.Load(示例 BaseFramework/UI/UGUI/PureMVC/...),挂 UGUICanvas,SetView |
| 隐藏 | Destroy 后 ViewComponent = null;与只 SetActive(false) 不是同一策略 |
| 刷新 | 面板就绪或升级后发 UPDATE_PLAYER_INFO,Body 带 RetrieveProxy(PlayerProxy.NAME).Data,所有 interests 命中的 Mediator 一起更新 |
| 角色 | 干什么 | 系列里 |
|---|---|---|
| Facade | 对外发通知、取 Proxy/Mediator | GameFacade.Instance |
| Proxy | 挂 Data、存档与业务 |
PlayerProxy + PlayerDataObj |
| Mediator | 绑 View、接通知、调 UpdateInfo |
NewMainViewMediator 等 |
| Command | 一步业务(显隐、升级等) | ShowPanelCommand、LevUpCommand 等 |
29.2 面试题精选
基础题
1. 为何用 PlayerProxy 包一层 PlayerDataObj
题目
玩家数据已经能用 POCO 表示,为什么还要注册 PlayerProxy,而不是写一个全局单例 Model 了事?
深入解析
- PureMVC 里 Model 侧的标准入口是
Proxy:通过Facade.RetrieveProxy(NAME)取,数据放在Data,和谁一起注册、何时反注册跟框架表走。 - POCO 只承载字段;读档、
LevUp、SaveData放在 Proxy 上,比把所有静态方法堆进一个 God 类好拆、好测。 - 和本系列手写
PlayerModel比:PureMVC 更强调「经 Facade 拿模型」和「用通知驱动界面」,而不是业务里到处PlayerModel.Data。
| 对比 | 手写 MVC | PureMVC |
|---|---|---|
| 取模型 | 静态 PlayerModel.Data |
RetrieveProxy,走注册表 |
| 结构 | 数据与逻辑常混在一个类 | POCO + Proxy 分工 |
| 刷界面 | UnityAction → Controller |
Command 发通知 → Mediator |
答题示例
Proxy是框架认的 Model 角色,Data上挂真正的数据对象;读档改数在 Proxy,不在散落的静态里。界面更新靠 Command 发通知、Mediator 接,而不是全局单例到处被直接调用。
参考文章
- 22.PureMVC框架-Model数据层和Proxy代理模式
2. 为什么要单独建 PureNotification 常量类
题目
通知名就是字符串,直接 SendNotification("xxx") 有什么问题?
深入解析
- 拼错一个字符,C# 编译器不管,运行期常见现象是「按钮没反应、断点进不去 Command」,排查成本高。
RegisterCommand、Mediator 的 interests、业务里的SendNotification必须引用同一串字面量;收成const类等于项目内通知字表,改名、全局搜索都简单。
答题示例
魔法字符串容易写错且错得安静。
集中定义成常量,注册 Command 和写 interests 时共用,改一处全项目对齐。
参考文章
- 21.PureMVC框架-框架导入和通知名类
进阶题
1. RegisterCommand 为何用工厂每次 new Command
题目
注册时为什么写 () => new StartUpCommand(),而不是注册一个长期复用的 Command 实例?
深入解析
- Command 里常会带本次执行的临时状态;复用同一个实例,上一次
Execute留下的字段可能污染下一次。 - 工厂配合
InitializeController里的映射表:业务只发通知,「谁处理」在注册阶段配好,加流程多半是加一条映射而不是改调用栈。
| 做法 | 结果 |
|---|---|
每次 new |
无跨次执行的脏状态 |
| 单例 Command | 成员可能被上次执行污染 |
| 映射表注册 | 扩展加表项,调用方保持薄 |
答题示例
每次执行 new 一个 Command,避免实例上残留状态。
通知到命令类型的关系写在注册里,调用栈只负责发通知。
参考文章
- 25.PureMVC框架-Facade外观模式和Command命令模式
2. 从发 SHOW_PANEL 到主界面刷上数据,中间经过谁
题目
按本系列示例:SendNotification(SHOW_PANEL, "MainPanel") 之后,数据是怎么进到 NewMainView 的?哪里断了会白屏或没数据?
深入解析
- Facade 把通知派给已注册的
ShowPanelCommand。 - Command 按
Body注册或取得Mediator,需要时Resources.Load预制、SetParent、SetView;随后再发UPDATE_PLAYER_INFO,Body里塞RetrieveProxy(PlayerProxy.NAME).Data。 - 主界面 Mediator 在
ListNotificationInterests里声明了UPDATE_PLAYER_INFO,在HandleNotification里把Body转成PlayerDataObj,调用NewMainView.UpdateInfo。 - 若启动流程里没
RegisterProxy,后面RetrieveProxy为空,界面能出来但没有玩家数据。
HidePanelCommand 里 Destroy 后必须把 ViewComponent 置 null,否则下次 Show 可能误判「已有 View」而跳过实例化;这与只 SetActive(false) 的缓存策略不要混用。
Facade → ShowPanelCommand → Mediator + Load/SetView
→ SendNotification(UPDATE_PLAYER_INFO, Proxy.Data)
→ Mediator.HandleNotification → NewMainView.UpdateInfo
答题示例
Facade 转到 ShowPanelCommand,Command 负责 Mediator 和预制体,SetView 后再发更新玩家信息,Body 里是 Proxy 的数据。
Mediator 收到后在 HandleNotification 里把数据交给 View;Proxy 没注册好的话这一步拿不到数据。
参考文章
- 26.PureMVC框架-显隐面板命令
- 27.PureMVC框架-更新通知和升级命令
深度题
1. 单核下封装 GameFacade.Instance 的边界
题目
在 PureMVC 单核用法里,自己包一层 GameFacade.Instance 时要注意什么?多 new 一个会怎样?
深入解析
- 约定整个进程一个 Facade;子类
Instance里new GameFacade()赋给基类静态instance,返回时as GameFacade才能调用子类扩展。 - 若误造第二套 Facade,或把单核 API 和多核混着用,会出现通知进 A 表、Proxy 挂在 B 表,表现像「随机不响应」。
- 多模块隔离应走官方多核,而不是单核里硬拆多套。
答题示例
单核就是全局一个 Facade,Instance 负责造那一个并挂到基类静态上。
多实例等于多套注册表,Command 和 Proxy 对不上号,问题难查。
参考文章
- 21.PureMVC框架-框架导入和通知名类
- 25.PureMVC框架-Facade外观模式和Command命令模式
2. 跨面板刷新:胖面板、手写 MVC、PureMVC 怎么各干各的
题目
胖面板里升级后要 MainPanel.Panel.UpdateInfo();手写 MVC 用模型事件;PureMVC 里数据变了界面怎么一起更新?LEV_UP 之后为什么还要发 UPDATE_PLAYER_INFO?
深入解析
- 胖面板:面板之间静态引用互调,耦合随功能线性涨,改一个界面容易牵一片。
- 手写 MVC:
PlayerModel用UnityAction通知各Controller,各更新自己的 View;Add/Remove必须成对,否则泄漏。 - PureMVC:Proxy 改完数据后,由 Command
SendNotification;多个 Mediator 可以听同一条UPDATE_PLAYER_INFO,Body共享一份PlayerDataObj,主界面和子面板等级、数值一致。LevUpCommand里只改 Proxy 不会自动重绘,再发UPDATE_PLAYER_INFO才能把「数据已变」广播给所有关心的界面;省掉这条就要各 Mediator 自己去RetrieveProxy,风格上又回到紧耦合。
| 模式 | 跨面板同步手段 |
|---|---|
| 胖面板 | 静态互调 |
| 手写 MVC | Model 事件 → 多个 Controller |
| PureMVC | 同一通知 + 共享 Body,一对多 Mediator |
答题示例
胖面板是面板互相捅;MVC 是模型事件推给各个 Controller。
PureMVC 用通知广播,多个 Mediator 听同一条更新;升级后多发一次
UPDATE_PLAYER_INFO是为了让所有订阅方拿到新Data,否则只改 Proxy 界面不会自己变。
参考文章
- 4.MVC框架-不使用MVC框架-主面板
- 6.MVC框架-使用MVC框架-Model数据层
- 27.PureMVC框架-更新通知和升级命令
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com