- Published on
PureMVC框架
- Authors

- Name
- 东哥
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,同时提供如下方法,在初始化的时候设置必要参数
void SetFacade(UMVC_Facade* facade);
void SetWorldContext(UObject* worldContext);
后面的Proxy,Command,Mediator均是继承自Notifier类
Modle/Proxy
M层使用了设计模式中的代理模式,即外部与数据的通信都通过Proxy类来交互而非直接对数据进行处理
M层提供了注册/获取/移除/判断 Proxy的接口
void RegisterProxy(UMVC_Proxy* Proxy);
UMVC_Proxy* RetrieveProxy(const FString& ProxyName)const;
bool RemoveProxy(const FString& ProxyName);
bool HasProxy(const FString& ProxyName)const;
一般情况我们在代码中需要实现一个Modle类和一个IModel接口,在这里我对此进行了简化,传统的IModel,甚至包括后面的IFacade,IView,IProxy等等接口都统统简化掉,把所有接口方法都放到对应的基类内
虽然有悖于依赖倒置原则,但是为了简化代码量,如有必要后面添加相应接口并把方法移动至接口内
Proxy类的主要则是存储数据,以及跟服务端通信
Proxy可以发送消息, 但是不能接收消息, 在官方文档中有解释,原因是如果Proxy能接受消息的话那么M层与VC两层的耦合就太高了
VC层可以接受来自Proxy的消息而对视图作为一定处理;反过来,VC层的改变不应该影响M层
我们在Proxy类内实现一些基础方法
void SetInfo(const FString& Name);
void OnRegister();
void OnRemove();
FString GetProxyName()const;
void SetData(UObject* Data);
UObject* GetProxyData()const;
其中这些方法都不是必须实现的, 在蓝图层面, 我们可以在OnRegister和OnRemove两个方法最为启动和结束的入口函数对数据做一定的处理
View/Mediator
View类相比Model类会复杂一点,这里我们会用到几个新的类
Observer类应用了观察者模式,在注册每一个Mediator类的时候都会创建一个临时Observer类来作为观察者,在接受到特定消息以后将消息通知给Mediator类
如下代码简单解释了这一操作
//注册mediator时
UMVC_Observer* obs = `NewObject<UMVC_Observer>`();
obs->SetInfo(mediator, [mediator](UMVC_Notification* notification) {
mediator->HandleNotification(notification);
});
//sendNotification后调用到的
void UMVC_View::NotifyObservers_Implementation(UMVC_Notification* noitifyCation)
{
FString name = noitifyCation->GetName();
if (ObserverMap.Contains(name))
{
for (auto cur : ObserverMap[name].Observers)
{
cur->NotifyObserver(noitifyCation);
}
}
}
- UMVC_Notification
此类作为一个封装类存在,在这里其实就是简单封装了一个字符串和UObject类,对应的就是SendNotification方法的2个参数;
当然, 我们后面可以自定义我们所需要的Notification类来添加更多的参数
在View类中,保存了2个字典以便于随时获取
UPROPERTY()
TMap<FString, UMVC_Mediator*> MediatorMap;
UPROPERTY()
TMap<FString, FObserverArray> ObserverMap;
这里补充一点,一个消息字符串对应的是一个观察者数组, 因为观察同一个消息的可能有很多对象,而一个对象就对应一个Observer类
View主要代码如下
void RegisterObserver(const FString& NotificationName, UMVC_Observer* observer);
void RemoveObserver(const FString& NotificationName, UObject* notifyObject);")
void NotifyObservers(UMVC_Notification* noitifyCation);
void RegisterMeditor(UMVC_Mediator* mediator);
UMVC_Mediator* RetrieveMediator(const FString& mediatorName)const;
bool RemoveMediator(const FString& mediatorName);
bool HasMediator(const FString& mediatorName) ;
Mediator类作为非常核心的一个类而存在, 多数情况, 大多数的交互逻辑都放在此类中, 如发送消息打开某某界面等等
Mediator发送、声明、接收消息
Mediator类在创建后需要调用一个初始化方法,目的是让Mediator绑定一个具体对象, 如我们UE中的UMG类, 但其实MVC框架不仅仅只适合用与UI的管理, 我们同样可以Mediator来绑定其他对象比如我们玩家甚至我们的GameMode类
void UMVC_Mediator::Init_Implementation(const FString& mediatorName, UObject* viewInstance)
{
MediatorName = mediatorName;
ViewInstance = viewInstance;
UE_LOG(UPM, Log, TEXT("Meditor [%s, %s] Init "), *mediatorName,viewInstance?*(UKismetSystemLibrary::GetDisplayName(viewInstance)):TEXT(" NONE "));
}
我们同样封装了一个蓝图函数库来用于创建Mediator类
UMVC_Mediator* UFlib_UPM::CreateMediator(UObject* worldContext, `TSubclassOf<UMVC_Mediator>` mediatorClass, UObject* Instance, const FString& specialName)
{
if (mediatorClass && Instance)
{
UMVC_Mediator* m = `NewObject<UMVC_Mediator>`(worldContext, mediatorClass);
m->Init(specialName.IsEmpty() ? mediatorClass->GetName() : specialName, Instance);
m->SetWorldContext(worldContext);
return m;
}
return nullptr;
}
而我们后面派生的Mediator子类需要重写的函数是ListNotificationInterests和HandleNotification,也就是我们最前面说的两个事件,前者返回的数组是我们观察的消息,后者处理收到的消息后的逻辑
同样,在OnRegister和OnRemove事件中可以在启动和销毁时做处理, 如绑定我们GetViewInstance得到的对象的代理事件 或者 获取数据类Proxy

