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来实现这个同样的功能