1. 为什么需要架构模式?
刚开始接触Unity开发时,我最常干的事情就是把所有代码都塞进一个脚本里。比如做个简单的计数器功能,UI显示、按钮交互、数据存储全都写在一个MonoBehaviour里。这样确实能快速实现功能,但随着项目规模扩大,问题就来了:
- 改个数字类型要从头改到尾
- 团队协作时各种代码冲突
- 功能扩展像在拆炸弹
后来我发现,架构模式就是解决这些痛点的银弹。它像乐高说明书,告诉你哪些零件(代码)该放哪里,怎么组合。在Unity UI开发中,最常见的三种架构就是:
- MVC(Model-View-Controller):老牌架构,适合中小项目
- MVP(Model-View-Presenter):MVC的升级版,解耦更彻底
- MVVM(Model-View-ViewModel):数据绑定利器,适合复杂UI
举个真实案例:我们团队做过一个卡牌游戏,初期把所有卡牌逻辑写在单个脚本里。后来要加新功能时,改一行代码能报五个错。重构采用MVP后,新成员两天就能上手开发,效率提升明显。
新手常见误区:认为架构模式会增加复杂度。其实好的架构就像分类收纳,前期多花5分钟,后期省下5小时。
2. MVC实战:计数器案例拆解
2.1 原始代码的痛点
先看最常见的"面条式代码":
// 反例:所有逻辑混在一起 public class Counter : MonoBehaviour { public Text countText; private int count = 0; void Start() { count = PlayerPrefs.GetInt("count"); UpdateUI(); } public void OnAddClick() { count++; PlayerPrefs.SetInt("count", count); UpdateUI(); } void UpdateUI() { countText.text = count.ToString(); } }这段代码有三个致命问题:
- 改数据要动UI:如果想改用float类型,所有相关方法都要改
- 无法单元测试:业务逻辑和Unity引擎强耦合
- 难以扩展:加个存档功能得重写半个类
2.2 MVC改造方案
按MVC模式拆分后:
// Model:纯数据逻辑 public class CounterModel { public int Count { get; private set; } public void Load() { Count = PlayerPrefs.GetInt("count"); } public void Add() { Count++; PlayerPrefs.SetInt("count", Count); } } // View:只处理显示 public class CounterView : MonoBehaviour { public Text countText; public void UpdateCount(int count) { countText.text = count.ToString(); } } // Controller:中间人 public class CounterController : MonoBehaviour { [SerializeField] CounterView view; CounterModel model = new CounterModel(); void Start() { model.Load(); view.UpdateCount(model.Count); } public void OnAddClick() { model.Add(); view.UpdateCount(model.Count); } }关键改进点:
- Model不依赖Unity,可单独测试
- View只关心显示,不碰业务逻辑
- Controller处理流程,像交通警察
2.3 实际项目中的优化技巧
在真实项目中,我常用这些技巧优化MVC:
- 事件通信:用Action或EventBus解耦
// Model中定义事件 public event Action<int> OnCountChanged; // Controller订阅事件 model.OnCountChanged += view.UpdateCount;- 依赖注入:用Zenject等框架管理对象
[Inject] private CounterModel model;- 脚本分工:按功能拆分多个Controller
- InputController:处理输入
- AudioController:管理音效
- SaveController:负责存档
3. MVP进阶:更彻底的解耦
3.1 MVC的遗留问题
虽然MVC已经不错,但View还是要知道Model的存在。比如:
// View需要知道Model结构 public void UpdateCount(CounterModel model) { text.text = model.Count.ToString(); }这在大型项目中会导致:
- 改Model可能影响View
- 单元测试需要Mock整个Model
3.2 MVP改造方案
MVP的关键改进是:
- View变笨:只暴露UI控件,不包含更新方法
- Presenter接管:所有逻辑移到这里
// View只提供控件访问 public class CounterView : MonoBehaviour { public Text countText; public Button addButton; } // Presenter全权负责 public class CounterPresenter { private CounterView view; private CounterModel model; public void Initialize(CounterView view) { this.view = view; model = new CounterModel(); view.addButton.onClick.AddListener(OnAddClick); UpdateView(); } void OnAddClick() { model.Add(); UpdateView(); } void UpdateView() { view.countText.text = model.Count.ToString(); } }优势对比:
| 维度 | MVC | MVP |
|---|---|---|
| View复杂度 | 需要实现更新方法 | 仅提供控件引用 |
| 可测试性 | 需模拟Unity环境 | 纯逻辑可独立测试 |
| 耦合度 | View依赖Model | 完全解耦 |
3.3 实际应用场景
MVP特别适合:
- 跨平台项目:同一套Presenter可适配不同View
- 自动化测试:Presenter不依赖Unity引擎
- 复杂UI逻辑:如分步骤的表单填写
我在一个AR项目中用MVP实现了:
- Android/iOS不同UI层
- 共用相同的识别逻辑Presenter
- 测试覆盖率从30%提升到80%
4. MVVM探索:数据绑定的魅力
4.1 为什么选择MVVM?
MVVM的核心是数据绑定- 当数据变化时UI自动更新。传统方式需要手动同步:
// 传统方式 void Update() { text.text = model.Value.ToString(); }而MVVM只需要声明绑定关系,像这样:
// ViewModel public class CounterViewModel { public ReactiveProperty<int> Count { get; } = new(); } // View绑定 view.Bind(viewModel.Count, text);当ViewModel.Count变化时,text自动刷新。
4.2 Unity中的实现方案
虽然Unity没有原生MVVM支持,但可以通过这些方案实现:
- UniRx:响应式编程扩展
// ViewModel public class CounterViewModel { public ReactiveProperty<int> Count { get; } = new(); public void Add() { Count.Value++; } } // View绑定 viewModel.Count.Subscribe(count => { text.text = count.ToString(); });- 第三方框架:如uFrame、MVVM Toolkit
// 使用MVVM Toolkit [Binding] public class CounterView : MonoBehaviour { [Inject] public CounterViewModel ViewModel { get; set; } [Binding("Count")] public Text countText; }4.3 适用场景与坑点
最适合场景:
- 表单密集型应用(如设置界面)
- 实时数据展示(如排行榜)
- 需要频繁UI更新的游戏(如模拟经营)
我踩过的坑:
- 性能问题:大量绑定会导致GC
- 调试困难:变更来源不明确
- 学习曲线:需要理解响应式编程
建议:简单项目用MVC/MVP足矣,超过20个UI控件再考虑MVVM。
5. 架构选型指南
5.1 技术对比矩阵
| 维度 | MVC | MVP | MVVM |
|---|---|---|---|
| 学习成本 | ★★☆ | ★★★ | ★★★★ |
| 代码量 | 中等 | 较多 | 较少 |
| 解耦程度 | 部分解耦 | 完全解耦 | 完全解耦 |
| 适合项目规模 | 小型到中型 | 中型到大型 | 大型复杂 |
| 测试便利性 | 需模拟环境 | 可单元测试 | 可单元测试 |
| 适用场景 | 常规UI | 跨平台/测试驱动 | 数据驱动型UI |
5.2 决策流程图
根据我的经验,可以按这个流程选择:
开始 │ ├─ 项目是否简单? → 是 → 不用架构/简单MVC │ (如Game Jam) │ ├─ 需要跨平台? → 是 → MVP │ ├─ UI数据绑定需求多? → 是 → MVVM │ (如实时仪表盘) │ └─ 其他情况 → MVC/MVP混合5.3 团队协作建议
- 新人团队:从MVC开始,逐步引入MVP概念
- 成熟团队:建立架构规范,比如:
- View层命名加
_View后缀 - Presenter放在特定文件夹
- 禁止跨层直接调用
- View层命名加
- 大型项目:使用依赖注入框架管理各层
我在带团队时制定的"三不原则":
- View不直接访问Model
- Model不包含任何Unity相关代码
- 业务逻辑不放在MonoBehaviour中
6. 常见问题解决方案
6.1 如何处理跨层通信?
问题场景:Model数据变化时,需要更新多个View
解决方案:
- 事件中心(推荐):
// 全局事件中心 EventCenter.OnCountChanged += UpdateAllViews;- 观察者模式:
// Model实现INotifyPropertyChanged model.PropertyChanged += (s,e) => { if(e.PropertyName == "Count") ... };- 响应式流(UniRx):
model.Count .Throttle(TimeSpan.FromSeconds(1)) .Subscribe(UpdateViews);6.2 如何管理依赖关系?
错误示范:
// 直接new导致强耦合 public class Presenter { private Model model = new Model(); }正确做法:
- 构造函数注入:
public Presenter(IModel model) { this.model = model; }- 使用IOC容器:
// 注册 container.Bind<IModel>().To<Model>(); // 获取 var model = container.Resolve<IModel>();6.3 性能优化技巧
- 避免频繁绑定:对高频数据使用Throttle
model.Score .ThrottleFrame(5) .Subscribe(UpdateScore);- 对象池管理View:复用UI元素而非销毁
- 分层加载:先加载核心Model,再懒加载View
7. 从理论到实践
7.1 渐进式迁移策略
对于已有项目,我推荐这样迁移:
- 先抽离Model:
- 找出所有业务逻辑
- 移入新创建的Model类
- 再分离View:
- 标识UI控件
- 创建View类管理
- 最后加中间层:
- 用Controller/Presenter连接
- 逐步替换:
- 按功能模块逐个重构
7.2 代码规范建议
- 命名约定:
- Model:
XXXModel - View:
XXXView - Presenter:
XXXPresenter
- Model:
- 目录结构:
/Scripts /Models /Views /Presenters /Services - 禁止事项:
- View中写if-else业务逻辑
- Model引用UnityEngine
- Presenter直接操作GameObject
7.3 调试技巧
- 分层调试法:
- 先确保Model数据正确
- 再测试Presenter逻辑
- 最后检查View显示
- 日志标记:
[Dependency] public class Logger { public void Log(string message) { Debug.Log($"[{DateTime.Now}] {message}"); } }- 单元测试示例:
[Test] public void TestModelAdd() { var model = new CounterModel(); model.Add(); Assert.AreEqual(1, model.Count); }8. 架构模式扩展应用
8.1 与其他设计模式结合
- 状态模式:管理游戏状态
public interface IGameState { void Enter(); void Exit(); } public class MenuState : IGameState { [Inject] private MenuView view; public void Enter() { view.Show(); } }- 策略模式:实现不同算法
public interface ISaveStrategy { void Save(int data); } public class BinarySave : ISaveStrategy { ... }8.2 ECS架构融合
对于性能敏感场景,可以结合ECS:
// Model作为Component public struct CounterData : IComponentData { public int Count; } // Presenter作为System public class CounterSystem : SystemBase { protected override void OnUpdate() { Entities.ForEach((ref CounterData data) => { data.Count++; }).Run(); } }8.3 网络通信处理
典型的分层处理:
- Model层:定义数据协议
- Service层:处理网络请求
- Presenter层:转换数据格式
- View层:展示最终结果
public class NetworkService { public async Task<PlayerData> FetchPlayerData() { // 调用API... } } public class PlayerPresenter { public async void LoadData() { var data = await networkService.FetchPlayerData(); view.UpdatePlayer(data.ToViewModel()); } }