Controller/Command
MVC中的C,其实就是作为设计模式中的中介者模式存在,同时Command类又应用了命令模式
在PureMVC中的Command只作为一个无状态的类存在,在需要的时候被创建,执行逻辑以后就销毁
目的是方便了我们封装一些常用功能的逻辑,如显示某个UI,移除某个UI
Controller类的代码
void Init(UMVC_View* view);
void RegisterCommand(const FString& notificationName, UMVC_Command* command);
UMVC_Command* RetrieveCommand(const FString& notificationName);
void ExecuteCommand(UMVC_Notification* notification);
bool RemoveCommand(const FString& noitificationName);
bool HasCommand(const FString& notificationName)const;
我们会注意到有一个Init方法来获取一个View类对象,意味着其实我们VC两层是耦合的,
同样我们注册Command类的时候会创建一个观察者类Observer,注册的变量notificationName即Command对应的消息名称,参考我们Mediator对象的ListNotificationInterests方法,
观察者的对象是我们Controller本身,在接受到消息的时候直接调用ExecuteCommand命令
即Notification可以直接触发Comand执行
void UMVC_Controller::RegisterCommand(const FString& notificationName, UMVC_Command* command)
{
if (CommandMap.Contains(notificationName))
return;
CommandMap.Add(notificationName, command);
UMVC_Observer* obs = `NewObject<UMVC_Observer>`();
obs->SetInfo(this, [this](UMVC_Notification* notification)
{
ExecuteCommand(notification);
});
View->RegisterObserver(notificationName, obs);
}
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的方法
UUserWidget* UFlib_UPM::CreateWidgetObject(UObject* worldContext, `TSubclassOf<UUserWidget>` umgClass)
{
if (umgClass)
{
UUserWidget* m = `NewObject<UUserWidget>`(worldContext, umgClass);
return m;
}
return nullptr;
}
我们创建了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来处理,那么我们先声明一个接口
void SendNotification(const FString& notification,UObject* body);
void HandleNotification(UMVC_Notification* notification);
FString GetOwnMeditorName()const;
UMVC_Proxy* RetrieveProxy(const FString& ProxyName);
void OnRegister();
void OnRemove();
`TArray<FString>` ListNotificationInterests()const;
UMVC_Facade* GetOwnFacade()const;
UUPM_Mediator* GetOwnMediator()const;
void RegisterUPMObject(UMVC_Facade* facade);
然后我们创建一个继承自UMVC_Mediator类 UPM_Mediator
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FUPMMulDlg);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUPMMulDlgOneParam, UMVC_Notification*, notification);
/****/
UPROPERTY(BlueprintCallable,BlueprintAssignable)
FUPMMulDlg OnRegistered;
UPROPERTY(BlueprintCallable, BlueprintAssignable)
FUPMMulDlg OnRemoved;
UPROPERTY(BlueprintCallable, BlueprintAssignable)
FUPMMulDlgOneParam OnHandleNotification;
主要目的是作为一个中介连接UE类和PureMVC
拿OnRegister为例
void UUPM_Mediator::OnRegister_Implementation()
{
Super::OnRegister_Implementation();
OnRegistered.Broadcast();
}
然后我们创建继承自UserWidget的UMG类UPM_Widget
virtual void SendNotification_Implementation(const FString& notification, UObject* body) override;
virtual void HandleNotification_Implementation(UMVC_Notification* notification)override;
virtual FString GetOwnMeditorName_Implementation()const override;
virtual UMVC_Proxy* RetrieveProxy_Implementation(const FString& ProxyName)override;
virtual void OnRegister_Implementation()override;
virtual void OnRemove_Implementation()override;
virtual `TArray<FString>` ListNotificationInterests_Implementation()const override;
virtual UMVC_Facade* GetOwnFacade_Implementation()const override;
virtual void RegisterUPMObject_Implementation(UMVC_Facade* facade)override;
virtual UUPM_Mediator* GetOwnMediator_Implementation()const override;
/*UPM END*/
UPROPERTY()
FString CustomName;
protected:
UPROPERTY(Instanced)
UUPM_Mediator* ownMediator;
void UUPM_Widget::RegisterUPMObject_Implementation(UMVC_Facade* facade)
{
if (facade )
{
ownMediator = `NewObject<UUPM_Mediator>`();
// `CreateDefaultSubobject<UUPM_Mediator>`(TEXT("UPM_Mediator"));
ownMediator->Init(CustomName.IsEmpty()?this->GetName():CustomName, this);
ownMediator->SetWorldContext(this);
ownMediator->OnRemoved.AddDynamic(this, &UUPM_Widget::OnRemove);
ownMediator->OnRegistered.AddDynamic(this, &UUPM_Widget::OnRegister);
ownMediator->OnHandleNotification.AddDynamic(this, &UUPM_Widget::HandleNotification);
ownMediator->NotificationInterests = Execute_ListNotificationInterests(this);
facade->RegisterMediator(ownMediator);
return;
}
UE_LOG(UPM, Warning, TEXT("UPM Object Register Failed!! "));
}
外部用来注册此类的方法来创建这个中介UUPM_Mediator,然后绑定代理
UUPM_Widget* UFlib_UPMEx::CreateUPMWidget(UMVC_Facade* facade, `TSubclassOf<UUPM_Widget>` umgClass, const FString& specialName)
{
if (umgClass && facade)
{
UUPM_Widget* w = `NewObject<UUPM_Widget>`(facade->WorldContext, umgClass);
w->CustomName = specialName;
w->Execute_RegisterUPMObject(w, facade);
return w;
}
return nullptr;
}
实现一个蓝图库函数创建和注册此UMG类

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