106. UE5 GAS RPG 使用MVVM

MVVM 是 Model-View-ViewModel的缩写,个人理解它和MVC很相似,有区别的地方在于,在MVC里,Controller会服务多个View,而MVVM里,每个View都拥有一个单独的ViewModel,所以ViewModel相当于精简版的Controller。

在这一篇文章里,我们将使用UE5新增的MVVM插件实现对UI控件的动态更新。

创建用户控件

要实现存档,我们需要创建几个用户控件,用于玩家去创建或者加载对应的存储。

这里我们直接通过UserWidget创建蓝图控件

首先创建一个创建新存档的用户控件

效果如下,玩家在主菜单点击开始游戏,可以通过点击此控件+号创建一个新的存档。

接着创建一个点击加号后显示的控件,这里需要设置存档的名称

效果如下。

我们再创建一个可加载的存档的显示控件,这个控件里显示存档名称,玩家等级和所在的地图。

效果如下。

制作加载游戏界面

我们接下来创建一个新的UI,用于在加载进度的关卡内显示UI,首先我们创建一个新的控件蓝图,在里面添加一个画布面板,使用控件切换器来切换显示。

控件切换器可以设置索引,来显示当前设定的子控件,我们将之前制作的三个控件放置到其下面。

可以在细节里切换器里切换默认显示的子控件。

为了后续使用,我们可以将其创建为一个单独的用户控件。

在画布面板里放置三个。

在下面添加三个按钮,用于进入游戏,删除存档,以及返回主菜单。

接着,我们在加载存档的关卡打开关卡蓝图

在此蓝图里,我们加载此用户控件来显示。

使用MVVM

由于加载存档关卡专门对存档进行处理,我们为此关卡单独创建一个GameMode。

在HUD的c++类上面创建一个对应的蓝图类

设置好命名和保存路径。

将关卡使用的游戏模式修改

接着,我们创建一个新的HUD的c++类,用于单独处理加载存档关卡的UI相关。

命名定义好对应的路径

接着编译出类后,我们基于c++类创建一个蓝图类

在新的GameMode蓝图里,修改使用HUD。

接下来,我们要开启MVVM的插件,现在还是Beta版本,开启后注意重启。

我们创建一个基于MVVM基类的c++类,用于自定义

这个类专门用于处理加载存档关卡的相关。

接着,我们定义一个加载场景的部件的用户控件c++基类,继承UserWidget

它作为加载存档关卡的使用MVVM的基础用户控件。

在类里添加一个函数,蓝图可调用,蓝图可实现,为了方便后续初始化使用

cpp 复制代码
UCLASS()
class RPG_API ULoadScreenWidget : public UUserWidget
{
	GENERATED_BODY()

public:

	//蓝图实现初始化函数,主要用于设置MVVM
	UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
	void BlueprintInitializeWidget();
};

接着,我们通过c++类创建一个蓝图基类

然后修改所有加载存档关卡的用户控件的父类

接下来,我们将不再通过关卡蓝图去加载用户控件,因为我们有了自定义的HUD,我们在HUD里面去实现对用户控件的加载。

打开IDE,我们在HUD里增加一些代码,可以通过类去初始化一些实例。

cpp 复制代码
UCLASS()
class RPG_API ALoadScreenHUD : public AHUD
{
	GENERATED_BODY()

public:

	//存档关卡用户控件类
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UUserWidget> LoadScreenWidgetClass;

	//用户控件实例
	UPROPERTY(BlueprintReadOnly)
	TObjectPtr<ULoadScreenWidget> LoadScreenWidget;

	//MVVM使用的类
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UMVVM_LoadScreen> LoadScreenViewModelClass;

	//MVVM生成的实例
	UPROPERTY(BlueprintReadOnly)
	TObjectPtr<UMVVM_LoadScreen> LoadScreenViewModel;

protected:
	virtual void BeginPlay() override;
};

然后在BeginPlay函数里,去初始化。我们将之前在关卡蓝图里创建的内容在代码重复实现了出来。

