PureMVC框架
Pure MVC是在基于模型、视图和控制器的MVC模式建立的一个轻量级的应用框架.
目前已经广泛应用于各类平台,常见用的语言如C#,Java等
本文尝试根据标准的C++PureMVC框架魔改成UE4版本方便使用的UnrealPureMVC(UPM)框架插件
- 参考文献
GitHub:puremvc-cpp-multicore-framework

PureMVC简单介绍
PureMVC框架把程序分为低耦合的三层:Model、View和 Controller。
在PureMVC首先实现了设计模式中的单例模式,其中以上3部分都是单例模式来管理,然后通过单例**Facade作为对外的唯一接口访问**
- Model
Model 保存对 Proxy 对象的引用,Proxy 负责操作数据模型,与远程服务通 信存取数据。
- Proxy
Proxy 负责操作数据模型,与远程服务通信存取数据;
- View
View 保存对 Mediator 对象的引用;
Mediator 对象来操作具体的视图组件,如UE4中的UMG组件
这样做是把视图的逻辑层和表现层剥离开
- Mediator:
Mediator操作具体的视图组件UI,包括:添加事件监听器,发送或接收 Notification ,直接改变视图组件的状态;
- Controller
Controller 保存所有 Command 的映射。
Command 类是无状态的,只在需 要时才被创建。
- Command
Command 可以获取 Proxy 对象并与之交互,发送 Notification,执行其他的 Command。

还有几点需要注意
- 新建我们自己的Facade类
- 用该类初始化各种需要初始化的Command/Proxy/Mediator
- M/V/C三个单例是内部类,尽量不要给外部访问,这里我们把3个单例的方法的
UFUNCTION宏都注释掉了
PureMVC通信机制和接口
先说明两个核心的事件**[发送消息/SendNotification]和[处理消息/HandleNotification]**,我们做的事件多数围绕着这两件事情而展开
Notifier
最为基类存在,实现了发送消息的方法SendNotification,同时提供如下方法,在初始化的时候设置必要参数
1 | void SetFacade(UMVC_Facade* facade); |
后面的Proxy,Command,Mediator均是继承自Notifier类
Modle/Proxy
M层使用了设计模式中的代理模式,即外部与数据的通信都通过Proxy类来交互而非直接对数据进行处理
M层提供了注册/获取/移除/判断 Proxy的接口
1 | void RegisterProxy(UMVC_Proxy* Proxy); |
一般情况我们在代码中需要实现一个Modle类和一个IModel接口,在这里我对此进行了简化,传统的IModel,甚至包括后面的IFacade,IView,IProxy等等接口都统统简化掉,把所有接口方法都放到对应的基类内
虽然有悖于依赖倒置原则,但是为了简化代码量,如有必要后面添加相应接口并把方法移动至接口内
Proxy类的主要则是存储数据,以及跟服务端通信
Proxy可以发送消息, 但是不能接收消息, 在官方文档中有解释,原因是如果Proxy能接受消息的话那么M层与VC两层的耦合就太高了
VC层可以接受来自Proxy的消息而对视图作为一定处理;反过来,VC层的改变不应该影响M层
我们在Proxy类内实现一些基础方法
1 | void SetInfo(const FString& Name); |
其中这些方法都不是必须实现的, 在蓝图层面, 我们可以在OnRegister和OnRemove两个方法最为启动和结束的入口函数对数据做一定的处理
View/Mediator
View类相比Model类会复杂一点,这里我们会用到几个新的类
Observer类应用了观察者模式,在注册每一个Mediator类的时候都会创建一个临时Observer类来作为观察者,在接受到特定消息以后将消息通知给Mediator类
如下代码简单解释了这一操作
1 | //注册mediator时 |
1 | //sendNotification后调用到的 |
- UMVC_Notification
此类作为一个封装类存在,在这里其实就是简单封装了一个字符串和UObject类,对应的就是SendNotification方法的2个参数;
当然, 我们后面可以自定义我们所需要的Notification类来添加更多的参数
在View类中,保存了2个字典以便于随时获取
1 | UPROPERTY() |
这里补充一点,一个消息字符串对应的是一个观察者数组, 因为观察同一个消息的可能有很多对象,而一个对象就对应一个Observer类
View主要代码如下
1 | void RegisterObserver(const FString& NotificationName, UMVC_Observer* observer); |
Mediator类作为非常核心的一个类而存在, 多数情况, 大多数的交互逻辑都放在此类中, 如发送消息打开某某界面等等
Mediator发送、声明、接收消息
Mediator类在创建后需要调用一个初始化方法,目的是让Mediator绑定一个具体对象, 如我们UE中的UMG类, 但其实MVC框架不仅仅只适合用与UI的管理, 我们同样可以Mediator来绑定其他对象比如我们玩家甚至我们的GameMode类
1 | void UMVC_Mediator::Init_Implementation(const FString& mediatorName, UObject* viewInstance) |
我们同样封装了一个蓝图函数库来用于创建Mediator类
1 | UMVC_Mediator* UFlib_UPM::CreateMediator(UObject* worldContext, TSubclassOf<UMVC_Mediator> mediatorClass, UObject* Instance, const FString& specialName) |
而我们后面派生的Mediator子类需要重写的函数是ListNotificationInterests和HandleNotification,也就是我们最前面说的两个事件,前者返回的数组是我们观察的消息,后者处理收到的消息后的逻辑
同样,在OnRegister和OnRemove事件中可以在启动和销毁时做处理, 如绑定我们GetViewInstance得到的对象的代理事件 或者 获取数据类Proxy

