BlueRoseNote/03-UnrealEngine/UI/Slate学习笔记(二):UI混合使用方案.md
2023-06-29 11:55:02 +08:00

11 KiB
Raw Permalink Blame History

title, date, tags, rating
title date tags rating
Slate学习笔记UI混合使用方案 2021-03-12 15:40:04      Slate

前言

之前在学习OnlineSubsytem其中有一个将网络错误显示给用户的需求这就需要实现一个MessageBox方案。所以在开发前我构造了以下几个思路

  1. UMG作为MessageBox主体使用GameInstance来管理UI。
  2. Slate作为MessageBox主体使用GameViewportClient来管理UI。
  3. 混合使用蓝图上使用UMGc++使用Slate。

下面说一下我的测试结果——上述方案的优缺点:

UMG方案

这种方案也有两种实现思路:

  1. 使用编辑器中创建的UMG Asset
  2. 使用C++创建的UUserWidget

第一种方案除开需要用反射系统来绑定委托就是完美方案。但因为本人没解决这个问题所以就用了C++方案。当然使用第一种方案也会导致使用GameInstanceSubsystem会变得比较麻烦可以通过函数的方式来赋予UMG Class但对美术来说比较麻烦
优点:使用蓝图迭代速度较快。

Slate方案

使用GameViewportClient作为UI管理。
优点可以管理所有的UI也就是说所有添加到屏幕上或者移除的UI都会调用AddViewportWidgetContent与RemoveViewportWidgetContent。在ShooterGame中通过重写这两个函数使用2个SWidget数组控制各种情况下UI的显示与隐藏。比如当LoadingScreen出现时隐藏所有UI结束时回复。

那GameViewportClient可以管理UMG么经过测试调用RemoveViewportWidgetContent移除UMG对应的SlateUMG本质就是使用UWidget包裹的SlateUUserWiget也能正常销毁理论是可以的。但很遗憾还在存在几个问题

  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()后面调用。大致代码如下:

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函数中调用即可。

FReply UMessageBoxWidget::SlateHandleConfirmButtonClicked()
{
	TransmitOnConfirmButtonClickedEvent.ExecuteIfBound();
	OnConfirmButtonClicked.Broadcast();
	return FReply::Handled();
}

注意2个委托的顺序下面这段代码里我通过判断动态多播委托是否绑定函数来决定是否需要绑定RemoveFromParent()来起到移除MessageBox的作用。因为上面的Handle函数在UWidget中非UUserWidget中无法直接调用RemoveFromParent。无论是再传递委托到UUserWidget或者传递指针都感觉比较啰嗦所以我就这么写了。

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;
}

最后直接调用就可以了。

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的用法
使用c++开发UI时其中一个最大的问题就是如何从C ++控制蓝图创建的Widget 可以使用BindWidget meta标签来解决。

#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不一定处于绑定状态推荐使用。

// [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,

该网站其他感觉有用的技巧:

有关通过反射绑定Delegate的资料

在写本文的时候找到了一些资料,打算以后再尝试。
Unlua的FDelegatePropertyDesc

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

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);
		}
	}
}