1 M-V-C
看下百度百科的定义:
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。
架构的兴起有它的时代和背景意义,那么随着时代的变迁,工具和大环境甚至是目标设备的变化,框架都是需要逐步调整的。
MVC出现的初衷是源自于与操作系统和软件的日益复杂化。MFC或者VB那种界面和逻辑揉在在一起的模式已经远远不能满足于大型复杂的功能系统开发。
然后随着软件行业的日益成熟,团队的职能分工也越来越明确,那么如何揉在一起的任务拆离出来,让不同职能的人专注于自己的领域和设计也是一个重要的方面。
基于很多的原因,软件UI框架开始分化为M-V-C的模式。M层专注于数据处理, V层专注实现的专注于表现,柱状图,饼状图、表格按你意愿。
一个典型的MVC的框架可以这么表示:
话说真的爱死 Unity的Animator编辑器了,好方便。
可以看到这是一个局部生态的自给自足。考虑了大部分的交互和变更情况,并通过规定每个部分的职能来满足功能需求。
但是MVC并不是完美的,严格来说它是只是一种指导性的框架,是综合了大部分的软件需求和时代发展结合得出的相对较优的方案。这种模式仍然存在它的缺陷:
1.局部生态,数据(M层)封闭,如果多个模块需要同一个数据块,数据之间的互通和重用性都非常的低效。
2.局部耦合,虽然在大的环境下实现了局部封闭,但是局部内的各个层之间的逻辑耦合还是很深。
想象一下,如果把上面的局部展示放大到游戏开发中的话会是什么样?
在这个情境下,MVC的框架就不能很好的应对了。游戏数据很多时候是互相依赖,并不能完全封闭。比如角色的属性展示需要用到背包里的装备,背包需要显示货币,货币可能受某些角色属性影响,任务依赖角色等级,同时奖励货币和道具、装备等等。
那么这种情况下,MVC要么需要将M写的非常复杂,每遇到一个新的需求开出一个新的接口,提供数据插叙或者变更,要么写一个通用的M管理器,用来统一中转数据。
在游戏开发这种需求多变,并且数据嘈杂的背景下,无论哪种都不能很好的应对需求,并且开发人员会受到无尽的折磨,在遵循架构和实现需求的之间来回纠结,摩擦,最终写的不伦不类,BUG还超多。
2 M-V-P
有需求就要有变化,当一个需求只是个别现象的时候,你可以特殊处理、特殊对待。但需求大批量出现的时候,就得重新审视现在的实现是不是需要重构或者升级了。
来看看谷歌的MVP模式:
https://github.com/googlesamples/android-architecture/tree/todo-mvp/github.com
MVP即Model-View-Presenter。
Model的工作就是完成对数据的操纵,数据的获取、存储、数据状态变化都是model层的任务,如网络请求,持久化数据增删改查等任务
View只处理视图相关,不做任何逻辑处理。
Presenter作为桥梁,处理所有二者之间的中转。
在这个模式下,M和V的连接被完全切断了,以前C层只是负责一些简单的转发和处理,现在P的任务变的更重,除了桥梁的作用之外,还需要做初步甚至高级的逻辑处理来处理M-V或者V-M的交流过程。
居然P的任务变重了,那么相对来说,P也会变得更加臃肿和难以维护,但是好处是将M和V彻底解耦,不管哪一方的实现方式发生变化,只要最终和P同步的数据不变另一方都不需要关心和修改。
另外V的逻辑功能一部分移入P之后,V可以更加专注的处理自身的表现,同时因为对接是通过接口实现的,所以满足接口的各种测试或者模拟都能够得以使用。
另外这里每一个V都对应一个P,写法非常复杂,软件复杂的时候,类和文件贼多。并且它仍然没有解决M复用的问题。
3 M-V-VM
MVVM是Model-View-ViewModel的简写。
从示意图上最直观的感受是两个:
1.使用ViewModel替代了Presenter。
2.原本P和V一对一的关系现在变为VM-V一对多的关系。
这解决了什么问题呢?
1.VM在一定程度上能够重用,就表示M层在一定程度上也可以复用了。
2.VM一对多的关系,表示在类和文件的数量和管理上要减轻很多。
VM是通过DataBinding的技术实现V和M之间的关系映射。抛弃了P层的手动关系接口和维护。当然每种技术都有其存在的意义和解决的问题。至于选取什么样的方案去解决问题,就要看项目自己的需求更符合哪一类的设计。如果都没有那就需要自己去实现变种或者是新的设计,当然也可以修改需求的啦。
4.Unity ECS框架
在ECS框架中,原本类似MonoBehavior的职能被拆分成纯数据以及操作数据的系统,所以你将会看到大量xxxComponent,xxxSystem的类。比如接下来我们就要建立一个只带移动方向的移动组件、修改移动方向的输入系统以及控制移动的移动系统。
表示移动数据的MoveComponent.cs 数据类
` using Unity.Entities;
using UnityEngine;
public class MoveComponent : MonoBehaviour
{
public Vector3 moveDir;
}`
表示移动系统的MoveSystem.cs
using Unity.Entities;
using UnityEngine;
public class MoveSystem : ComponentSystem
{
public struct Filter
{
public Transform tf;
public MoveComponent moveComponent;
}
protected override void OnUpdate()
{
foreach (var entity in GetEntities<Filter>())
{
Vector3 pos = entity.tf.position + entity.moveComponent.moveDir * Time.deltaTime * 3;
entity.tf.position = pos;
}
}
}
表示输入系统的InputSystem.cs
using Unity.Entities;
using UnityEngine;
public class InputSystem : ComponentSystem
{
struct Data
{
public ComponentArray<MoveComponent> moveArray;
}
[Inject] private Data _data;
protected override void OnUpdate()
{
for (int i = 0; i < _data.moveArray.Length; ++i)
{
_data.moveArray[i].moveDir = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0);
}
}
}
在上面的例子里,输入系统每帧检测输入,把输入赋给移动组件,_data是包含所有MoveComponent的数组,这个数组是通过[Inject]属性标签注入进去的。这个注入的标签很有意思,它会匹配好里面你需要的组件给你放进去,它的原理我们找时间再深入研究。而移动系统就更简单了,就是遍历所有移动组件,然后根据moveDir去移动它对应的transform。
大概你也注意到了InputSystem和MoveSystem的遍历方式是不一样的,这是为了演示可以有不同的遍历方式。而且要注意,上面例子里MoveComponent继承自MonoBehavior,如果它继承自IComponentData,它就只能是一个struct,而struct并不能够像MoveSystem那样去遍历,而且也不能直接用组件方式添加到一个GameObject上了。
继承自IComponentData的好处自然是让纯数据的组件以及系统与GameObject分离,试想一下你可以同时跑100w个移动的对象,但这些对象却不一定需要一个GameObject,这就是ECS带来性能提升的一大原因。
你可能觉得奇怪,球上只添加了两个组件,怎么就移动了,MoveSystem既没new出来也没拖到什么对象上,谁在激活这些系统?答案是世界,当然这是另一个话题了,在本篇里你只需要知道有个“世界”在运行你写的System就够了,只要你写了个系统继承自ComponentSystem,它就会被执行。