
1. 项目概述为什么Unity开发者需要PureMVC如果你是一个Unity开发者尤其是从客户端逻辑一路做到需要处理复杂业务和数据交互的项目时你大概率经历过这样的场景UI按钮点击后需要更新数据数据变化后又要刷新多个界面同时可能还要向服务器发送请求。写着写着MonoBehaviour脚本里的代码就变成了意大利面条FindObjectOfType满天飞public static变量到处都是测试无从下手加个新功能生怕把旧逻辑搞崩。这就是典型的“大泥球”架构项目初期跑得快中后期维护起来简直是一场噩梦。这时一个清晰、可测试、易于扩展的架构模式就成了刚需。PureMVC正是为解决这类问题而生的经典框架。它不是Unity官方的也不是某个引擎特有的而是一个起源于ActionScript/Flex社区后来被移植到C#、Java、C等数十种语言的、轻量级的应用程序架构。它的核心思想非常明确将你的应用逻辑清晰地划分为模型Model、视图View、控制器Controller三个核心层并通过一种松耦合的消息通知Notification机制让它们通信。在Unity里用PureMVC有什么好处首先职责分离做得非常彻底。你的数据管理、业务逻辑和界面显示被完全拆开MonoBehaviour脚本只负责最纯粹的视图表现和用户输入捕获复杂的计算和状态管理交给后端的模型和控制器。其次可测试性极大增强。因为核心逻辑不依赖于Unity的运行时环境你完全可以写单元测试来验证你的命令Command和代理Proxy是否正确。最后团队协作会更顺畅。新人接手时只要理解框架规则就能快速定位功能对应的代码模块而不是在成千上万行混杂的脚本里大海捞针。简单来说当你觉得Unity项目里的脚本依赖开始失控修改一处波及全身时就是引入PureMVC这类框架的最佳时机。它带来的不是立即的功能提升而是长期可维护性的巨大收益。2. PureMVC核心概念深度解析不只是MVC很多人一听MVC就觉得是老生常谈但PureMVC在经典MVC的基础上做了更精细和严格的定义形成了一套独特的“多核”架构。理解这几个核心角色及其关系是上手的关键。2.1 模型Model与数据代理Proxy在PureMVC中Model层并不直接是一个类而是一个持有和管理多个Proxy代理的单例对象。Proxy是框架中数据的保管员。它的职责是封装数据管理特定的数据集比如用户信息、背包物品列表、游戏关卡数据等。这些数据通常以自定义的Data类或结构体的形式存在作为Proxy的属性。提供访问接口对外提供获取GetData和更新SetData这些数据的方法。任何其他部分如Command、Mediator需要操作数据都必须通过对应的Proxy而不是直接访问。与外部服务通信Proxy是与“外界”打交道的首选。例如向服务器请求数据、读写本地文件、调用平台原生接口等操作都应该封装在Proxy内部。当数据获取或更新完成后Proxy可以发送一个Notification通知来告知系统。注意一个常见的误解是把Unity的ScriptableObject当作Proxy来用。ScriptableObject是优秀的数据容器和配置工具但它本身不具备PureMVC框架的“生命周期”和“消息系统”感知。更佳实践是在Proxy内部持有或引用ScriptableObject实例由Proxy来管理其加载、保存和状态同步。2.2 视图View与中介者MediatorView层对应Unity中的UI界面、HUD、特效显示等一切用户能看到并与之交互的东西。PureMVC通过Mediator中介者来管理这些视图组件。每个重要的UI界面如MainMenuPanel、InventoryWindow都应该有一个对应的Mediator类。这个Mediator的职责是监听用户输入在Start()或初始化时订阅UI按钮的onClick、滑动条的onValueChanged等事件。发送用户意图当用户进行操作时Mediator不处理业务逻辑而是创建一个包含必要参数的Notification并发送出去。例如点击“登录”按钮Mediator发送一个LOGIN_BTN_CLICKED通知并附上输入框的用户名和密码。响应数据变化Mediator会监听HandleNotification其他部分发出的、与它相关的通知比如由某个Proxy发出的USER_DATA_UPDATED。当收到通知时它从Proxy获取最新数据并调用其管理的UI组件的方法来更新显示。这种设计实现了视图与逻辑的彻底解耦。你的UI预制体可以独立开发和修改只要它暴露的接口方法不变Mediator的代码就几乎不用动。2.3 控制器Controller与命令CommandController层是系统的“大脑”负责处理具体的业务逻辑和流程控制。它的具体执行单元是Command命令。Command是一个独立的类代表一个可执行的、相对完整的操作。当Mediator或另一个Command发送出一个通知时如果该通知在控制器中被注册了映射关系就会触发对应的Command实例化并执行。Command的典型工作流是接收通知从通知中获取参数。操作Proxy调用一个或多个Proxy的方法来获取或修改数据。发送新通知操作完成后发送新的通知来驱动下一步如更新UI或触发下一个命令。执行完毕销毁Command实例是短生命周期的执行完Execute方法后就会被销毁。例如处理登录的LoginCommand它收到LOGIN_BTN_CLICKED通知调用UserProxy的Login方法内部可能包含网络请求根据登录结果发送LOGIN_SUCCESS或LOGIN_FAILED通知。2.4 通知Notification与观察者模式Notification是PureMVC的“血液”是整个框架松耦合通信的基石。它本质上是一个消息对象包含一个名称Name、可选的数据体Body和类型Type。名称是一个字符串如STARTUPITEM_ACQUIRED。它是消息的唯一标识用于匹配监听者。数据体可以携带任何类型的对象用于传递参数比如一个用户对象、一个物品ID列表。类型用于对通知进行更细粒度的分类实践中使用频率较低。框架的核心单例Facade提供了SendNotification方法。任何地方Mediator, Command, Proxy都可以发送通知。而Mediator和Command可以通过重写HandleNotification或注册监听列表来声明自己关心哪些通知。这种基于消息的机制让模块之间不需要相互引用。InventoryMediator完全不知道BattleProxy的存在它只关心INVENTORY_UPDATED这个通知是谁发的。这极大地降低了模块间的依赖提升了代码的灵活性和可复用性。3. 从零搭建Unity PureMVC项目实战理论讲得再多不如动手搭一遍。下面我们以一个极简的“计数器”应用为例完整走一遍PureMVC在Unity中的集成和应用流程。这个应用有一个显示数字的文本和一个“增加”按钮。3.1 第一步获取与导入PureMVC框架PureMVC的C#版本是开源的你可以直接从其官方GitHub仓库https://github.com/PureMVC/puremvc-csharp-standard-framework下载。对于Unity项目最方便的方式是使用Unity的Package Manager从Git URL添加。在Unity编辑器中打开Window Package Manager。点击左上角的“”按钮选择“Add package from git URL...”。输入仓库地址https://github.com/PureMVC/puremvc-csharp-standard-framework.git点击“Add”。Unity会自动克隆仓库并将其作为本地包导入你的项目。导入后你会在项目的Packages目录下看到PureMVC。核心文件位于Runtime/PureMVC目录中主要包括Patterns核心模式和Interfaces接口等。你不需要修改这些文件。3.2 第二步定义应用入口与核心数据任何PureMVC应用都需要一个启动点在Unity中我们通常创建一个不销毁的全局GameObject并挂载一个ApplicationFacade继承自Facade的脚本。1. 创建AppFacade// AppFacade.cs using PureMVC.Patterns.Facade; using UnityEngine; public class AppFacade : Facade { // 单例访问点 public new static IFacade Instance (instance ?? new AppFacade()); // 初始化Controller注册Command protected override void InitializeController() { base.InitializeController(); // 注册启动命令 RegisterCommand(STARTUP, () new StartupCommand()); } // 启动应用 public void Launch() { SendNotification(STARTUP); } }2. 创建启动命令StartupCommand这是整个应用的“点火器”负责初始化Model、View和Controller的各个部分。// StartupCommand.cs using PureMVC.Patterns.Command; using PureMVC.Interfaces; public class StartupCommand : SimpleCommand { public override void Execute(INotification notification) { // 1. 注册数据代理Model Facade.RegisterProxy(new CounterProxy()); // 2. 注册界面中介者View // 注意这里假设CounterViewMediator会在其构造函数中自己注册 // 更常见的做法是在一个专门的PrepViewCommand中处理 // 3. 注册其他业务命令Controller Facade.RegisterCommand(ADD_COUNT, () new AddCountCommand()); } }3. 创建数据代理CounterProxy// CounterProxy.cs using PureMVC.Patterns.Proxy; using System; // 简单的数据类 public class CounterData { public int Count { get; set; } 0; } public class CounterProxy : Proxy { // 声明一个公共的常量名称方便其他地方引用 public new const string NAME CounterProxy; // 持有的数据 public CounterData CounterData Data as CounterData; public CounterProxy() : base(NAME, new CounterData()) { } // 提供一个增加计数的方法 public void AddCount(int value 1) { CounterData.Count value; // 数据变更后发送通知 SendNotification(COUNTER_UPDATED, CounterData.Count); } // 获取当前计数 public int GetCount() { return CounterData.Count; } }3.3 第三步构建Unity界面与中介者1. 创建Unity UI在场景中创建一个Canvas里面放一个Text (TMP)显示数字一个Button用于增加计数。将Text组件命名为CountText按钮命名为AddButton。可以简单写一个CounterView.cs脚本挂在这个UI根节点上用于持有这些组件的引用。// CounterView.cs using TMPro; using UnityEngine; using UnityEngine.UI; public class CounterView : MonoBehaviour { public TMP_Text countText; public Button addButton; void Start() { // 视图组件初始化事件监听由Mediator负责 } // 提供给Mediator调用的更新UI的方法 public void UpdateCountDisplay(int count) { countText.text $Count: {count}; } }2. 创建对应的中介者CounterMediator// CounterMediator.cs using PureMVC.Patterns.Mediator; using PureMVC.Interfaces; using UnityEngine; public class CounterMediator : Mediator { public new const string NAME CounterMediator; // 对视图组件的引用 private CounterView View ViewComponent as CounterView; public CounterMediator(CounterView view) : base(NAME, view) { // 在构造函数中声明关心哪些通知 // 当COUNTER_UPDATED通知发出时会调用HandleNotification } // 重写通知监听列表 public override string[] ListNotificationInterests() { return new string[] { COUNTER_UPDATED }; } // 处理收到的通知 public override void HandleNotification(INotification notification) { switch (notification.Name) { case COUNTER_UPDATED: // 从通知体Body中获取最新的计数值 int newCount (int)notification.Body; // 调用视图组件的方法更新显示 View.UpdateCountDisplay(newCount); break; } } // 重写OnRegister在这里进行事件绑定等初始化操作 public override void OnRegister() { // 监听UI按钮点击事件 View.addButton.onClick.AddListener(OnAddButtonClicked); // 初始化显示从Proxy获取初始数据 CounterProxy proxy Facade.RetrieveProxy(CounterProxy.NAME) as CounterProxy; View.UpdateCountDisplay(proxy.GetCount()); } // 按钮点击事件处理 private void OnAddButtonClicked() { // 不处理业务逻辑只发送通知 SendNotification(ADD_COUNT); } // 重写OnRemove清理事件监听 public override void OnRemove() { View.addButton.onClick.RemoveListener(OnAddButtonClicked); } }3. 修改启动命令注册中介者我们需要在StartupCommand中完成Mediator的注册确保UI准备好后能被框架管理。// 在StartupCommand的Execute方法中补充 public override void Execute(INotification notification) { // ... 之前注册Proxy的代码 ... // 注册中介者需要先找到场景中的View组件 CounterView counterView GameObject.FindObjectOfTypeCounterView(); // 简单查找生产环境建议用更稳健的方式 if (counterView ! null) { Facade.RegisterMediator(new CounterMediator(counterView)); } else { Debug.LogError(CounterView not found in scene!); } // ... 注册其他命令的代码 ... }3.4 第四步实现业务逻辑命令最后我们实现点击按钮后真正执行增加操作的命令。// AddCountCommand.cs using PureMVC.Patterns.Command; using PureMVC.Interfaces; public class AddCountCommand : SimpleCommand { public override void Execute(INotification notification) { // 1. 获取数据代理 CounterProxy counterProxy Facade.RetrieveProxy(CounterProxy.NAME) as CounterProxy; // 2. 执行业务逻辑增加计数 counterProxy.AddCount(1); // 调用Proxy的方法 // 3. Proxy的AddCount方法内部已经发送了COUNTER_UPDATED通知 // 所以这里不需要再发送。如果需要更复杂的流程可以在这里发送其他通知。 } }3.5 第五步启动应用在场景中创建一个空的GameObject命名为“AppRoot”将其设置为DontDestroyOnLoad。挂载一个Launcher.cs脚本。// Launcher.cs using UnityEngine; public class Launcher : MonoBehaviour { void Start() { // 确保Facade单例初始化 var facade AppFacade.Instance as AppFacade; // 启动PureMVC应用 facade.Launch(); } }运行Unity点击按钮你应该能看到计数器文本随之更新。至此一个完整的、符合PureMVC架构的微型应用就搭建完成了。整个数据流是View(Button Click) - Mediator(Send “ADD_COUNT”) - Command(Execute) - Proxy(AddCount Send “COUNTER_UPDATED”) - Mediator(HandleNotification) - View(Update Display)形成了一个清晰的单向循环。4. 进阶实战构建一个简易背包系统理解了基础计数器我们来看一个更贴近真实游戏的例子背包系统。这个系统涉及物品数据列表、UI格子渲染、物品添加/使用/丢弃等操作能充分体现PureMVC在管理复杂状态和UI交互上的优势。4.1 数据层设计ItemProxy与InventoryProxy背包系统至少需要两个核心数据模型物品定义和背包实例。我们创建两个Proxy。ItemProxy管理所有物品的静态定义如ID、名称、图标、类型、使用效果等。这些数据通常从配置表如JSON、ScriptableObject加载。// ItemProxy.cs using PureMVC.Patterns.Proxy; using System.Collections.Generic; [System.Serializable] public class ItemDef { public int id; public string name; public string iconPath; // 或直接是Sprite的引用 public ItemType type; // ... 其他属性 } public enum ItemType { Consumable, Equipment, Material } public class ItemProxy : Proxy { public new const string NAME ItemProxy; private Dictionaryint, ItemDef _itemDefDict new Dictionaryint, ItemDef(); public ItemProxy() : base(NAME) { } // 初始化加载配置 public void LoadConfig(ListItemDef defList) { _itemDefDict.Clear(); foreach (var def in defList) { _itemDefDict[def.id] def; } SendNotification(ITEM_CONFIG_LOADED); } public ItemDef GetItemDef(int id) { _itemDefDict.TryGetValue(id, out var def); return def; } }InventoryProxy管理玩家当前背包的动态数据即物品槽位列表。// InventoryProxy.cs using PureMVC.Patterns.Proxy; using System.Collections.Generic; [System.Serializable] public class InventorySlot { public int itemId; public int count; public bool isEmpty itemId 0 || count 0; } public class InventoryProxy : Proxy { public new const string NAME InventoryProxy; public ListInventorySlot Slots Data as ListInventorySlot; public const int MAX_SLOTS 30; public InventoryProxy() : base(NAME, new ListInventorySlot(new InventorySlot[MAX_SLOTS])) { // 初始化空槽位 for(int i0; iMAX_SLOTS; i) { Slots[i] new InventorySlot(); } } // 添加物品自动堆叠或寻找空位 public bool AddItem(int itemId, int addCount) { // 1. 尝试堆叠到已有物品槽 // 2. 尝试放入空槽 // ... 实现具体逻辑 ... bool success true; // 假设添加成功 if(success) { SendNotification(INVENTORY_UPDATED); } return success; } // 使用/移除物品 public bool UseItem(int slotIndex) { // ... 实现具体逻辑减少数量或清空槽位 ... SendNotification(INVENTORY_UPDATED); return true; } public InventorySlot GetSlot(int index) Slots[index]; }4.2 视图层设计背包UI与SlotMediator背包UI通常是一个包含多个ItemSlot物品格子的网格。每个ItemSlot是一个独立的UI单元包含图标、数量文本等。我们可以为整个背包窗口创建一个InventoryPanelMediator再为每个物品格子创建一个ItemSlotMediator形成Mediator嵌套的结构。InventoryPanelMediator管理整个背包窗口的开关、背景并负责创建和管理所有ItemSlotMediator。// InventoryPanelMediator.cs public class InventoryPanelMediator : Mediator { public new const string NAME InventoryPanelMediator; private InventoryPanelView _view; private ListItemSlotMediator _slotMediators new ListItemSlotMediator(); public InventoryPanelMediator(InventoryPanelView view) : base(NAME, view) { _view view; } public override void OnRegister() { // 监听背包打开/关闭通知 // 初始化所有格子Mediator InventoryProxy inventoryProxy Facade.RetrieveProxy(InventoryProxy.NAME) as InventoryProxy; for (int i 0; i InventoryProxy.MAX_SLOTS; i) { var slotView _view.CreateSlotView(i); // 假设View能创建格子UI var slotMediator new ItemSlotMediator(slotView, i); Facade.RegisterMediator(slotMediator); _slotMediators.Add(slotMediator); } // 监听背包数据更新刷新所有格子 } public override string[] ListNotificationInterests() { return new string[] { INVENTORY_UPDATED, OPEN_INVENTORY, CLOSE_INVENTORY }; } public override void HandleNotification(INotification notification) { switch (notification.Name) { case INVENTORY_UPDATED: // 通知所有ItemSlotMediator更新自己 foreach (var mediator in _slotMediators) { mediator.UpdateSlotView(); } break; case OPEN_INVENTORY: _view.SetVisible(true); break; // ... 其他处理 } } }ItemSlotMediator管理单个物品格子的显示和交互。// ItemSlotMediator.cs public class ItemSlotMediator : Mediator { private int _slotIndex; public ItemSlotMediator(ItemSlotView view, int slotIndex) : base($ItemSlotMediator_{slotIndex}, view) { _slotIndex slotIndex; } public override void OnRegister() { // 监听该格子的点击、拖拽等事件 (ViewComponent as ItemSlotView).OnClick.AddListener(OnSlotClicked); // 初始化显示 UpdateSlotView(); } private void OnSlotClicked() { // 发送通知携带格子索引 SendNotification(INVENTORY_SLOT_CLICKED, _slotIndex); } // 外部如PanelMediator或自己可以调用来更新显示 public void UpdateSlotView() { InventoryProxy inventoryProxy Facade.RetrieveProxy(InventoryProxy.NAME) as InventoryProxy; ItemProxy itemProxy Facade.RetrieveProxy(ItemProxy.NAME) as ItemProxy; var slot inventoryProxy.GetSlot(_slotIndex); var view ViewComponent as ItemSlotView; if (slot.isEmpty) { view.SetEmpty(); } else { var itemDef itemProxy.GetItemDef(slot.itemId); view.SetItem(itemDef.iconPath, slot.count); } } }4.3 控制器层设计处理物品交互命令当玩家点击一个背包格子时ItemSlotMediator发出INVENTORY_SLOT_CLICKED通知。我们需要一个命令来处理这个交互其逻辑可能很复杂是使用是丢弃还是与其他界面如商店、合成台进行交互这体现了Command的价值。// ProcessInventorySlotCommand.cs public class ProcessInventorySlotCommand : SimpleCommand { public override void Execute(INotification notification) { int slotIndex (int)notification.Body; InventoryProxy inventoryProxy Facade.RetrieveProxy(InventoryProxy.NAME) as InventoryProxy; var slot inventoryProxy.GetSlot(slotIndex); if (slot.isEmpty) return; // 这里可以根据游戏状态如是否打开商店、是否在战斗中决定不同的行为 // 例如检查当前是否有“物品使用确认框”打开 // 我们假设直接使用 inventoryProxy.UseItem(slotIndex); // UseItem内部会发送INVENTORY_UPDATED触发UI刷新 // 如果需要更复杂的逻辑比如弹出二级菜单使用/丢弃/查看 // 可以在这里发送一个新的通知如SHOW_ITEM_CONTEXT_MENU由另一个Mediator来显示菜单。 } }别忘了在StartupCommand或某个初始化命令中注册它Facade.RegisterCommand(INVENTORY_SLOT_CLICKED, () new ProcessInventorySlotCommand());通过这个背包系统的例子你可以看到PureMVC如何优雅地处理数据与UI的绑定、复杂UI的模块化管理以及根据上下文变化的用户交互。每个部分职责清晰且通过通知系统松散地连接在一起。5. 避坑指南与性能优化实战心得用了几年PureMVC踩过的坑和优化的点不少。下面这些经验很多是官方文档里不会写的但对于保证项目健康运行至关重要。5.1 通知泛滥与性能陷阱PureMVC基于通知但滥用通知会成为性能杀手和调试噩梦。问题1高频更新导致广播风暴。想象一个实时显示玩家位置的UI。如果你在Update里每帧都从PlayerProxy取数据并直接设置UI没问题。但如果你每帧都发送一个PLAYER_POSITION_UPDATED通知所有注册监听了这个通知的Mediator哪怕它们不关心位置的HandleNotification方法都会被调用做无用的检查。解决方案区分通知粒度。对于高频数据如位置、血量采用拉取Pull模式而非推送Push模式。让特定的Mediator在Update里主动从Proxy获取数据并更新视图。只为那些重要的、离散的状态变化如死亡、升级、获得物品使用通知。问题2通知名称为魔法字符串难以维护。代码里到处散落着SendNotification(SOME_EVENT)时间一长你根本记不住SOME_EVENT到底是在哪里定义的有哪些参数。解决方案使用常量类集中管理所有通知名称。public class NotificationConstants { public const string STARTUP STARTUP; public const string COUNTER_UPDATED COUNTER_UPDATED; public const string INVENTORY_UPDATED INVENTORY_UPDATED; public const string PLAYER_LEVEL_UP PLAYER_LEVEL_UP; // ... }使用时SendNotification(NotificationConstants.PLAYER_LEVEL_UP, newLevel)。这样便于查找、重命名和避免拼写错误。5.2 Mediator的生命周期与资源管理Mediator持有对MonoBehaviour视图对象的引用如果管理不当很容易造成内存泄漏或空引用。坑点场景切换时Mediator未注销。Unity场景切换时旧的GameObject被销毁但其对应的Mediator可能还注册在PureMVC框架中。当与该Mediator相关的通知发出时它的HandleNotification会被调用而它内部尝试访问已销毁的视图组件就会抛出MissingReferenceException。解决方案建立严格的配对机制。在视图GameObject的OnDestroy方法中确保其对应的Mediator被注销。通常可以在Mediator的OnRemove方法中做清理工作并让视图在销毁时通知Facade移除Mediator。一个更稳健的模式是使用一个ViewDestroyHelper组件挂载在视图根节点上在OnDestroy时发送一个特定的通知如${viewName}_DESTROYED由一个全局的ViewManagementCommand来统一处理Mediator的注销。技巧使用弱引用或接口隔离。Mediator直接持有GameObject或Component的强引用。可以考虑让Mediator只持有一个视图的接口IView而具体的视图组件实现这个接口。这样进一步降低了耦合。对于只是监听事件而不控制生命周期的对象可以考虑使用C#的弱引用模式但这在Unity中需谨慎使用。5.3 应对复杂业务逻辑MacroCommand与序列执行有些业务操作由多个步骤组成比如“领取每日奖励”1) 向服务器请求2) 验证返回3) 更新本地数据4) 刷新UI5) 播放特效。如果把这些全写在一个Command里它会变得臃肿。PureMVC提供了MacroCommand来解决这个问题。它可以按顺序执行多个子Command。public class ClaimDailyRewardMacroCommand : MacroCommand { protected override void InitializeMacroCommand() { // 按顺序添加子命令 AddSubCommand(() new RequestServerRewardCommand()); AddSubCommand(() new ValidateRewardDataCommand()); AddSubCommand(() new UpdateLocalDataCommand()); AddSubCommand(() new RefreshUICommand()); AddSubCommand(() new PlayRewardEffectCommand()); } }注册时只需注册这个宏命令即可RegisterCommand(CLAIM_DAILY_REWARD, () new ClaimDailyRewardMacroCommand())。每个子命令负责一个独立的步骤可以单独测试逻辑清晰得多。如果步骤间需要传递数据可以通过MacroCommand的note属性即最初的通知对象来传递。5.4 与Unity现有生态的融合PureMVC是一个纯C#框架如何与Unity的Addressable、UnityEvent、Coroutine等特性结合协程Coroutine处理异步Proxy中经常需要处理网络请求等异步操作。可以在Proxy内部启动一个MonoBehaviour来运行协程但更干净的做法是创建一个专门的Service层或使用UniTask等库。例如在Proxy中public void RequestServerData() { // 假设有一个全局的MonoBehaviour用于启动协程 App.Instance.StartCoroutine(RequestServerDataCoroutine()); } private IEnumerator RequestServerDataCoroutine() { // ... UnityWebRequest 等 ... yield return ...; // 请求完成更新数据并发送通知 SendNotification(SERVER_DATA_RECEIVED, data); }Addressable资源加载资源加载也适合放在Proxy中。加载完成后将加载的Sprite、GameObject等资源存入Proxy管理的数据中并发送通知。对应的Mediator监听通知获取资源引用并设置到UI上。这保证了资源生命周期的集中管理。ScriptableObject作为配置源如前所述让Proxy在初始化时读取ScriptableObject将其数据转换为框架内部的纯数据类。这样既利用了ScriptableObject在编辑器中的便利性又保持了PureMVC核心逻辑对Unity引擎的独立性便于单元测试。6. 常见问题排查与调试技巧在实际开发中你肯定会遇到通知没反应、Mediator找不到视图、数据不同步等问题。下面是一个快速排查清单和调试方法。问题现象可能原因排查步骤点击按钮毫无反应1. Mediator未注册。2. 按钮事件未绑定到Mediator方法。3. 发送的通知名称与Command注册的名称不匹配。1. 检查StartupCommand或相关命令中是否注册了对应Mediator。2. 在Mediator的OnRegister方法中打日志确认事件监听是否成功添加。3. 检查SendNotification的参数和RegisterCommand的参数是否完全一致大小写敏感。UI显示的数据与实际数据不符1. Proxy数据更新后未发送通知。2. Mediator监听了错误的通知名称。3. Mediator的HandleNotification方法中更新UI的代码有bug。1. 在Proxy的SetData或更新方法中确认SendNotification被调用。2. 核对Mediator的ListNotificationInterests返回的数组是否包含正确的通知名。3. 在HandleNotification中打日志检查收到的notification.Body是否正确并单步调试UI更新逻辑。场景切换后旧UI的Mediator报空引用Mediator在视图销毁后未从框架中注销。1. 确保视图GameObject销毁时有机制触发Mediator的注销如发送特定通知。2. 在AppFacade中提供一个RemoveMediatorByView的公共方法供视图销毁时调用。某个Command似乎从未执行1. Command未注册。2. 触发通知的代码未被执行。3. 有多个Command注册到同一通知执行顺序或条件导致预期外的行为。1. 检查启动流程确认Command注册成功。2. 在发送通知的代码行前打日志或断点。3. 使用PureMVC的调试工具或自定义日志查看通知的发送和派发流程。内存泄漏Mediator和视图未被释放1. Mediator中订阅了外部事件如全局事件总线、静态事件未取消订阅。2. 持有对视图的强引用且视图未正确销毁。1. 在Mediator的OnRemove或ListNotificationInterests返回空数组以确保其被框架移除后必须清理所有非框架内的订阅。2. 使用WeakReference或确保视图销毁路径清晰。调试技巧重写Facade的SendNotification添加日志输出记录每个通知的发送者、名称和参数。这在复杂流程中追踪问题非常有效。使用条件编译在开发阶段定义#define PURMVC_DEBUG并在所有Proxy、Mediator、Command的关键方法构造函数、Execute、HandleNotification等开头加入Debug.Log。发布时关闭这个宏定义即可。可视化调试工具社区有一些为PureMVC开发的简单可视化调试器可以实时显示已注册的Proxy、Mediator、Command和流动的通知。如果项目规模大考虑引入或自己实现一个简单的版本。最后我的个人体会是PureMVC像是一套严谨的“交通规则”。在项目初期设立这些规则定义Proxy、Mediator、Command会感觉有些繁琐不如直接写MonoBehaviour来得快。但当项目规模增长到一定程度当需要多人协作、当需要为老代码添加新功能时这套规则带来的秩序感和可预测性的价值就凸显出来了。你知道数据一定在Proxy里你知道UI逻辑一定在Mediator里你知道业务流程一定在Command里。这种心智负担的降低对于长期维护和团队健康来说收益远大于初期的学习成本。它不是银弹对于极度追求原型速度的超小项目可能过重但对于任何有长期运营潜力的Unity项目尤其是涉及复杂状态管理和UI交互的提前引入PureMVC这样的架构无疑是给未来的自己一份宝贵的礼物。