cpp 复制代码
void ALoadScreenHUD::BeginPlay()
{
	Super::BeginPlay();

	//实例化MVVM
	LoadScreenViewModel = NewObject<UMVVM_LoadScreen>(this, LoadScreenViewModelClass);
	LoadScreenViewModel->SetWidgetName("WidgetName"); //测试代码

	//创建用户控件并添加到视口
	LoadScreenWidget = CreateWidget<ULoadScreenWidget>(GetWorld(), LoadScreenWidgetClass);
	LoadScreenWidget->AddToViewport();

	APlayerController* PC = GetOwningPlayerController();
	FInputModeUIOnly InputMode;
	InputMode.SetWidgetToFocus(LoadScreenWidget->TakeWidget());
	InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	
	PC->SetInputMode(InputMode);
	PC->SetShowMouseCursor(true);

	//创建完成用户控件后,调用用户控件函数
	LoadScreenWidget->BlueprintInitializeWidget();
}

有了对应的基类,我们再创建一个基于MVVM的蓝图类

接着,基于之前的HUD创建一个蓝图类,并在里面设置用户控件蓝图和MVVM类

防止出现歧义,将WBP_LoadMenu修改为WBP_LoadScreen

初始化MVVM

准备工作完成,我们可以在用户控件蓝图里添加ViewModel了,添加需要先选中设计器,然后在窗口里才能看到视图模型选项。

选中视图模型以后,我们可以为此用户控件添加所需的视图模型。

点击添加按钮后,左侧会列出来当前包含的所有的MVVM,我们设置添加。

这样添加了以后,你需要初始化视图模版,初始化有多种方式,可以根据需要进行设置。

这里我们采用属性路径的方式去获取,在用户控件的基类里,我们增加一个函数,通过playerController获取HUD身上的ViewModel

这个函数必须设置为常量

在我们选中了对应的ViewModel类以后,细节这里就会显示设置实例如何初始化,这里我们采用路径的方式,就需要实现从自身调用的函数。

注意

如果你只添加的MVVM,但是你没有使用,你在初始化的时候将会失败。如果你想测试,可以先随便添加一个属性,并绑定。

这里,我在MVVM里随意添加了一个属性,用户控件的名称,并设置对应的Getter和Setter的函数。

cpp 复制代码
UCLASS()
class RPG_API UMVVM_LoadScreen : public UMVVMViewModelBase
{
	GENERATED_BODY()

public:
	void SetWidgetName(const FString& InSlotName);
	FString GetWidgetName() const { return WidgetName; };
	
private:

	//用户控件的名称
	UPROPERTY(BlueprintReadOnly, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
	FString WidgetName;
	
};

在设置实现里,我们需要使用到内置的宏去定义。

UE_MVVM_SET_PROPERTY_VALUE 可以定义属性,并触发广播

UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED 如果用户控件绑定了是函数,这个可以触发函数的广播。

cpp 复制代码
void UMVVM_LoadScreen::SetWidgetName(const FString& InSlotName)
{
	if (UE_MVVM_SET_PROPERTY_VALUE(WidgetName, InSlotName))
	{
		// UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercent); //通过宏调用其它函数的广播
	}
}

然后,我们在初始化实例化时,调用了此函数实现了设置。然后,我们需要在用户控件里进行绑定,在窗口打开视图绑定

在视图绑定里,我们添加控件,然后选择绑定的方式,如果MVVM里返回的类型于显示类型不同,我们还需要设置转换函数,将字符串转换为文本显示。

转换这里,我们可以选择一次或者每次修改都触发,方向可以单向或者双向,看个人需求。

控件是我们添加到视图的左上角,我们不做任何修改,在运行时查看是否能够自动修改

或者在帧更新里打印MVVM的名称,如果能够获取到,也代表我们初始化完成。

运行,查看左上角是否修改正确,并且查看打印。

创建存档ViewModel

接下来,我们创建一个存档使用的ViewModel,这里提醒一下,一个用户控件可以使用多个ViewModel,一个ViewModel也可以应用到多个用户控件。

我们基于MVVMViewModelBase创建一个供存档用户控件专用的ViewModel,在这个类里,我们创建一个用于切换存档显示的用户控件的委托,一个存档位有三个用户控件显示,根据用户的需求进行切换。然后我们设置了一个初始化函数,在实例化后,进行一些处理使用。最后增加一个ViewModel名称显示功能,这个是为了防止在用户控件上实例化以后,没有绑定对应的属性时,导致实例化失败,无法获取对应的ViewModel实例。

cpp 复制代码
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSetWidgetSwitcherIndex, int32, WidgetSwitecherIndex);

