细节面板的变量绑定逻辑

发布于 11 天前  5 次阅读


细节面板的变量绑定逻辑

todo uploading image

很久之前在细节面板实现过一个变量编辑的逻辑,基于 FInstancedPropertyBag 和 FInstancedStruct 实现了在细节面板的一些计算逻辑,功能为先,制作时大多是通过 IPropertyTypeCustomization 来完成的,反射的细节面板并不是特别漂亮,虽然运算时提供了筛选,但左值的选择不太直观,只能通过命名来确定,因此最近考虑研究下 UE 里的细节面板的变量绑定的逻辑是如何实现的,考虑把样式部分搞过来参考下。

写在前面

UE 里这一块的功能都很特异化,不是很易于拓展,网上也几乎没有相关拓展的资料。

若要完成自定义的功能,能够复用的大抵只有一个 UI,核心的绑定逻辑都是需要自己来实现的(除非逻辑很一致,可以参考 FSmartObjectDefinitionBindingExtensionFStateTreeBindingExtension)。

理论上目前有了 FInstancedPropertyBag,可以考虑提供一个方便拓展的方案,后续笔者凑点时间的话可能会考虑提供一个。

UMG 绑定流程分析

系统中有很多面板存在绑定逻辑,例如 UMG、动画蓝图、Control Rig、StateTree 等,笔者这里以 UMG 为例。

UI 创建

大多变量绑定的 UI 都是由继承 IDetailPropertyExtensionHandler 来实现的,以针对单行来自定义 (如 FStateTreeBindingExtension, FAnimGraphNodeBindingExtension 等)。UMG 这里是基于细节面板的自定义拓展的,但最终逻辑都是一致的:

class FBlueprintWidgetCustomization : public IDetailCustomization
{
...

/** IDetailCustomization interface */
virtual void CustomizeDetails( IDetailLayoutBuilder& DetailLayout ) override;

/** Make a property binding widget */
static TSharedRef<SWidget> MakePropertyBindingWidget(TWeakPtr<FWidgetBlueprintEditor> InEditor, UFunction* SignatureFunction, TSharedRef<IPropertyHandle> InDelegatePropertyHandle, bool bInGeneratePureBindings, bool bAllowDetailsPanelLegacyBinding);

...

void CreateEventCustomization( IDetailLayoutBuilder& DetailLayout, FDelegateProperty* Property, UWidget* Widget );
...
};

UI 构建的逻辑在 MakePropertyBindingWidget 方法里,核心就是 FPropertyBindingWidgetArgs 参数的构建,这里参数构建与业务密切相关,一会逐个详细解析,先整体捋一捋 UMG 绑定这一块的执行流程:

TSharedRef<SWidget> FBlueprintWidgetCustomization::MakePropertyBindingWidget(TWeakPtr<FWidgetBlueprintEditor> InEditor, UFunction* SignatureFunction, TSharedRef<IPropertyHandle> InPropertyHandle, bool bInGeneratePureBindings, bool bAllowDetailsPanelLegacyBinding)
{
...
    FPropertyBindingWidgetArgs Args;
    Args.MenuExtender = FExtender::Combine(MenuExtenders);
    Args.Property = InPropertyHandle->GetProperty();
    Args.BindableSignature = SignatureFunction;
    Args.BindButtonStyle = &FCoreStyle::Get().GetWidgetStyle<FButtonStyle>("Button");
...
    Args.OnAddBinding = FOnAddBinding::CreateLambda([InEditor, Objects](FName InPropertyName, const TArray<FBindingChainElement>& InBindingChain)
    Args.OnRemoveBinding = FOnRemoveBinding::CreateLambda([InEditor, Objects, InPropertyHandle, ActiveExtensions](FName InPropertyName)
...

    IPropertyAccessEditor& PropertyAccessEditor = IModularFeatures::Get().GetModularFeature<IPropertyAccessEditor>("PropertyAccessEditor");
    return PropertyAccessEditor.MakePropertyBindingWidget(InEditor.Pin()->GetBlueprintObj(), Args);
}

参数构建完成后,使用 IPropertyAccessEditor::MakePropertyBindingWidget 方法传入构建的参数即可完成 PropertyBinding SWidget 的构建,UMG 这里将返回的 SWidget 赋值到了 PropertyRow 的 ValueWidget 里,进而完成了细节面板反射 Value 部分的自定义:

void FBlueprintWidgetCustomization::CreateEventCustomization( IDetailLayoutBuilder& DetailLayout, FDelegateProperty* Property, UWidget* Widget )
{
TSharedRef<IPropertyHandle> DelegatePropertyHandle = DetailLayout.GetProperty(Property->GetFName(), Property->GetOwnerChecked<UClass>());
UWidgetBlueprint* BlueprintObj = Blueprint.Get();
if (!BlueprintObj)
    {
return;
    }

const bool bHasValidHandle = DelegatePropertyHandle->IsValidHandle();
if(!bHasValidHandle)
    {
return;
    }

IDetailCategoryBuilder& PropertyCategory = DetailLayout.EditCategory(FObjectEditorUtils::GetCategoryFName(Property), FText::GetEmpty(), ECategoryPriority::Uncommon);

IDetailPropertyRow& PropertyRow = PropertyCategory.AddProperty(DelegatePropertyHandle);
    PropertyRow.OverrideResetToDefault(FResetToDefaultOverride::Create(FResetToDefaultHandler::CreateSP(this, &FBlueprintWidgetCustomization::ResetToDefault_RemoveBinding)));

FString LabelStr = Property->GetDisplayNameText().ToString();
    LabelStr.RemoveFromEnd(TEXT("Event"));

FText Label = FText::FromString(LabelStr);

const bool bShowChildren = true;
    PropertyRow.CustomWidget(bShowChildren)
       .NameContent()
[
SNew(SHorizontalBox)
          .ToolTipText(Property->GetToolTipText())
+ SHorizontalBox::Slot()
          .AutoWidth()
          .VAlign(VAlign_Center)
          .Padding(0,0,5,0)
[
SNew(SImage)
             .Image(FAppStyle::Get().GetBrush("GraphEditor.Event_16x"))
]

          + SHorizontalBox::Slot()
          .VAlign(VAlign_Center)
[
SNew(STextBlock)
             .Text(Label)
]
       ]
.ValueContent()
       .MinDesiredWidth(200)
       .MaxDesiredWidth(250)
[
          MakePropertyBindingWidget(Editor.Pin(), Property->SignatureFunction, DelegatePropertyHandle, false, true)
];
}

Args 的构建

FPropertyBindingWidgetArgs Args 参数的构建需要我们创建一系列的代理、参数,以供 S 类的 UI 展示使用及交互时回调,

我们构建的参数最终会传递到 SPropertyBinding 的 成员 Args 中:

void SPropertyBinding::Construct(const FArguments& InArgs, UBlueprint* InBlueprint, const TArray<FBindingContextStruct>& InBindingContextStructs)
{
    Blueprint = InBlueprint;
    BindingContextStructs = InBindingContextStructs;

    Args = InArgs._Args;
...

在 UI 交互时,会借由 Args 转发并调用我们创建的代理来执行相关逻辑,例如:

void SPropertyBinding::HandleAddBinding(TArray<TSharedPtr<FBindingChainElement>> InBindingChain)
{
    if(Args.OnAddBinding.IsBound())
    {
        const FScopedTransaction Transaction(LOCTEXT("BindDelegate", "Set Binding"));

        TArray<FBindingChainElement> BindingChain;
        BindingChain.Reserve(InBindingChain.Num());
        Algo::Transform(InBindingChain, BindingChain, [](TSharedPtr<FBindingChainElement> InElement)
        {
            return *InElement.Get();
        });
        Args.OnAddBinding.Execute(PropertyName, BindingChain);
    }
}

或 TAttribute 类的:

FSlateColor SPropertyBinding::GetCurrentBindingColor() const
{
if(Args.CurrentBindingColor.IsSet())
    {
return Args.CurrentBindingColor.Get();
    }

return FLinearColor(0.25f, 0.25f, 0.25f);
}

因此核心的逻辑就是构建好这些代理的回调方法。

我们先以 UMG 中的 OnAddBinding 为例:

    Args.OnAddBinding = FOnAddBinding::CreateLambda([InEditor, Objects](FName InPropertyName, const TArray<FBindingChainElement>& InBindingChain)
    {
        UWidgetBlueprint* ThisBlueprint = InEditor.Pin()->GetWidgetBlueprintObj();
        UBlueprintGeneratedClass* SkeletonClass = Cast<UBlueprintGeneratedClass>(ThisBlueprint->SkeletonGeneratedClass);

        ThisBlueprint->Modify();

        TArray<FFieldVariant> FieldChain;
        Algo::Transform(InBindingChain, FieldChain, [](const FBindingChainElement& InElement)
        {
            return InElement.Field;
        });

        UFunction* Function = FieldChain.Last().Get<UFunction>();
        FProperty* Property = FieldChain.Last().Get<FProperty>();

        check(Function != nullptr || Property != nullptr);

        for (const TWeakObjectPtr<UObject>& ObjectPtr : Objects)
        {
            UObject* Object = ObjectPtr.Get();

            // Ignore null outer objects
            if ( Object == nullptr )
            {
                continue;
            }

            FDelegateEditorBinding Binding;
            Binding.ObjectName = Object->GetName();
            Binding.PropertyName = InPropertyName;
            Binding.SourcePath = FEditorPropertyPath(FieldChain);

            if ( Function != nullptr)
            {
                Binding.FunctionName = Function->GetFName();

                UBlueprint::GetGuidFromClassByFieldName<UFunction>(
                    Function->GetOwnerClass(),
                    Function->GetFName(),
                    Binding.MemberGuid);

                Binding.Kind = EBindingKind::Function;
            }
            else if( Property != nullptr )
            {
                Binding.SourceProperty = Property->GetFName();

                UBlueprint::GetGuidFromClassByFieldName<FProperty>(
                    SkeletonClass,
                    Property->GetFName(),
                    Binding.MemberGuid);

                Binding.Kind = EBindingKind::Property;
            }

            ThisBlueprint->Bindings.Remove(Binding);
            ThisBlueprint->Bindings.AddUnique(Binding);
        }

        FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(ThisBlueprint);
    });

OnAddBinding 提供了两个参数 DECLARE_DELEGATE_TwoParams(FOnAddBinding, FName /*InPropertyName*/, const TArray<FBindingChainElement>& /*InBindingChain*/);InPropertyName 是发起绑定的 Property 的 Name,InBindingChain 是选中的绑定目标的访问路径属性链,例如当我在 UMG 拖入一个进度条,并将 Percent 参数绑定到 UMG 蓝图中定义的一个 LinearColor 类型中的 R 时,传入的参数结果为:

todo uploading image

也正因此,这里获取最终的 Property 时取的是 Last()。

回调到这里,可以通用的绑定部分就已结束,剩下的就是如何具体完成绑定逻辑的构建了,绑定逻辑的这一块各个系统都不太一样,都是依赖这些来自 UI 回调的参数,以及我们在构建代理匿名函数时传入的信息,根据具体业务来具体实现的。

接下来来跟下 UMG 这一块的实现逻辑。

UMG 的绑定逻辑

(这里最好了解一些蓝图的基础知识,UMG 蓝图的相关逻辑不再赘述,可以简单理解为)

接上段的 OnAddBinding,会发现这一段代码的逻辑很简单,只是基于入参构造了一个 FDelegateEditorBinding Binding 对象,构建的参数值如:

todo uploading image

然后将其添加到了 ThisBlueprint->Bindings 中,这里的 ThisBlueprint 就是当前正在编辑的 UI 蓝图对象。

存储到这里以后,当我们编译时,便会将这一绑定关系传递到 UWidgetBlueprintGeneratedClass 的 TArray< FDelegateRuntimeBinding > Bindings 成员中:

void FWidgetBlueprintCompilerContext::FinishCompilingClass(UClass* Class)
{
    if (Class == nullptr)
        return;

    UWidgetBlueprint* WidgetBP = WidgetBlueprint();
...
        // Only check bindings on a full compile.  Also don't check them if we're regenerating on load,
        // that has a nasty tendency to fail because the other dependent classes that may also be blueprints
        // might not be loaded yet.
        const bool bIsLoading = WidgetBP->bIsRegeneratingOnLoad;
        if ( bIsFullCompile )
        {
            SanitizeBindings(BPGClass);

            // Convert all editor time property bindings into a list of bindings
            // that will be applied at runtime.  Ensure all bindings are still valid.
            for ( const FDelegateEditorBinding& EditorBinding : WidgetBP->Bindings )
            {
                if ( bIsLoading || EditorBinding.IsBindingValid(Class, WidgetBP, MessageLog) )
                {
                    BPGClass->Bindings.Add(EditorBinding.ToRuntimeBinding(WidgetBP));
                }
            }

示例说明

todo