Controller/Command
MVC中的C,其实就是作为设计模式中的中介者模式存在,同时Command类又应用了命令模式
在PureMVC中的Command只作为一个无状态的类存在,在需要的时候被创建,执行逻辑以后就销毁
目的是方便了我们封装一些常用功能的逻辑,如显示某个UI,移除某个UI
Controller类的代码
1 | void Init(UMVC_View* view); |
我们会注意到有一个Init方法来获取一个View类对象,意味着其实我们VC两层是耦合的,
同样我们注册Command类的时候会创建一个观察者类Observer,注册的变量notificationName即Command对应的消息名称,参考我们Mediator对象的ListNotificationInterests方法,
观察者的对象是我们Controller本身,在接受到消息的时候直接调用ExecuteCommand命令
即Notification可以直接触发Comand执行
1 | void UMVC_Controller::RegisterCommand(const FString& notificationName, UMVC_Command* command) |
Command类可以获取Proxy和Mediator类,如下是我们后面的案例实现的一个PushUI的Command

我们会注意到有一个名称带Body的类,也就是我们上面提到的SendNotification方法中的UObject扩展类
我们显示/隐藏UI的时候提供的参数都使用的是MediatorName,因为围绕MVC的中心思想, 对视图的控制我们会集中在我们自己的框架中,每个UMG都是由一个Mediator管理,那么我们对视图的显示隐藏就直接用MediatorName类作为唯一参数是比较直观的
蓝图案例

我们简单实现了一个小游戏
包含功能
- 设置子弹数量
- 子弹自动回复
- 鼠标点击击杀怪物(盒子)
- 获取击杀数量数据和得分
- 右下角测试UI
- 测试UI切换
以上大致分为两部分内容,一部分是关于UMG的内容,另外是其他类

我们从Mediator类就可以发现,我们把GameMode以及如Enemy类也加入了我们的PureMVC框架

上图是GameMode中的内容,在运行初期就创建Facade以及把GameMode本身注册到框架内

Facade中的3个提供给蓝图重写的方法分别创建了初始的Mediator,Command以及Proxy
这里的Mediator对应的对象都是UMG类,因为也只有UMG类可以在最初的时候就先实例化出来(不显示到屏幕)
这里因为蓝图不方便用构造obj的方式来创建UMG,所以我们简单封装了一个构造UMG的方法
1 | UUserWidget* UFlib_UPM::CreateWidgetObject(UObject* worldContext, TSubclassOf<UUserWidget> umgClass) |
我们创建了2个蓝图Command类,用来显示/隐藏UI


如上图,两个Command类对UI进行处理,主要修改了对应的Proxy类的数据, 以及从Mediator信息得到的具体UMG对象进行处理

我们封装了2个显示/隐藏UI的宏,传入调用的mediator对象以及UI对应的MediatorClass类型
从这里我们就已经可以看到, UI与UI, UI与数据,UI与其他类之间已经完全解耦,逻辑部分都集中在
Mediator/Command中
同样的,我们也可以用Mediator来管理其他场景类的交互
扩展
思考:如果我们主要想使用的就是PureMVC中的收发消息功能, 我们已经有很多蓝图类而不想创建同样数量的Mediator类,那有没有办法让蓝图类本身就替代或者模拟Mediator类在框架中的角色呢
这里我们打算对UE一般的类都当作Mediator来处理,那么我们先声明一个接口
1 |
|
然后我们创建一个继承自UMVC_Mediator类 UPM_Mediator
1 | DECLARE_DYNAMIC_MULTICAST_DELEGATE(FUPMMulDlg); |
主要目的是作为一个中介连接UE类和PureMVC
拿OnRegister为例
1 | void UUPM_Mediator::OnRegister_Implementation() |
然后我们创建继承自UserWidget的UMG类UPM_Widget
1 | virtual void SendNotification_Implementation(const FString& notification, UObject* body) override; |
1 | void UUPM_Widget::RegisterUPMObject_Implementation(UMVC_Facade* facade) |
外部用来注册此类的方法来创建这个中介UUPM_Mediator,然后绑定代理
1 | UUPM_Widget* UFlib_UPMEx::CreateUPMWidget(UMVC_Facade* facade, TSubclassOf<UUPM_Widget> umgClass, const FString& specialName) |
实现一个蓝图库函数创建和注册此UMG类

封装一个蓝图宏库,直接把自己Push到屏幕;对于其他地方如果想Push/Pop该UMG,那么需要得到对应的MediatorName,这个可以自行扩展
同样的方法, 我们也可以申明同样的类,如
UPM_Actor,UPM_Pawn或者我们可以创建一个ActorComponent来实现这个同样的功能