2025-08-26 10:39:13 +08:00
|
|
|
|
---
|
|
|
|
|
title: UE5 MVVM 笔记
|
|
|
|
|
date: 2025-08-26 10:05:44
|
|
|
|
|
excerpt:
|
|
|
|
|
tags:
|
|
|
|
|
rating: ⭐
|
|
|
|
|
---
|
|
|
|
|
# 前言
|
|
|
|
|
- 知乎文章
|
|
|
|
|
- [UE5.6新功能MVVM使用方式(从笔记里迁移)](https://zhuanlan.zhihu.com/p/1918763422243325641)
|
2025-08-26 12:30:00 +08:00
|
|
|
|
|
|
|
|
|
- B站视频
|
|
|
|
|
- [第43期 | 虚幻引擎新UI——MVVM | quabqi](https://www.bilibili.com/video/BV1Dj411N735/?spm_id_from=333.1387.search.video_card.click&vd_source=d47c0bb42f9c72fd7d74562185cee290)
|
2025-08-26 10:39:13 +08:00
|
|
|
|
|
2025-08-26 13:33:02 +08:00
|
|
|
|
![[UMG_MVVM.png]]
|
2025-08-26 10:39:13 +08:00
|
|
|
|
# Example
|
|
|
|
|
```c++
|
|
|
|
|
UCLASS(Blueprintable)
|
|
|
|
|
class UMyViewModelBase : public UMVVMViewModelBase
|
|
|
|
|
{
|
|
|
|
|
GENERATED_BODY()
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
// FieldNotify:使得属性可以用于通知广播
|
|
|
|
|
// Setter:此属性可以被设置,Setter函数的名称格式 Set[Variable Name],CurrentHealth的Setter为:SetCurrentHealth
|
|
|
|
|
// Setter = [Function Name] 也可以自己指定名称,写法如前
|
|
|
|
|
// Getter:此属性可以被获取,Getter函数的名称格式为 Get[Variable Name],CurrentHealth的Getter为:GetCurrentHealth
|
|
|
|
|
// Getter = [Function Name] 也可以自己指定名称,写法如前
|
|
|
|
|
// 此字段在ViewModel中使用Get/Set访问,在蓝图中是Public的,在蓝图中ViewBanding使用Get/Set
|
|
|
|
|
UPROPERTY(BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
|
|
|
|
|
int32 CurrentHealth;
|
|
|
|
|
|
|
|
|
|
UPROPERTY(BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
|
|
|
|
|
int32 MaxHealth;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
void SetCurrentHealth(int32 NewCurrentHealth)
|
|
|
|
|
{
|
|
|
|
|
if (UE_MVVM_SET_PROPERTY_VALUE(CurrentHealth, NewCurrentHealth))
|
|
|
|
|
{
|
|
|
|
|
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercent);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetMaxHealth(int32 NewMaxHealth)
|
|
|
|
|
{
|
|
|
|
|
// 内部是一个模版函数,有特化版本,最终作用是做compare并触发Boradcast
|
|
|
|
|
// 原理也一样,就是通过名字传递Property做compare,
|
|
|
|
|
// 然后通过名字拿到Fiedld取Delegate进行boradcast
|
|
|
|
|
// ([Variable Name][NewValue])
|
|
|
|
|
if (UE_MVVM_SET_PROPERTY_VALUE(MaxHealth, NewMaxHealth))
|
|
|
|
|
{
|
|
|
|
|
// 如果MaxHealth改变了,HealthPercent也需要更新
|
|
|
|
|
// 内部通过名称拿到一个FieldId,然后通过FieldId找到对应的Delegate进行Boradcast
|
|
|
|
|
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32 GetCurrentHealth() const
|
|
|
|
|
{
|
|
|
|
|
return CurrentHealth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32 GetMaxHealth() const
|
|
|
|
|
{
|
|
|
|
|
return MaxHealth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
/**
|
|
|
|
|
* 必须具有带 FieldNotify 和 BlueprintPure 说明符的 UFUNCTION 宏。
|
|
|
|
|
* 不得带有参数。
|
|
|
|
|
* 必须是 const 函数。
|
|
|
|
|
* 必须仅返回单个值(没有输出参数)。
|
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
UFUNCTION(BlueprintPure, FieldNotify)
|
|
|
|
|
float GetHealthPercent() const
|
|
|
|
|
{
|
|
|
|
|
if (MaxHealth != 0)
|
|
|
|
|
{
|
|
|
|
|
return (float) CurrentHealth / (float) MaxHealth;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-08-26 11:56:17 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# MVVM
|
|
|
|
|
ViewModels管理器位于:Window -> ViewModels。
|
|
|
|
|
|
|
|
|
|
大致操作步骤:
|
|
|
|
|
1. 在该界面中创建ViewModels。
|
|
|
|
|
2. 选择ViewModels并且添加动态绑定的变量。
|
|
|
|
|
3. 修改CreationType(有4种类型)。
|
|
|
|
|
|
|
|
|
|
## MVVM CreationType
|
2025-08-26 16:05:57 +08:00
|
|
|
|
### Manual(看设计,可以一对多)
|
|
|
|
|
- 默认不创建,需要业务自己调用SetViewModel来关联
|
2025-08-26 16:42:12 +08:00
|
|
|
|
- 可以手动把同一个ViewModel绑定到个Widget中
|
2025-08-26 16:05:57 +08:00
|
|
|
|
|
2025-08-26 11:56:17 +08:00
|
|
|
|
### Create Instance (一对一)
|
2025-08-26 16:42:12 +08:00
|
|
|
|
- 在界面Construct时候会创建一个新的ViewModel
|
|
|
|
|
- 每个界面实例都是单独的ViewModel
|
2025-08-26 11:56:17 +08:00
|
|
|
|
|
|
|
|
|
### Global View Model Collection (全局共享)
|
2025-08-26 16:42:12 +08:00
|
|
|
|
- GlobalViewModelCollection自动从全局的MVVMSubsystem中获取指定Name和类型的ViewModel
|
|
|
|
|
- 业务必须手动创建一个,并添加到MVVMSubsyste
|
|
|
|
|
|
|
|
|
|
```c++
|
|
|
|
|
const auto Collection = GEngine->GetEngineSubsystem<UMVVMSubsystem>()->GetGlobalViewModelCollection()
|
|
|
|
|
FMVVMViewModelContext Context;
|
|
|
|
|
Context.ContextClass = ViewModelClass;
|
|
|
|
|
Context.ContextName = "Test";
|
|
|
|
|
|
|
|
|
|
// 把VM添加到UMVVMSubsystem
|
|
|
|
|
const auto VM = NewObject<UMVVMViewModelBase>(LocalPlayer, ViewModelClass, "Test");
|
|
|
|
|
Collection->AddViewModelInstance(Context, VM);
|
|
|
|
|
|
|
|
|
|
//查询VM
|
|
|
|
|
const auto VMResult = Collection->FindViewModelInstance(Context);
|
|
|
|
|
|
|
|
|
|
// 从UMVVMSubsystem移除VM
|
|
|
|
|
Collection->RemoveViewModel(Context);
|
|
|
|
|
```
|
2025-08-26 11:56:17 +08:00
|
|
|
|
|
|
|
|
|
Note:这里注意,AddViewModel必须提供ContextName,UE要求此名称必须和ViewMode的类名一致。
|
|
|
|
|
原因:MVVM中维护数据使用的是TArray,内存连续,可以通过索引快速查询。
|
|
|
|
|
|
|
|
|
|
数据结构:
|
|
|
|
|
```text
|
|
|
|
|
{
|
|
|
|
|
// 类型
|
|
|
|
|
TSubclassOf<UMVVMViewModelBase> ContextClass;
|
|
|
|
|
// 上下文信息,UE要求和类名一致
|
|
|
|
|
FName ContextName;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Property Path
|
|
|
|
|
类似如下写法:(加粗的self是默认的,不需要手动写,和函数默认的This类似)
|
|
|
|
|
**Self.**GetPlayerController.Vehicle.ViewModel
|
|
|
|
|
|
|
|
|
|
这个指定方式是说,通过当前的Widget获取PC,通过PC拿到载具上的ViewModel信息。
|
|
|
|
|
## 使用方法
|
|
|
|
|
1. 从MVVM界面中直接退拽进UMG编辑器。
|
|
|
|
|
2. Detail面板Binding。
|
|
|
|
|
3. 使用ViweBinding(Window→ViewBindings)
|
|
|
|
|
|
|
|
|
|
![[UMG_MVVM_ViewBindings.jpg|800]]绑定方向
|
|
|
|
|
OneTimeToWidget(←1):VM到控件,执行一次。
|
|
|
|
|
OneWayToWidget: VM到V,每次刷新都会通知
|
|
|
|
|
OneWayToViewModel :V到VM, 每次通知到VM,典型案例:编辑文本或图形选项,按钮需要自己包裹一下(**研究下给个合理方式**)
|
|
|
|
|
TwoWay:绑定在两个方向都可用,可以相互通知,不用担心陷入死循环,MVVMModelBase中的SetFunc,已经做好了处理,只有值不等才会Boradcast
|
|
|
|
|
|
|
|
|
|
### Conversion Function
|
|
|
|
|
何时使用?
|
|
|
|
|
如图:我想要在Text文本中显示我最大血量,最大血量是integer,如果直接绑定,compile时会有报错无法编译,此时就需要有转换函数出现了,通过转化函数将Integer转化为可用的FText
|
|
|
|
|
![[UMG_MVVM_ViewBindings_ConversionFunction.jpg|800]]
|
|
|
|
|
|
|
|
|
|
如何自定义转化函数:
|
|
|
|
|
如果我们是自己的某个接口,或者一个特殊的类型要有额外操作怎么处理,这个时候就需要自定义转化函数。
|
|
|
|
|
新的转换函数可以全局添加或在UserWidget(控件蓝图)上添加。函数不能是事件或网络,也不能弃用或仅限编辑器。函数需要对蓝图可见,有一个输入参数和一个返回值。如果在全局定义,函数还需要带有static关键字。如果在UserWidget中定义,函数还需要带有pure和const关键字。
|
|
|
|
|
一个在蓝图中转化的示例:
|
|
|
|
|
![[UMG_MVVM_ViewBindings_ConversionFunction_Custom.jpg|800]]![[UMG_MVVM_ViewBindings_ConversionFunction_CustomFunction.jpg|800]]
|
|
|
|
|
![[UMG_MVVM_ViewBindings_ConversionFunction_CustomFunction_Settings.jpg|800]]**Note:当前版本如果在Bindings赋值,然后再Deatails再次赋值,这样会出问题,绑定会失效,解决办法:从Bindings中删掉原来的绑定,重新赋值,(基于此原因,个人建议关掉DeatailsBind的入口)**
|
|
|
|
|
|
|
|
|
|
### 禁用旧版绑定
|
|
|
|
|
旧版的Bind并不是监听事件,而是通过Tick刷新。
|
|
|
|
|
新版确实是通过蓝图可用的多播委托去通知。
|
|
|
|
|
为了防止两种方式混用(**我们也不允许使用旧版方式绑定,性能洼地**),需要禁用旧版绑定。
|
|
|
|
|
|
|
|
|
|
#### DiablePropertBinding
|
|
|
|
|
![[UMG_DisablePropertBinding.jpg|800]]
|
|
|
|
|
开启上选项后将禁用传统属性绑定到Widget的选项,可以看到如图效果,BindList中只有ViewModel选项。
|
|
|
|
|
![[UMG_DisablePropertBinding_Result.jpg]]
|
|
|
|
|
|
|
|
|
|
#### ViewModel禁用DetailsBind
|
|
|
|
|
![[UMG_MVVM_DisableDetailsBinding.jpg|800]]
|
|
|
|
|
|
|
|
|
|
![[UMG_MVVM_DisableDetailsBinding_Result.jpg]]
|
2025-08-26 15:23:07 +08:00
|
|
|
|
|
|
|
|
|
# INotifyFieldValueChanged
|
|
|
|
|
|