MVC 模式的全称是 Model-View-Controller,即模型-视图-控制器。严格来说,MVC并不是GoF(四人帮)归纳出的一种独立设计模式,而是一种架构模式。这种模式常用于用户图形界面的图形 / 数据 / 逻辑分离,因此在游戏开发领域中常被用于 UI 相关的功能上。那么,什么是 M / V / C 呢?
- M(模型)
模型层(M 层)负责存放数据和业务逻辑。 - V(视图)
视图层(V 层)负责展示用户图形界面,是用户与之直接交互的元素。 - C(控制器)
控制层(C 层)负责响应用户输入或事件,更新 M 的数据并让 V 展示的界面刷新。
使用 MVC 模式的目的是将 M 和 V 的实现代码分离,从而使同一个程序可以使用不同的表现形式。比如一批统计数据可以分别用柱状图、饼图来表示。C 存在的目的则是确保 M 和 V 的同步,一旦 M 改变,V 应该同步更新。
Unity中的MVC
以上述框架为例,假设我们要做一个点击按钮更新金币数量(即点击一次金币数 +1 )和清空当前金币的功能:
Model(M)
M 层是纯数据类,不继承 Monobehavior,主要作用是存放所有数据,包括静态数据、变量、资源(如贴图、游戏对象等)等。对于 Model 的数据管理,可以通过序列化或 ScriptableObject 管理数据。
1 | [ ] |
既然我们强调 M 是纯数据类,为什么这里出现了增加金币和重置金币的逻辑 AddCoin
和 ResetCoins
呢?
其实在刚接触 MVC 时,我对 MVC 和 ECS 两个设计模式产生了混淆,认为 M 层类似 ECS 中的 Entity 只能存放数据,而业务逻辑应该放在 C 层(或者说 ECS 的 System),这其实是不对的。
M 层通常既包含数据,也包含与这些数据相关的业务逻辑,主要原因有下:
-
避免贫血模型
如果 Model 只存储数据,而所有业务逻辑都放到 Controller 中,那么 M 层就沦为了一种数据结构,这在很多架构思想中被称为“贫血模型”,容易导致业务逻辑分散在各个 Controller 中,后期维护和测试都不便。 -
Model 应该表达某个业务(例如金币相关管理)
一般来说,Model 不仅仅是数据的容器,它往往是对现实业务的抽象,包括该业务对象的属性和行为。在例子中的金币场景里,“增加金币”和“重置金币”就是金币的业务行为,与金币这个实体强相关,放在 CoinModel 中更符合面向对象的思路(而非 ECS 的面向数据思维),也让逻辑更清晰可测试。 -
Controller 应该负责调度
Controller 的职责是响应用户输入、调用 Model 的方法或更新 Model 的数据,然后让 View 去刷新到用户界面上。C 层通常不承担具体的业务运算,而是调用 Model 中的方法。如果把所有业务逻辑都丢给 Controller,会造成 Controller 过度膨胀,跟 View 绑定得更死,后期扩展也会麻烦。
View(V)
V 层负责管理所有 UI 组件对象,并为它们声明相关的事件:
1 | // View(管理 UI组件,监听 Model 变化) |
在 V 层中,我们添加了所有需要更新的和可以和玩家发生交互的 UI 组件(非静态 UI),例如上面的按钮和金币文本。除了手动一个个赋值之外,也可以在 Start
函数中通过 Find
方法查找并赋值。
Initialize
方法则为 UI 元素绑定了相应的事件,该方法在 Controller 中被初始化。
Controller(C)
C 层负责持有对 Model 和 View 的引用,响应用户输入或事件,更新 Model 并让 View 刷新。从下面的代码可以看出,Controller 确实只起到了调度和协调的作用,是 M 层和 V 层之间的桥梁。
1 | // Controller(协调 Model 和 View) |
在用户点击增加金币的按钮时,MVC 架构下的 UI 发生了这样一系列的事情:
- 按钮被点击,触发
OnClick
事件,通知所有订阅者 - View 中绑定的
onAdd
回调被执行 - Model 中的
AddCoin
函数被执行,更新金币数量同时触发OnCoinsChanged
事件 - View 中绑定的回调被执行,更新金币文本
实际上,MVC 模式不仅可以用于 UI 系统(只是在大部分情况下,它天然地适合于此),也可以拓展引用到其他模块上,例如这个仓库所展示的玩家系统。