255 lines
11 KiB
Markdown
255 lines
11 KiB
Markdown
|
---
|
|||
|
title: Slate学习笔记(二):UI混合使用方案
|
|||
|
date: 2021-03-12 15:40:04
|
|||
|
tags: Slate
|
|||
|
rating: ⭐️
|
|||
|
---
|
|||
|
## 前言
|
|||
|
之前在学习OnlineSubsytem,其中有一个将网络错误显示给用户的需求,这就需要实现一个MessageBox方案。所以在开发前我构造了以下几个思路:
|
|||
|
1. UMG作为MessageBox主体,使用GameInstance来管理UI。
|
|||
|
2. Slate作为MessageBox主体,使用GameViewportClient来管理UI。
|
|||
|
3. 混合使用,蓝图上使用UMG,c++使用Slate。
|
|||
|
|
|||
|
下面说一下我的测试结果——上述方案的优缺点:
|
|||
|
### UMG方案
|
|||
|
这种方案也有两种实现思路:
|
|||
|
1. 使用编辑器中创建的UMG Asset
|
|||
|
2. 使用C++创建的UUserWidget
|
|||
|
|
|||
|
第一种方案除开需要用反射系统来绑定委托就是完美方案。但因为本人没解决这个问题,所以就用了C++方案。当然使用第一种方案也会导致使用GameInstanceSubsystem会变得比较麻烦(可以通过函数的方式来赋予UMG Class,但对美术来说比较麻烦)<br>
|
|||
|
**优点**:使用蓝图迭代速度较快。
|
|||
|
|
|||
|
### Slate方案
|
|||
|
使用GameViewportClient作为UI管理。<br>
|
|||
|
**优点**:可以管理所有的UI,也就是说所有添加到屏幕上或者移除的UI都会调用AddViewportWidgetContent与RemoveViewportWidgetContent。在ShooterGame中通过重写这两个函数,使用2个SWidget数组控制各种情况下UI的显示与隐藏。比如:当LoadingScreen出现时隐藏所有UI,结束时回复。
|
|||
|
|
|||
|
那GameViewportClient可以管理UMG么?经过测试,调用RemoveViewportWidgetContent移除UMG对应的Slate(UMG本质就是使用UWidget包裹的Slate),UUserWiget也能正常销毁,理论是可以的。但很遗憾还在存在几个问题:
|
|||
|
1. 对于UMG这种套了一层UWidget的Slate,你无法分辨UWidget类型。
|
|||
|
2. 在GameViewportClient里调用CreateWidget()与RemoveFromParent()会出现Access Violation Rendering Location 0x000000 的问题,尝试将函数与成员都写在GameInstance中,之后在GameViewportClient调用时,RemoveFromParent()还是会出现这个问题。
|
|||
|
|
|||
|
## 实现
|
|||
|
对于**混合使用**:本人不太喜欢混合使用方案,本人认为应该把相关的管理代码都写在一个地方。本人最后使用了方案1。使用Slate实现底层。分别创建UWidget(作为UMG编辑器中的控件)与UUserWidget版本(MessageBox主体)。这么做的好处在于:
|
|||
|
1. 类型统一方便管理
|
|||
|
2. 默认样式统一
|
|||
|
3. C++与蓝图使用同一个函数
|
|||
|
|
|||
|
### 通过c++设置UUserWidget内容
|
|||
|
最早UMG刚出来是,可以通过RootWigdet(以前默认是UCanvasPannel)以AddChild方式添加UWiget。不过现在版本获取到RootWidget是空指针。所以只能通过RebuildWidget来设置UI控件了。这里定义了UMessageBoxWidget对象(UWidget),需要在Initialize()中初始化,NativeConstruct()会在RebuildWidget()后面调用。大致代码如下:
|
|||
|
```c++
|
|||
|
class RPGGAMEPLAYABILITY_API UMessageBox : public UUserWidget
|
|||
|
{
|
|||
|
GENERATED_BODY()
|
|||
|
public:
|
|||
|
virtual bool Initialize() override;
|
|||
|
|
|||
|
UPROPERTY(Instanced, BlueprintReadWrite,EditAnywhere)
|
|||
|
UMessageBoxWidget* MessageBox;
|
|||
|
};
|
|||
|
|
|||
|
bool UMessageBox::Initialize()
|
|||
|
{
|
|||
|
if (!Super::Initialize())
|
|||
|
return false;
|
|||
|
|
|||
|
MessageBox = NewObject<UMessageBoxWidget>(this, TEXT("MessageBox"));
|
|||
|
MessageBoxSize = FVector2D(500, 400);
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
TSharedRef<SWidget> UMessageBox::RebuildWidget()
|
|||
|
{
|
|||
|
return SNew(SConstraintCanvas)
|
|||
|
+ SConstraintCanvas::Slot()
|
|||
|
.Anchors(0.5)
|
|||
|
.Alignment(FVector2D(0.5, 0.5))
|
|||
|
.Offset(FMargin(0, 0, MessageBoxSize.X, MessageBoxSize.Y))
|
|||
|
[
|
|||
|
MessageBox->TakeWidget()
|
|||
|
];
|
|||
|
}
|
|||
|
```
|
|||
|
如果想要在子类中预览结果,可以实现SynchronizeProperties()与NativeTick();
|
|||
|
|
|||
|
### 绑定委托
|
|||
|
Slate上的FOnClicked委托是静态的,UWidget为了能实时绑定委托用的是动态多播委托(BlueprintAssignable只支持动态多播委托),而蓝图中UFUNCTION只支持动态单播委托作为形参。
|
|||
|
|
|||
|
所以这里我在UMessageBoxWidget中声明了用于在蓝图UFUNCTION中绑定的动态委托。之后再在与Slate静态委托绑定的Handle函数中调用即可。
|
|||
|
```c++
|
|||
|
FReply UMessageBoxWidget::SlateHandleConfirmButtonClicked()
|
|||
|
{
|
|||
|
TransmitOnConfirmButtonClickedEvent.ExecuteIfBound();
|
|||
|
OnConfirmButtonClicked.Broadcast();
|
|||
|
return FReply::Handled();
|
|||
|
}
|
|||
|
```
|
|||
|
注意2个委托的顺序,下面这段代码里我通过判断动态多播委托是否绑定函数,来决定是否需要绑定RemoveFromParent()来起到移除MessageBox的作用。因为上面的Handle函数在UWidget中非UUserWidget中,无法直接调用RemoveFromParent。无论是再传递委托到UUserWidget或者传递指针,都感觉比较啰嗦,所以我就这么写了。
|
|||
|
```c++
|
|||
|
UMessageBox* URPGGameInstanceBase::ShowMessageBox(const FText& MessageText, const FText& ConfirmText, const FText& CancelText,const FTransmitDynamicEvent& OnConfirm, const FTransmitDynamicEvent& OnCancel, const FVector2D MessageBoxSize)
|
|||
|
{
|
|||
|
UMessageBox* MessageBox=CreateWidget<UMessageBox>(GetWorld(), UMessageBox::StaticClass());
|
|||
|
MessageBox->MessageBox->MessageText = MessageText;
|
|||
|
MessageBox->MessageBox->ConfirmText = ConfirmText;
|
|||
|
MessageBox->MessageBox->CancelText = CancelText;
|
|||
|
MessageBox->MessageBoxSize= MessageBoxSize;
|
|||
|
|
|||
|
//判断是否有绑定,没绑定则直接绑定RemoveFromParent()
|
|||
|
if (!MessageBox->MessageBox->OnConfirmButtonClicked.IsBound())
|
|||
|
{
|
|||
|
MessageBox->MessageBox->OnConfirmButtonClicked.AddDynamic(MessageBox, &UMessageBox::RemoveFromParent);
|
|||
|
}
|
|||
|
if (!MessageBox->MessageBox->OnCancelButtonClicked.IsBound())
|
|||
|
{
|
|||
|
MessageBox->MessageBox->OnCancelButtonClicked.AddDynamic(MessageBox, &UMessageBox::RemoveFromParent);
|
|||
|
}
|
|||
|
|
|||
|
MessageBox->MessageBox->TransmitOnConfirmButtonClickedEvent = OnConfirm;
|
|||
|
MessageBox->MessageBox->TransmitOnCancelButtonClickedEvent = OnCancel;
|
|||
|
|
|||
|
MessageBox->AddToViewport((int32)EUIZOrder::EUIZOrder_MessageBox);
|
|||
|
return MessageBox;
|
|||
|
}
|
|||
|
```
|
|||
|
最后直接调用就可以了。
|
|||
|
```c++
|
|||
|
ShowMessageBox(FText::FromString("TestMessage!!!"),FText::FromString(TEXT("确定")),FText::FromString(TEXT("取消")));
|
|||
|
```
|
|||
|
|
|||
|
PS.在开发过程中我还遇到按钮需要点两下才能按到情况,后来发现是因为在Slate中Button的IsFocusable设置为false造成的,改成true就可以了。
|
|||
|
|
|||
|
## BindWidget meta
|
|||
|
在找资料的过程中发现这个网站的UI资料挺不错的:https://benui.ca/unreal,这里我简单说明一下BindWidget meta的用法:<br>
|
|||
|
使用c++开发UI时其中一个最大的问题就是:如何从C ++控制蓝图创建的Widget?
|
|||
|
可以使用BindWidget meta标签来解决。
|
|||
|
```c++
|
|||
|
#pragma once
|
|||
|
|
|||
|
#include "BindExample.generated.h"
|
|||
|
|
|||
|
UCLASS(Abstract)
|
|||
|
class UBindExample : public UUserWidget
|
|||
|
{
|
|||
|
GENERATED_BODY()
|
|||
|
|
|||
|
public:
|
|||
|
virtual void NativeConstruct() override;
|
|||
|
|
|||
|
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
|
|||
|
class UTextBlock* ItemTitle = nullptr;
|
|||
|
};
|
|||
|
```
|
|||
|
```
|
|||
|
#include "BindExample.h"
|
|||
|
#include "Components/TextBlock.h"
|
|||
|
|
|||
|
void UBindExample::NativeConstruct()
|
|||
|
{
|
|||
|
// Call the Blueprint "Event Construct" node
|
|||
|
Super::NativeConstruct();
|
|||
|
|
|||
|
// ItemTitle can be nullptr if we haven't created it in the
|
|||
|
// Blueprint subclass
|
|||
|
if (ItemTitle)
|
|||
|
{
|
|||
|
ItemTitle->SetText(TEXT("Hello world!"));
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
这样就可以在C++获得对应控件的指针了,除此之外还有其他meta选择:
|
|||
|
|
|||
|
- DesignerRebuild
|
|||
|
- BindWidget
|
|||
|
- BindWidgetOptional
|
|||
|
- OptionalWidget
|
|||
|
- BindWidgetAnim
|
|||
|
- BindWidgetAnimOptional
|
|||
|
- IsBindableEvent
|
|||
|
|
|||
|
其中Optional代表可选,也就是Widget不一定处于绑定状态,推荐使用。
|
|||
|
```c++
|
|||
|
// [PropertyMetadata] This property if changed will rebuild the widget designer preview. Use sparingly, try to update most properties by
|
|||
|
// setting them in the SynchronizeProperties function.
|
|||
|
// UPROPERTY(meta=(DesignerRebuild))
|
|||
|
DesignerRebuild,
|
|||
|
|
|||
|
// [PropertyMetadata] This property requires a widget be bound to it in the designer. Allows easy native access to designer defined controls.
|
|||
|
// UPROPERTY(meta=(BindWidget))
|
|||
|
BindWidget,
|
|||
|
|
|||
|
// [PropertyMetadata] This property optionally allows a widget be bound to it in the designer. Allows easy native access to designer defined controls.
|
|||
|
// UPROPERTY(meta=(BindWidgetOptional))
|
|||
|
BindWidgetOptional,
|
|||
|
|
|||
|
// [PropertyMetadata] This property optionally allows a widget be bound to it in the designer. Allows easy native access to designer defined controls.
|
|||
|
// UPROPERTY(meta=(BindWidget, OptionalWidget=true))
|
|||
|
OptionalWidget,
|
|||
|
|
|||
|
// [PropertyMetadata] This property requires a widget animation be bound to it in the designer. Allows easy native access to designer defined animations.
|
|||
|
// UPROPERTY(meta=(BindWidgetAnim))
|
|||
|
BindWidgetAnim,
|
|||
|
|
|||
|
// [PropertyMetadata] This property optionally allows a animation widget be bound to it in the designer. Allows easy native access to designer defined animation.
|
|||
|
// UPROPERTY(meta=(BindWidgetAnimOptional))
|
|||
|
BindWidgetAnimOptional,
|
|||
|
|
|||
|
// [PropertyMetadata] Exposes a dynamic delegate property in the details panel for the widget.
|
|||
|
// UPROPERTY(meta=(IsBindableEvent))
|
|||
|
IsBindableEvent,
|
|||
|
```
|
|||
|
|
|||
|
该网站其他感觉有用的技巧:
|
|||
|
- 虚幻4提高UI性能技巧:https://benui.ca/unreal/ui-performance/
|
|||
|
- UPROPERTY的编辑条件(EditCondition)与可以编辑条件(CanEditChange):https://benui.ca/unreal/uproperty-edit-condition-can-edit-change/
|
|||
|
- 虚幻4UI本地化技巧:https://benui.ca/unreal/ui-localization/
|
|||
|
|
|||
|
## 有关通过反射绑定Delegate的资料
|
|||
|
在写本文的时候找到了一些资料,打算以后再尝试。<br>
|
|||
|
Unlua的**FDelegatePropertyDesc**
|
|||
|
```c++
|
|||
|
virtual bool SetValueInternal(lua_State *L, void *ValuePtr, int32 IndexInStack, bool bCopyValue) const override
|
|||
|
{
|
|||
|
UObject *Object = nullptr;
|
|||
|
const void *CallbackFunction = nullptr;
|
|||
|
int32 FuncIdxInTable = GetDelegateInfo(L, IndexInStack, Object, CallbackFunction); // get target UObject and Lua function
|
|||
|
if (FuncIdxInTable != INDEX_NONE)
|
|||
|
{
|
|||
|
FScriptDelegate *ScriptDelegate = DelegateProperty->GetPropertyValuePtr(ValuePtr);
|
|||
|
FCallbackDesc Callback(Object->GetClass(), CallbackFunction);
|
|||
|
FName FuncName = FDelegateHelper::GetBindedFunctionName(Callback);
|
|||
|
if (FuncName == NAME_None)
|
|||
|
{
|
|||
|
// no delegate function is created yet
|
|||
|
lua_rawgeti(L, IndexInStack, FuncIdxInTable);
|
|||
|
int32 CallbackRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
|||
|
FDelegateHelper::Bind(ScriptDelegate, DelegateProperty, Object, Callback, CallbackRef);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
ScriptDelegate->BindUFunction(Object, FuncName); // a delegate function is created already
|
|||
|
}
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
https://zhuanlan.zhihu.com/p/333751025
|
|||
|
ZhuRong-HomoStation/UnrealEvent
|
|||
|
```c++
|
|||
|
void FEventBinder::BindToObject(UObject* InObject)
|
|||
|
{
|
|||
|
for (auto & BindMap : EventBindMap)
|
|||
|
{
|
|||
|
FMulticastDelegateProperty* Property = static_cast<FMulticastDelegateProperty*>(
|
|||
|
InObject->GetClass()->FindPropertyByName(BindMap.Key));
|
|||
|
|
|||
|
for (auto& Delegate : BindMap.Value.AllDelegates)
|
|||
|
{
|
|||
|
if (Delegate.BindFunction == NAME_None || !Delegate.TargetActor) continue;
|
|||
|
|
|||
|
FScriptDelegate BindDelegate;
|
|||
|
BindDelegate.BindUFunction(Delegate.TargetActor.Get(), Delegate.BindFunction);
|
|||
|
Property->AddDelegate(BindDelegate, InObject);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
```
|