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的基础用户控件。

在类里添加一个函数,蓝图可调用,蓝图可实现,为了方便后续初始化使用
UCLASS()
class RPG_API ULoadScreenWidget : public UUserWidget
{GENERATED_BODY()public://蓝图实现初始化函数,主要用于设置MVVMUFUNCTION(BlueprintImplementableEvent, BlueprintCallable)void BlueprintInitializeWidget();
};
接着,我们通过c++类创建一个蓝图基类

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

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

打开IDE,我们在HUD里增加一些代码,可以通过类去初始化一些实例。
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函数里,去初始化。我们将之前在关卡蓝图里创建的内容在代码重复实现了出来。
void ALoadScreenHUD::BeginPlay()
{Super::BeginPlay();//实例化MVVMLoadScreenViewModel = 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的函数。
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 如果用户控件绑定了是函数,这个可以触发函数的广播。
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实例。
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;
};
初始化函数的实现,我们使用委托显示第一个就是创建插槽的用户控件。
void UMVVM_LoadSlot::InitializeSlot() const
{SetWidgetSwitcherIndex.Broadcast(1);
}
然后设置存档使用的对应的ViewModel名称,每个存档都有一个对应的ViewModel实例。
void UMVVM_LoadSlot::SetSlotName(const FString& InSlotName)
{UE_MVVM_SET_PROPERTY_VALUE(SlotName, InSlotName);
}
接着,我们要在UMVVM_LoadScreen的类里增加一些内容,用于处理一些事件。
我们预计要创建三个存档,那么在类里增加三个ViewModel的引用,防止只使用Map映射导致引用丢失(垃圾回收机制),然后创建一个映射,用于设置索引和ViewModel
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使用的实例的类的参数,并创建一个函数,用于初始化。
public://每个存档插槽使用的MVVM类UPROPERTY(EditDefaultsOnly)TSubclassOf<UMVVM_LoadSlot> LoadSlotViewModelClass;void InitializeLoadSlots();
在实现这里,我们初始化函数里,创建三个存档使用的ViewModel,并设置名称,并添加到映射。
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);
}
接着,我们增加一些函数,用于后续使用。
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函数,直接从映射获取。
在开始新游戏按下事件里,我们通过委托修改显示的用户控件,用于调试。
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的名称,我们可以将设置为不可视即可。
相关文章:
106. UE5 GAS RPG 使用MVVM
MVVM 是 Model-View-ViewModel的缩写,个人理解它和MVC很相似,有区别的地方在于,在MVC里,Controller会服务多个View,而MVVM里,每个View都拥有一个单独的ViewModel,所以ViewModel相当于精简版的Co…...
Elasticsearch中什么是倒排索引?
倒排索引(Inverted Index)是一种索引数据结构,它在信息检索系统中被广泛使用,特别是在全文搜索引擎中。倒排索引允许系统快速检索包含给定单词的文档列表。它是文档内容(如文本)与其存储位置之间的映射&…...
深度学习:AT Decoder 详解
AT Decoder 详解 在序列到序列的模型架构中,自回归解码器(Autoregressive Translator, AT Decoder)是一种核心组件,其设计目标是确保生成的序列在语义和语法上的连贯性与准确性。自回归解码器通过逐步、依赖前一输出来生成新的输…...
pythons工具——图像的随机增强变换(只是变换了图像,可用于分类训练数据的增强)
从文件夹中随机选择一定数量的图像,然后对每个选定的图像进行一次随机的数据增强变换。 import os import random import cv2 import numpy as np from PIL import Image, ImageEnhance, ImageOps# 定义各种数据增强方法 def random_rotate(image, angle_range(-30…...
C++中volatile限定符详解
volatile是 C 和 C 中的一个类型限定符,它用于告诉编译器被修饰的变量具有特殊的属性,编译器在对该变量进行优化时需要特殊对待。以下是volatile限定符的主要作用: 1. 防止优化 内存访问顺序:在多线程环境或者与硬件交互的程序中…...
如何关闭Python解释器
方法1:采用sys.exit(0)正常终止程序,从图中可以看到,程序终止后shell运行不受影响。 方法2:采用os._exit(0)关闭整个shell,从图中看到,调用sys._exit(0)后整个shell都重启了(RESTART Shell&…...
《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项
《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项 《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项套接字可选项和 I/O 缓冲大小套接字多种可选项getsockopt & setsockoptSO_SNDBUF & SO_RCVBUF SO_REUSEADDR发生地址绑定…...
渗透测试---网络基础之HTTP协议与内外网划分
声明:学习素材来自b站up【泷羽Sec】,侵删,若阅读过程中有相关方面的不足,还请指正,本文只做相关技术分享,切莫从事违法等相关行为,本人一律不承担一切后果 目录 一、HTTP协议各版本介绍 二、HTTP请求的方…...
15分钟学 Go 第 45 天 : 使用Docker容器
第45天:使用Docker容器 目标 在本节中,我们将深入了解Docker及其基本用法,掌握如何使用Docker容器来简化开发和部署流程。 背景知识 Docker是一个开源平台,用于开发、运输和运行应用程序。它使我们能够使用容器技术将应用程序…...
DriveLM 论文学习
论文链接:https://arxiv.org/pdf/2312.14150 代码链接:https://github.com/OpenDriveLab/DriveLM 解决了什么问题? 当前,自动驾驶方案的性能仍然不足。一个必要条件就是泛化能力,需要模型能处理未经训练的场景或不熟…...
YoloV10改进策略:上采样改进|CARAFE,轻量级上采样|即插即用|附改进方法+代码
论文介绍 CARAFE模块概述:本文介绍了一种名为CARAFE(Content-Aware ReAssembly of FEatures)的模块,它是一种用于特征上采样的新方法。应用场景:CARAFE模块旨在改进图像处理和计算机视觉任务中的上采样过程࿰…...
光模块基础知识
1. 光模块的封装 光模块是光收发模块的简称,主要根据不同的外型来区分,而在同一外型中,又有着多种规格;在数据通信领域,最常见的光模块(根据外型区分)分别是SFF、GBIC、SFP、和XFP、QSFP 、XEN…...
【go从零单排】Closing Channels通道关闭、Range over Channels
🌈Don’t worry , just coding! 内耗与overthinking只会削弱你的精力,虚度你的光阴,每天迈出一小步,回头时发现已经走了很远。 📗概念 在 Go 语言中,通道(channel)的关闭是一个重要…...
初始JavaEE篇 —— 文件操作与IO
找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程程(ಥ_ಥ)-CSDN博客 所属专栏:JavaEE 目录 文件介绍 Java标准库中提供操作文件的类 文件系统操作 File类的介绍 File类的使用 文件内容操作 二进制文件的读写操作…...
GitLab实现 HTTP 访问和 SMTP 邮件发送
GitLab实现 HTTP 访问和 SMTP 邮件发送 本教程详细记录了如何配置 SMTP 邮件通知、实现外网 HTTP 访问,并分享在配置过程中遇到的问题及解决方法。 一、准备工作 安装 Docker:确保在 Synology NAS 上安装 Docker 应用。下载 GitLab 镜像:在…...
HarmonyOS ArkTS 下拉列表组件
Entry Component struct Index {defaultValue: string 下拉列表;// 定义选项数组,包含 value 和可选的 labeloptions: Array<SelectOption> [{ value: aaa },{ value: bbb },{ value: ccc },{ value: ddd },{ value: eee },{ value: fff },{ value: ggg },{…...
zabbix监控Linux系统
1. zabbix agent安装 #sudo rpm -Uvh https://repo.zabbix.com/zabbix/6.0/rhel/8/x86_64/zabbix-release-6.0-4.el8.noarch.rpm #sudo dnf clean all #yum install zabbix-agent -y Running transaction test Transaction test succeeded. Running transactionPreparing …...
线性表-数组描述补充 迭代器(C++)
补充线性表数组实现的迭代器部分 知识点: typedef是C语言中的一个关键字,它的主要作用是为一种数据类型定义一个新的名字(别名)。 在 C 的 STL(Standard Template Library)中,迭代器是连接容…...
vue3 + element-plus 的 upload + axios + django 文件上传并保存
之前在网上搜了好多教程,一直没有找到合适自己的,要么只有前端部分没有后端,要么就是写的不是很明白。所以还得靠自己摸索出来后,来此记录一下整个过程。 其实就是不要用默认的 action,要手动实现上传方式 http-reque…...
dm 创建数据库实例【window】
参考链接:配置实例 1)打开 DM 数据库配置助手 2)按照默认的进行 字符串大小写敏感:譬如 mysql 默认是大小写不敏感,如果在迁移中还选择了 保持对象大小写,那么就会出现一种情况就是每次查询等带有表名的都…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...
省略号和可变参数模板
本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...
嵌入式常见 CPU 架构
架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集,单周期执行;低功耗、CIP 独立外设;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel(原始…...
永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器
一、原理介绍 传统滑模观测器采用如下结构: 传统SMO中LPF会带来相位延迟和幅值衰减,并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF),可以去除高次谐波,并且不用相位补偿就可以获得一个误差较小的转子位…...
实战三:开发网页端界面完成黑白视频转为彩色视频
一、需求描述 设计一个简单的视频上色应用,用户可以通过网页界面上传黑白视频,系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观,不需要了解技术细节。 效果图 二、实现思路 总体思路: 用户通过Gradio界面上…...
