PureMVC框架

Pure MVC是在基于模型、视图和控制器的MVC模式建立的一个轻量级的应用框架.

目前已经广泛应用于各类平台,常见用的语言如C#,Java等

本文尝试根据标准的C++PureMVC框架魔改成UE4版本方便使用的UnrealPureMVC(UPM)框架插件

github:UnrealPureMVC

  • 参考文献

PureMVC百度百科

GitHub:puremvc-cpp-multicore-framework

PureMVC官方中文文档

image-20201128123312313

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。

image-20201128134718774


还有几点需要注意

  • 新建我们自己的Facade类
  • 用该类初始化各种需要初始化的Command/Proxy/Mediator
  • M/V/C三个单例是内部类,尽量不要给外部访问,这里我们把3个单例的方法的UFUNCTION宏都注释掉了

PureMVC通信机制和接口

先说明两个核心的事件**[发送消息/SendNotification][处理消息/HandleNotification]**,我们做的事件多数围绕着这两件事情而展开

Notifier

最为基类存在,实现了发送消息的方法SendNotification,同时提供如下方法,在初始化的时候设置必要参数

1
2
void SetFacade(UMVC_Facade* facade);
void SetWorldContext(UObject* worldContext);

后面的Proxy,Command,Mediator均是继承自Notifier

Modle/Proxy

M层使用了设计模式中的代理模式,即外部与数据的通信都通过Proxy类来交互而非直接对数据进行处理

M层提供了注册/获取/移除/判断 Proxy的接口

1
2
3
4
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类内实现一些基础方法

1
2
3
4
5
6
void SetInfo(const FString& Name);
void OnRegister();
void OnRemove();
FString GetProxyName()const;
void SetData(UObject* Data);
UObject* GetProxyData()const;

其中这些方法都不是必须实现的, 在蓝图层面, 我们可以在OnRegisterOnRemove两个方法最为启动和结束的入口函数对数据做一定的处理

View/Mediator

View类相比Model类会复杂一点,这里我们会用到几个新的类

Observer类应用了观察者模式,在注册每一个Mediator类的时候都会创建一个临时Observer类来作为观察者,在接受到特定消息以后将消息通知给Mediator

如下代码简单解释了这一操作

1
2
3
4
5
//注册mediator时
UMVC_Observer* obs = NewObject<UMVC_Observer>();
obs->SetInfo(mediator, [mediator](UMVC_Notification* notification) {
mediator->HandleNotification(notification);
});
1
2
3
4
5
6
7
8
9
10
11
12
//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个字典以便于随时获取

1
2
3
4
UPROPERTY()
TMap<FString, UMVC_Mediator*> MediatorMap;
UPROPERTY()
TMap<FString, FObserverArray> ObserverMap;

这里补充一点,一个消息字符串对应的是一个观察者数组, 因为观察同一个消息的可能有很多对象,而一个对象就对应一个Observer

View主要代码如下

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
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子类需要重写的函数是ListNotificationInterestsHandleNotification,也就是我们最前面说的两个事件,前者返回的数组是我们观察的消息,后者处理收到的消息后的逻辑

同样,在OnRegisterOnRemove事件中可以在启动和销毁时做处理, 如绑定我们GetViewInstance得到的对象的代理事件 或者 获取数据类Proxy

image-20201128151312264

Controller/Command

MVC中的C,其实就是作为设计模式中的中介者模式存在,同时Command类又应用了命令模式

PureMVC中的Command只作为一个无状态的类存在,在需要的时候被创建,执行逻辑以后就销毁

目的是方便了我们封装一些常用功能的逻辑,如显示某个UI,移除某个UI

Controller类的代码

1
2
3
4
5
6
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,注册的变量notificationNameCommand对应的消息名称,参考我们Mediator对象的ListNotificationInterests方法,

观察者的对象是我们Controller本身,在接受到消息的时候直接调用ExecuteCommand命令

即Notification可以直接触发Comand执行

1
2
3
4
5
6
7
8
9
10
11
12
13
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类可以获取ProxyMediator类,如下是我们后面的案例实现的一个PushUICommand

image-20201128152841398

我们会注意到有一个名称带Body的类,也就是我们上面提到的SendNotification方法中的UObject扩展类

我们显示/隐藏UI的时候提供的参数都使用的是MediatorName,因为围绕MVC的中心思想, 对视图的控制我们会集中在我们自己的框架中,每个UMG都是由一个Mediator管理,那么我们对视图的显示隐藏就直接用MediatorName类作为唯一参数是比较直观的

蓝图案例

录制_2020_11_28_16_03_35_455

我们简单实现了一个小游戏

包含功能

  • 设置子弹数量
  • 子弹自动回复
  • 鼠标点击击杀怪物(盒子)
  • 获取击杀数量数据和得分
  • 右下角测试UI
  • 测试UI切换

以上大致分为两部分内容,一部分是关于UMG的内容,另外是其他类

image-20201130090855726

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

image-20201130091014189

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

image-20201130091148445

Facade中的3个提供给蓝图重写的方法分别创建了初始的Mediator,Command以及Proxy

这里的Mediator对应的对象都是UMG类,因为也只有UMG类可以在最初的时候就先实例化出来(不显示到屏幕)

这里因为蓝图不方便用构造obj的方式来创建UMG,所以我们简单封装了一个构造UMG的方法

1
2
3
4
5
6
7
8
9
UUserWidget* UFlib_UPM::CreateWidgetObject(UObject* worldContext, TSubclassOf<UUserWidget> umgClass)
{
if (umgClass)
{
UUserWidget* m = NewObject<UUserWidget>(worldContext, umgClass);
return m;
}
return nullptr;
}

我们创建了2个蓝图Command类,用来显示/隐藏UI

image-20201130091556643

image-20201130091607660

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

image-20201130092810959

我们封装了2个显示/隐藏UI的宏,传入调用的mediator对象以及UI对应的MediatorClass类型

从这里我们就已经可以看到, UI与UI, UI与数据,UI与其他类之间已经完全解耦,逻辑部分都集中在Mediator/Command

同样的,我们也可以用Mediator来管理其他场景类的交互

扩展

思考:如果我们主要想使用的就是PureMVC中的收发消息功能, 我们已经有很多蓝图类而不想创建同样数量的Mediator类,那有没有办法让蓝图类本身就替代或者模拟Mediator类在框架中的角色呢


这里我们打算对UE一般的类都当作Mediator来处理,那么我们先声明一个接口

1
2
3
4
5
6
7
8
9
10
11

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_MediatorUPM_Mediator

1
2
3
4
5
6
7
8
9
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为例

1
2
3
4
5
void UUPM_Mediator::OnRegister_Implementation()
{
Super::OnRegister_Implementation();
OnRegistered.Broadcast();
}

然后我们创建继承自UserWidget的UMG类UPM_Widget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,然后绑定代理

1
2
3
4
5
6
7
8
9
10
11
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类

image-20201130162447580

封装一个蓝图宏库,直接把自己Push到屏幕;对于其他地方如果想Push/Pop该UMG,那么需要得到对应的MediatorName,这个可以自行扩展


同样的方法, 我们也可以申明同样的类,如UPM_Actor,UPM_Pawn

或者我们可以创建一个ActorComponent来实现这个同样的功能