/**
 * 
 */
UCLASS()
class RPG_API UMVVM_LoadSlot : public UMVVMViewModelBase
{
	GENERATED_BODY()

public:

	//切换存档显示的用户控件的委托
	UPROPERTY(BlueprintAssignable)
	FSetWidgetSwitcherIndex SetWidgetSwitcherIndex;

	void InitializeSlot() const;

	void SetSlotName(const FString& InSlotName);
	FString GetSlotName() const { return SlotName; };
private:
	
	//用户控件的名称
	UPROPERTY(BlueprintReadOnly, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
	FString SlotName;
};

初始化函数的实现,我们使用委托显示第一个就是创建插槽的用户控件。

cpp 复制代码
void UMVVM_LoadSlot::InitializeSlot() const
{
	SetWidgetSwitcherIndex.Broadcast(1);
}

然后设置存档使用的对应的ViewModel名称,每个存档都有一个对应的ViewModel实例。

cpp 复制代码
void UMVVM_LoadSlot::SetSlotName(const FString& InSlotName)
{
	UE_MVVM_SET_PROPERTY_VALUE(SlotName, InSlotName);
}

接着,我们要在UMVVM_LoadScreen的类里增加一些内容,用于处理一些事件。

我们预计要创建三个存档,那么在类里增加三个ViewModel的引用,防止只使用Map映射导致引用丢失(垃圾回收机制),然后创建一个映射,用于设置索引和ViewModel

cpp 复制代码
private:

	//用户控件的名称
	UPROPERTY(BlueprintReadOnly, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
	FString WidgetName;

	//索引和对应MVVM实例的映射
	UPROPERTY()
	TMap<int32, UMVVM_LoadSlot*> LoadSlots;

	//对象对MVVM实例的引用,防止垃圾回收机制对其进行回收
	UPROPERTY()
	TObjectPtr<UMVVM_LoadSlot> LoadSlot_0;
	
	UPROPERTY()
	TObjectPtr<UMVVM_LoadSlot> LoadSlot_1;
	
	UPROPERTY()
	TObjectPtr<UMVVM_LoadSlot> LoadSlot_2;

然后添加一个设置存档ViewModel使用的实例的类的参数,并创建一个函数,用于初始化。

cpp 复制代码
public:

	//每个存档插槽使用的MVVM类
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UMVVM_LoadSlot> LoadSlotViewModelClass;
	
	void InitializeLoadSlots();

在实现这里,我们初始化函数里,创建三个存档使用的ViewModel,并设置名称,并添加到映射。

cpp 复制代码
void UMVVM_LoadScreen::InitializeLoadSlots()
{
	LoadSlot_0 = NewObject<UMVVM_LoadSlot>(this, LoadSlotViewModelClass);
	LoadSlot_0->SetSlotName("LoadSlot_0");
	LoadSlots.Add(0, LoadSlot_0);
	LoadSlot_1 = NewObject<UMVVM_LoadSlot>(this, LoadSlotViewModelClass);
	LoadSlot_1->SetSlotName("LoadSlot_1");
	LoadSlots.Add(1, LoadSlot_1);
	LoadSlot_2 = NewObject<UMVVM_LoadSlot>(this, LoadSlotViewModelClass);
	LoadSlot_2->SetSlotName("LoadSlot_2");
	LoadSlots.Add(2, LoadSlot_2);
}

接着,我们增加一些函数,用于后续使用。

cpp 复制代码
	UFUNCTION(BlueprintPure)
	UMVVM_LoadSlot* GetLoadSlotViewModelByIndex(int32 Index) const;

	//创建新存档按下事件
	UFUNCTION(BlueprintCallable)
	void NewSlotButtonPressed(int32 Slot, const FString& EnterName);

	//开始新游戏按下事件
	UFUNCTION(BlueprintCallable)
	void NewGameButtonPressed(int32 Slot);

	//选择存档按下事件
	UFUNCTION(BlueprintCallable)
	void SelectSlotButtonPressed(int32 Slot);

在实现这里,通过索引获取存档ViewModel函数,直接从映射获取。

在开始新游戏按下事件里,我们通过委托修改显示的用户控件,用于调试。

cpp 复制代码
UMVVM_LoadSlot* UMVVM_LoadScreen::GetLoadSlotViewModelByIndex(const int32 Index) const
{
	return LoadSlots.FindChecked(Index);
}

void UMVVM_LoadScreen::NewSlotButtonPressed(int32 Slot, const FString& EnterName)
{
}

void UMVVM_LoadScreen::NewGameButtonPressed(int32 Slot)
{
	LoadSlots[Slot]->SetWidgetSwitcherIndex.Broadcast(1);
}

void UMVVM_LoadScreen::SelectSlotButtonPressed(int32 Slot)
{
}

创建完成,我们基于UMVVM_LoadSlot 创建一个蓝图类

接着,在UMVVM_LoadScreen的蓝图类里设置它作为存档ViewModel使用的类。

接着,我们在存档节点使用的用户控件基类蓝图WBP_LoadScreenWidget_Base里添加一个存档索引,记得把眼睛打开。

我们在存档使用的三个切换的用户控件里,添加两个ViewModel的绑定。

存档视图我们使用Manual(手动)设置的方式去设置,稍后将在蓝图里去手动设置。

而加载界面全局使用的我们通过基类创建的获取函数去获取即可。

然后在用户控件创建两个显示界面VIewModel和存档ViewModel名称的文本。

通过视图绑定绑定对应的名称,如果名称被修改后,将进行更新显示的内容

接着,我们在存档切换的用户控件WBP_LoadSlot_WidgetSwitcher里,添加一个函数,用于设置当前存档的索引。

然后通过ViewModel去获取对应索引的存档ViewModel

接着,对三个用户界面设置使用的存档ViewModel

最后调用自身的初始化函数

在初始化回调里,我们绑定委托回调,如果委托的索引发生改变,我们去切换对应的用户界面

在主界面的用户控件里,我们对每个存档切换器用户控件设置对应的索引,实现了切换。

到这里,我们实现了MVVM的整体绑定功能,后面为我们实现完整的存档功能打下了坚实的基础。

接下来,我们运行查看一下效果,查看对应的ViewModel的名称是否能够正确获取到,如果能够正确更新ViewModel的名称,证明我们获取正确。

如果不需要显示ViewModel的名称,我们可以将设置为不可视即可。

相关推荐
Zhichao_9719 分钟前
【UE5 C++课程系列笔记】32——读Json文件并解析
c++·ue5
吴梓穆10 小时前
UE5学习笔记 FPS游戏制作42 按钮添加回调函数
笔记·学习·ue5
沾血滴蔷薇10 小时前
虚幻5入门
数码相机·ue5
吴梓穆10 小时前
UE5学习笔记 FPS游戏制作39 制作一个带有背景的预制面板 使用overlay和nameSlot
笔记·学习·ue5
AgilityBaby10 小时前
UE5把动画导出为视频格式
ue5·游戏引擎·unreal engine
远离UE41 天前
UE5 Simulation Stage
ue5
吴梓穆1 天前
UE5学习笔记 FPS游戏制作38 继承标准UI
笔记·学习·ue5
蛋卷卷-1 天前
【UE5】发现意外的文件尾解决方法
ue5
吴梓穆2 天前
UE5学习笔记 FPS游戏制作37 蓝图函数库 自己定义公共方法
笔记·学习·ue5
吴梓穆2 天前
UE5学习笔记 FPS游戏制作41 世界模式显示UI
笔记·学习·ue5