细节面板的变量绑定逻辑
很久之前在细节面板实现过一个变量编辑的逻辑,基于 FInstancedPropertyBag 和 FInstancedStruct 实现了在细节面板的一些计算逻辑,功能为先,制作时大多是通过 IPropertyTypeCustomization 来完成的,反射的细节面板并不是特别漂亮,虽然运算时提供了筛选,但左值的选择不太直观,只能通过命名来确定,因此最近考虑研究下 UE 里的细节面板的变量绑定的逻辑是如何实现的,考虑把样式部分搞过来参考下。
写在前面:
UE 里这一块的功能都很特异化,不是很易于拓展,网上也几乎没有相关拓展的资料。
若要完成自定义的功能,能够复用的大抵只有一个 UI,核心的绑定逻辑都是需要自己来实现的(除非逻辑很一致,可以参考
FSmartObjectDefinitionBindingExtension
和FStateTreeBindingExtension
)。
最终效果:
UMG 绑定流程分析
UMG 绑定的整体流程很长,并且除了 UI 的部分其实大部分对完成后续的 Sample 没有太多帮助,也可以跳过这部分,直接查看示例说明(笔者看的头昏脑胀的 )。
系统中有很多面板存在绑定逻辑,例如 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 方法里,最终会调用到 IPropertyAccessEditor::MakePropertyBindingWidget 方法,这里有两个重载,UMG 用的首个需要传入 UBlueprint,我们自定义拓展时可以使用需要传入结构数组的,这里的核心就是 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 )
{
...
IDetailPropertyRow& PropertyRow = PropertyCategory.AddProperty(DelegatePropertyHandle);
PropertyRow.OverrideResetToDefault(FResetToDefaultOverride::Create(FResetToDefaultHandler::CreateSP(this, &FBlueprintWidgetCustomization::ResetToDefault_RemoveBinding)));
...
PropertyRow.CustomWidget(bShowChildren)
.NameContent()
[
...
]
.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 时,传入的参数结果为(下文都以这一 Property 的绑定为例):
也正因此,这里获取最终的 Property 时取的是 Last()。
回调到这里,可以通用的绑定部分就已结束,剩下的就是如何具体完成绑定逻辑的构建了,绑定逻辑的这一块各个系统都不太一样,都是依赖这些来自 UI 回调的参数,以及我们在构建代理匿名函数时传入的信息,根据具体业务来具体实现的。
接下来来跟下 UMG 这一块的实现逻辑。
UMG 的绑定逻辑
(这里最好了解一些蓝图的基础知识,这里不再赘述,可以简单理解为 UWidgetBlueprint -> UMG 资源类;UBlueprintGeneratedClass -> 蓝图运行时类;UUserWidget -> 运行时对象。)
接上段的 OnAddBinding,会发现这一段代码的逻辑很简单,只是基于入参构造了一个 FDelegateEditorBinding Binding 对象,构建的参数值如:
然后将其添加到了 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));
}
}
执行 UUserWidget::Initialize 时,便会依赖这一成员,逐步完成 UWidget 的 TArray<TObjectPtr <UPropertyBinding>
> NativeBindings
的构建:
UUserWidget::Initialize
UWidgetBlueprintGeneratedClass::InitializeWidget
UWidgetBlueprintGeneratedClass::InitializeWidgetStatic
UWidgetBlueprintGeneratedClass::InitializeBindingsStatic
UWidget::AddBinding
最终调用到 UWidget::AddBinding 时参数如下:
BindingPath 可以看出,就是我们上文测试时绑定的参数路径。
DelegateProperty 是使用 Binding.PropertyName 组合 TEXT("Delegate") 得到的成员名获取的 FDelegateProperty:
void UWidgetBlueprintGeneratedClass::InitializeBindingsStatic(UUserWidget* UserWidget, const TArrayView<const FDelegateRuntimeBinding> InBindings, const TMap<FName, FObjectPropertyBase*>& InPropertyMap)
{
...
FDelegateProperty* DelegateProperty = FindFProperty<FDelegateProperty>(Widget->GetClass(), FName(*(Binding.PropertyName.ToString() + TEXT("Delegate"))));
if (!DelegateProperty)
{
DelegateProperty = FindFProperty<FDelegateProperty>(Widget->GetClass(), Binding.PropertyName);
}
if (DelegateProperty)
{
bool bSourcePathBound = false;
if (Binding.SourcePath.IsValid())
{
bSourcePathBound = Widget->AddBinding(DelegateProperty, UserWidget, Binding.SourcePath);
}
这里注意是用类获取的 FDelegateProperty 对象,可以理解为委托成员的反射元数据,借由这一数据后续才可以取到对象中真正的委托实例。
这也是为什么我们在实现自定义的 UWidget 控件时,要想提供绑定能力,还需要提供一个带对应返回值类型的代理成员变量的原因:
class UProgressBar : public UWidget
{
...
/** Used to determine the fill position of the progress bar ranging 0..1 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Getter, Setter, BlueprintSetter="SetPercent", Category="Progress", meta = (UIMin = "0", UIMax = "1"))
float Percent;
...
/** A bindable delegate to allow logic to drive the text of the widget */
UPROPERTY()
FGetFloat PercentDelegate;
...
组织好这些信息后,最终会在 GenerateBinder 方法中,创建对应的 PropertyBinding 对象:
static UPropertyBinding* GenerateBinder(FDelegateProperty* DelegateProperty, UObject* Container, UObject* SourceObject, const FDynamicPropertyPath& BindingPath)
{
FScriptDelegate* ScriptDelegate = DelegateProperty->GetPropertyValuePtr_InContainer(Container);
if ( ScriptDelegate )
{
// Only delegates that take no parameters have native binders.
UFunction* SignatureFunction = DelegateProperty->SignatureFunction;
if ( SignatureFunction->NumParms == 1 )
{
if ( FProperty* ReturnProperty = SignatureFunction->GetReturnProperty() )
{
TSubclassOf<UPropertyBinding> BinderClass = UWidget::FindBinderClassForDestination(ReturnProperty);
if ( BinderClass != nullptr )
{
UPropertyBinding* Binder = NewObject<UPropertyBinding>(Container, BinderClass);
Binder->SourceObject = SourceObject;
Binder->SourcePath = BindingPath;
Binder->Bind(ReturnProperty, ScriptDelegate);
return Binder;
}
}
}
}
return nullptr;
}
这一段代码首先借由委托元数据对象从 Container 中获取了实际的 FScriptDelegate,这个参数维护了真正的单播委托的绑定关系,其内存储了一个 UObject 指针和 FName 函数名,用于调用 UObject 的成员函数,此时这两个参数都为 NULL。
而后获取了 SignatureFunction,即指向该 FDelegateProperty 所代表的委托类型的 UFunction,这里确保了是无参有返回值的委托,然后基于委托返回值类型,查找应使用的绑定类:
TSubclassOf<UPropertyBinding> UWidget::FindBinderClassForDestination(FProperty* Property)
{
if (BinderClasses.IsEmpty())
{
TArray<UClass*> PropertyBindingClasses;
GetDerivedClasses(UPropertyBinding::StaticClass(), PropertyBindingClasses);
BinderClasses.Reserve(PropertyBindingClasses.Num());
for (UClass* PropertyBindingClass : PropertyBindingClasses)
{
BinderClasses.Emplace(PropertyBindingClass);
}
}
for (TSubclassOf<UPropertyBinding>& BinderClass : BinderClasses)
{
if (BinderClass.GetDefaultObject()->IsSupportedDestination(Property))
{
return BinderClass;
}
}
return nullptr;
}
这里就是获取了继承自 UPropertyBinding 的类,并取其 CDO 调用 IsSupportedDestination 方法来获取匹配的绑定类型。
例如本例中的,调用到 UFloatBinding 的相关方法时会匹配成功,返回该类:
bool UFloatBinding::IsSupportedDestination(FProperty* Property) const
{
return IsSupportedSource(Property);
}
bool UFloatBinding::IsSupportedSource(FProperty* Property) const
{
return IsConcreteTypeCompatibleWithReflectedType<float>(Property) || IsConcreteTypeCompatibleWithReflectedType<double>(Property);
}
而后基于返回的类,创建了 UFloatBinding 的对象,并传入 SourceObject 与 BindingPath,这里的 Outer 值定的是 UWdiegt 小控件。
最后执行 Binder->Bind(ReturnProperty, ScriptDelegate) 方法,来初始化一开始获取的 FScriptDelegate 对象。
方法内很简单:
void UPropertyBinding::Bind(FProperty* Property, FScriptDelegate* Delegate)
{
static const FName BinderFunction(TEXT("GetValue"));
Delegate->BindUFunction(this, BinderFunction);
}
UFloatBinding 这里没有重载 Bind 方法,故调用基类方法将这一代理绑定到了 GetValue 方法。
因为前面做了绑定类的筛选,所以理论上 GetValue 的返回类型是匹配的,UFloatBinding 的 GetValue 方法为:
float UFloatBinding::GetValue() const
{
//SCOPE_CYCLE_COUNTER(STAT_UMGBinding);
if ( UObject* Source = SourceObject.Get() )
{
// Since we can bind to either a float or double, we need to perform a narrowing conversion where necessary.
// If this isn't a property, then we're assuming that a function is used to extract the float value.
float FloatValue = 0.0f;
if (SourcePath.Resolve(Source))
{
double DoubleValue = 0.0;
if (SourcePath.GetValue<float>(Source, FloatValue))
{
return FloatValue;
}
else if (SourcePath.GetValue<double>(Source, DoubleValue))
{
FloatValue = static_cast<float>(DoubleValue);
return FloatValue;
}
}
}
return 0.0f;
}
这里的 GetValue 是调用的 FDynamicPropertyPath 的方法:
USTRUCT()
struct FDynamicPropertyPath : public FCachedPropertyPath
{
GENERATED_USTRUCT_BODY()
public:
/** */
UMG_API FDynamicPropertyPath();
/** */
UMG_API FDynamicPropertyPath(const FString& Path);
/** */
UMG_API FDynamicPropertyPath(const TArray<FString>& PropertyChain);
/** Get the value represented by this property path */
template<typename T>
bool GetValue(UObject* InContainer, T& OutValue) const
{
FProperty* OutProperty;
return GetValue<T>(InContainer, OutValue, OutProperty);
}
/** Get the value and the leaf property represented by this property path */
template<typename T>
bool GetValue(UObject* InContainer, T& OutValue, FProperty*& OutProperty) const
{
return PropertyPathHelpers::GetPropertyValue(InContainer, *this, OutValue, OutProperty);
}
};
而后就将 this 传入了 PropertyPathHelpers::GetPropertyValue 方法来获取最后的值,这里后续的流程可以不再关心,因为从这开始就不再是 UMG 模块的逻辑了,后续的逻辑在 Runtime 部分,完成我们的逻辑时组织好 FCachedPropertyPath 直接调用相同方法即可。
这样我们将 UWidget 中的代理成员就绑定到了根据 PropertyPath 参数来获取其值的 GetValue 方法。
这样后续访问时直接使用这个代理访问即可,UMG 具体在 UProgressBar 的 SynchronizeProperties 方法中:
void UProgressBar::SynchronizeProperties()
{
Super::SynchronizeProperties();
if (!MyProgressBar.IsValid())
{
return;
}
TAttribute< TOptional<float> > PercentBinding = OPTIONAL_BINDING_CONVERT(float, Percent, TOptional<float>, ConvertFloatToOptionalFloat);
TAttribute<FSlateColor> FillColorAndOpacityBinding = PROPERTY_BINDING(FSlateColor, FillColorAndOpacity);
MyProgressBar->SetStyle(&WidgetStyle);
MyProgressBar->SetBarFillType(BarFillType);
MyProgressBar->SetBarFillStyle(BarFillStyle);
MyProgressBar->SetPercent(bIsMarquee ? TOptional<float>() : PercentBinding);
MyProgressBar->SetFillColorAndOpacity(FillColorAndOpacityBinding);
MyProgressBar->SetBorderPadding(BorderPadding);
}
这里的 OPTIONAL_BINDING_CONVERT 宏展开后如下:
TAttribute<TOptional<float>> PercentBinding =
(PercentDelegate.IsBound() && !IsDesignTime())
? TAttribute<TOptional<float>>::Create(TAttribute<TOptional<float>>::FGetter::CreateUObject(this, &ThisClass::ConvertFloatToOptionalFloat, TAttribute<float>::Create(PercentDelegate.GetUObject(), PercentDelegate.GetFunctionName())))
: ConvertFloatToOptionalFloat(TAttribute<float>(Percent));
其实就是基于前文的 PercentDelegate (已经绑定到了 GetValue 方法),构建了一个 TAttribute 并赋值到了 SProgressBar 的成员中,就此完成了绑定的逻辑,SProgressBar 中在 OnPaint 时使用 TAttribute::Get 获取即是通过 GetValue 获取到的绑定后的值。
示例说明
因为 UE 中提供了 FInstancedPropertyBag,理论上来说我们细节面板上的变量,可以通过较为通用的方案绑定到 FInstancedPropertyBag 变量中的某个成员,如果实现后,对于使用了 FInstancedPropertyBag 的位置都可以较为轻易的再添加绑定的逻辑,对一些编辑器相关逻辑的开发很有帮助,因此考虑实现一个这一功能的简单 Sample。
经过上面 UMG 的分析,其实很明确,系统中提供的可以通用的部分基本只有细节面板自定义的 UI,后续的绑定逻辑不太具有通用性,因此笔者这里不太考虑使用 UMG 模块中的相关类。
方案
那么实现这一功能,可以考虑如下两种方案:
- 被绑定者属性变更时,同步到绑定方
FInstancedPropertyBag 并没有属性变更的通知,按此方式可能要考虑轮询同步、修改源码、或用一个提供操作接口的自定义类包起来供使用,以提供相关时机,体感都不太优雅,笔者也不倾向于在被绑定方完成额外的逻辑。 - 绑定方获取时,从被绑定者处获取
UMG 就是此方案,其额外多定义了一个 Delagate 的成员,然后基于 TAttribute 来完成的数据访问。理论上最优的方案肯定是对用户无感,但没有较好的办法完成这一点,因此笔者也考虑额外添加一个成员来完成这一功能(这种方案其实也依赖 InstancedPropertyBag,例如绑定到的属性变更了怎么办,这里没有回调就没有很好的办法处理,只能容错 O.o)。
UPROPERTY(EditAnywhere)
FVector TestVectorValue;
UPROPERTY()
FSampleBindingData TestVectorValueBindingData;
笔者这里定义了一个类 FSampleBindingData,承担类似 UMG 里的 XxxxxDelegate 成员的作用,因为 UPROPERTY 宏不能嵌套,所以也没有办法再精简,需要额外定义一个 Binding 类的成员,笔者也考虑过将此成员的放到外部管理,但这样无疑后续的访问都依赖于管理器,反而不方便,因此还是考虑定义到需要支持绑定功能的类里。
最终达到的效果就是直接访问 TestBoolValueBindingData 来获取变量值。
案例说明
笔者这里定义了一个 Standalone 的插件,名为 PropertyBindingSampleEdtor,可以在菜单栏 Window 中找到,作为案例,没再区分运行时模块,自行拓展时将 SampleBindingObject 与 SampleBindingData 在运行时模块完成即可。
TSharedRef<SDockTab> FPropertyBindingSampleEditorModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
SampleBindingObject = NewObject<USampleBindingObject>();
SampleBindingObject->AddToRoot();
// Details View
FDetailsViewArgs Args;
Args.bHideSelectionTip = true;
Args.bShowPropertyMatrixButton = false;
Args.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide;
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
DetailsView = PropertyModule.CreateDetailView(Args);
DetailsView->SetObject(SampleBindingObject);
DetailsView->SetExtensionHandler(MakeShared<FSampleBindingExtension>());
return SNew(SDockTab)
.OnTabClosed_Raw(this, &FPropertyBindingSampleEditorModule::OnTabClosed)
.TabRole(ETabRole::NomadTab)
[
// Put your tab content here!
SNew(SBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
DetailsView.ToSharedRef()
]
];
}
这里在插件界面打开时,初始化了一个细节面板,并指定了 ExtensionHandler 使用我们定义的 FSampleBindingExtension。
然后创建了一个 USampleBindingObject 类型的对象,并 Set 到该细节面板。
USampleBindingObject 类大致如下:
UCLASS()
class PROPERTYBINDINGSAMPLEEDITOR_API USampleBindingObject : public UObject
{
GENERATED_BODY()
USampleBindingObject()
: TestFloatValue(0.114)
, TestInt32Value(514)
, TestBoolValue(true)
, TestVectorValue(FVector(11.22, 22.33, 33.44))
{
Parameters.AddProperty(TEXT("TestFloat"), EPropertyBagPropertyType::Double);
Parameters.SetValueFloat(TEXT("TestFloat"), 123.123);
Parameters.AddProperty(TEXT("TestInt32"), EPropertyBagPropertyType::Int32);
Parameters.SetValueInt32(TEXT("TestInt32"), 123123);
Parameters.AddProperty(TEXT("TestBool"), EPropertyBagPropertyType::Bool);
Parameters.SetValueBool(TEXT("TestBool"), true);
Parameters.AddProperty(TEXT("TestVector"), EPropertyBagPropertyType::Struct, TBaseStructure<FVector>::Get());
Parameters.SetValueStruct(TEXT("TestVector"), FVector(1.11, 22.2, 3.33));
}
public:
UPROPERTY(EditAnywhere, Category = TestParameters)
FInstancedPropertyBag Parameters;
private:
UPROPERTY(EditAnywhere)
double TestFloatValue;
UPROPERTY()
FSampleBindingData TestFloatValueBindingData;
UPROPERTY(EditAnywhere)
int32 TestInt32Value;
UPROPERTY()
FSampleBindingData TestInt32ValueBindingData;
UPROPERTY(EditAnywhere)
bool TestBoolValue;
UPROPERTY()
FSampleBindingData TestBoolValueBindingData;
UPROPERTY(EditAnywhere)
FVector TestVectorValue;
UPROPERTY()
FSampleBindingData TestVectorValueBindingData;
};
这里将 FInstancedPropertyBag 一并定义在了这个类里,最终实现的效果就是可以支持下方的变量,绑定到上方的 Parameters 中的同类型参数中。实际使用时根据具体业务来获取 FInstancedPropertyBag 对象即可。
绑定说明
可以看到 SampleBindingObject 里,成员的定义与 UMG 中的 delegate 类似,额外定义了一个原变量名加 BindingData 后缀的成员。
在 FSampleBindingExtension::ExtendWidgetRow 方法中:
void FSampleBindingExtension::ExtendWidgetRow(FDetailWidgetRow& InWidgetRow, const IDetailLayoutBuilder& InDetailBuilder, const UClass* InObjectClass, TSharedPtr<IPropertyHandle> PropertyHandle)
{
...
if (!PropertyHandle->GetProperty())
{
return;
}
const FProperty* BindingDataProperty = FindFProperty<FProperty>(InObjectClass, FName(*(PropertyHandle->GetProperty()->GetName() + TEXT("BindingData"))));
if (BindingDataProperty == nullptr)
{
return;
}
void* BindingDataAddress = BindingDataProperty->ContainerPtrToValuePtr<void>(SampleBindingObject);
if (BindingDataAddress == nullptr)
{
return;
}
if (CastField<FStructProperty>(BindingDataProperty) == nullptr)
{
return;
}
FSampleBindingData* SampleBindingData = static_cast<FSampleBindingData*>(BindingDataAddress);
if (SampleBindingData == nullptr)
{
return;
}
会对每一行 Property 来检查是否存在同名带 BindingData 后缀的成员,如果存在,则视为可支持绑定的类,进而执行后续的逻辑。
FSampleBindingData 这个类定义了一些参数,来存储绑定信息,并执行变量访问的逻辑:
USTRUCT()
struct FSampleBindingData
{
GENERATED_BODY()
public:
static constexpr int32 FirstValidSegmentIndex = 0;
static constexpr int32 FirstNestSegmentIndex = 1;
const FInstancedPropertyBag* PropertyBag = nullptr;
FName PropertyDescName;
EPropertyBagPropertyType PropertyDescValueType = EPropertyBagPropertyType::None;
FCachedPropertyPath SourcePath;
FLinearColor Color = FLinearColor::White;
const FSlateBrush* Image = nullptr;
...
对于原始细节面板的 ContentWidget,也会根据这个类的变量绑定状态来确定是否启用:
TAttribute<bool> bOriginValueContentWidgetEnabled = TAttribute<bool>::Create(TAttribute<bool>::FGetter::CreateLambda([SampleBindingData]() -> bool
{
if (SampleBindingData != nullptr)
{
return !SampleBindingData->HasBinding();
}
return true;
}));
InWidgetRow.ValueContent().Widget->SetEnabled(bOriginValueContentWidgetEnabled);
参数构建
TArray\<FBindingContextStruct>
首个结构数组参数参考的 FSmartObjectDefinitionBindingExtension::ExtendWidgetRow 中的逻辑,测试如下逻辑,在 OnCanBindPropertyWithBindingChain 回调中,会将 PropertyBag 中的所有 Property 依次传入,满足我们的需求:
const FText DisplayText = FText::FromString(TEXT("Parameters"));
const FString CategoryStr = TEXT("TestCategory");
const FText SectionText = FText::FromString(TEXT("Global"));
TArray<FBindingContextStruct> BindingContextStructs;
FBindingContextStruct& ContextStruct = BindingContextStructs.AddDefaulted_GetRef();
ContextStruct.DisplayText = DisplayText;
const UStruct* Struct = PropertyBag.GetPropertyBagStruct();
ContextStruct.Struct = const_cast<UStruct*>(Struct);
ContextStruct.Section = SectionText;
ContextStruct.Color = FLinearColor::MakeRandomColor();
OnCanBindPropertyWithBindingChain
这个方法用来筛选是否是我们支持绑定的类型,这里检测的匹配类型使用的符合 EPropertyAccessCompatibility::Compatible,系统中有一些位置使用的不等于 EPropertyAccessCompatibility::Incompatible 时支持绑定,也可以如此,实现访问逻辑时注意支持隐式转换即可。
Args.OnCanBindPropertyWithBindingChain = FOnCanBindPropertyWithBindingChain::CreateLambda([PropertyHandle](FProperty* InProperty, TConstArrayView<FBindingChainElement> InBindingChain)
{
if (InProperty == nullptr || InBindingChain.IsEmpty())
{
return true;
}
IPropertyAccessEditor& PropertyAccessEditor = IModularFeatures::Get().GetModularFeature<IPropertyAccessEditor>("PropertyAccessEditor");
const FProperty* BindingProperty = PropertyHandle->GetProperty();
const bool Result = BindingProperty && PropertyAccessEditor.GetPropertyCompatibility(InProperty, BindingProperty) == EPropertyAccessCompatibility::Compatible;
return Result;
});
OnAddBinding
添加绑定时,核心逻辑就是构造了 FSampleBindingData 需要的几个参数,这里因为有嵌套的情况存在,即例如 double 变量绑定到 PropertyBag 中的 FVector 的 Y 值,所以也是需要存储 InBindingChain 携带的相关信息的:
Args.OnAddBinding = FOnAddBinding::CreateLambda([SampleBindingData, PropertyBag, PropertyHandle](const FName& InPropertyName, const TArray<FBindingChainElement>& InBindingChain)
{
TArray<FFieldVariant> FieldChain;
Algo::Transform(InBindingChain, FieldChain, [](const FBindingChainElement& InElement) {
return InElement.Field;
});
FCachedPropertyPath CachedPropertyPath;
TArray<FString> PropertyChain;
for (FFieldVariant Field : FieldChain)
{
if (const FProperty* CurProperty = Field.Get<FProperty>())
{
FName MemberName = CurProperty->GetFName();
PropertyChain.Add(MemberName.ToString());
}
}
if (PropertyChain.IsEmpty())
{
UE_LOG(LogTemp, Error, TEXT("%s:%d Binding chain is empty."), *FString(__FUNCTION__), __LINE__);
return;
}
SampleBindingData->SourcePath = FCachedPropertyPath(PropertyChain);
SampleBindingData->PropertyBag = &PropertyBag;
const FPropertyBagPropertyDesc* PropertyDesc = PropertyBag.FindPropertyDescByName(
SampleBindingData->SourcePath.GetSegment(FSampleBindingData::FirstValidSegmentIndex).GetName());
SampleBindingData->PropertyDescName = PropertyDesc->Name;
SampleBindingData->PropertyDescValueType = PropertyDesc->ValueType;
// Image
static FName PropertyIcon(TEXT("Kismet.Tabs.Variables"));
SampleBindingData->Image = FAppStyle::GetBrush(PropertyIcon);
// Color
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
check(Schema);
FEdGraphPinType PinType;
Schema->ConvertPropertyToPinType(PropertyHandle->GetProperty(), PinType);
SampleBindingData->Color = Schema->GetPinTypeColor(PinType);
});
这里获取 Color 的方法从系统中抄过来的,一些绑定逻辑均是如此获取的。
Other
其他的一些方法均利用构造的 FSampleBindingData 中的参数完成即可,不再赘述:
Args.OnRemoveBinding = FOnRemoveBinding::CreateLambda([SampleBindingData](FName InPropertyName)
{
SampleBindingData->RemoveBinding();
});
Args.OnHasAnyBindings = FOnHasAnyBindings::CreateLambda([SampleBindingData]()
{
return SampleBindingData->HasBinding();
});
Args.CurrentBindingText = MakeAttributeLambda([SampleBindingData]()
{
return SampleBindingData->GetBindingText();
});
Args.CurrentBindingToolTipText = MakeAttributeLambda([SampleBindingData]()
{
return SampleBindingData->GetBindingText();
});
Args.CurrentBindingImage = MakeAttributeLambda([SampleBindingData]() -> const FSlateBrush*
{
return SampleBindingData->Image;
});
Args.CurrentBindingColor = MakeAttributeLambda([SampleBindingData]() -> FLinearColor
{
return SampleBindingData->Color;
});
变量访问
在变量访问时,笔者期望能如原生变量一般访问,因此重载了类型转换运算符,具体变量访问时就是转调了 PropertyBag 中的获取值的方法:
template <typename T>
operator T() const
{
TValueOrError<T, EPropertyBagResult> Result = GetValue<T>();
if (Result.HasError())
{
UE_LOG(LogTemp, Error, TEXT("Type mismatch when reading property: %s"), *PropertyDescName.ToString());
return T();
}
return Result.GetValue();
}
这里只有一点不同,对于嵌套的绑定额外进行了处理,转调了 GetNestedPropertyValue 方法:
template <typename T>
TValueOrError<T, EPropertyBagResult> GetValue() const
{
if (!HasBinding())
{
return MakeError(EPropertyBagResult::TypeMismatch);
}
const FName PropertyName = PropertyDescName;
switch (PropertyDescValueType)
{
case EPropertyBagPropertyType::Bool:
{
if constexpr (std::is_same_v<T, bool>)
{
return PropertyBag->GetValueBool(PropertyName);
}
break;
}
...
{
case EPropertyBagPropertyType::Struct:
{
return GetNestedPropertyValue<T>();
}
default:
{
break;
}
}
return MakeError(EPropertyBagResult::TypeMismatch);
}
GetNestedPropertyValue 方法中,基于从 BindingChain 组织的 SourcePath,逐层完成了嵌套类型变量的访问:
template <typename T>
TValueOrError<T, EPropertyBagResult> GetNestedPropertyValue() const
{
if (!PropertyBag || SourcePath.GetNumSegments() == 0)
{
return MakeError(EPropertyBagResult::TypeMismatch);
}
const FPropertyBagPropertyDesc* RootDesc = PropertyBag->FindPropertyDescByName(SourcePath.GetSegment(FirstValidSegmentIndex).GetName());
if (!RootDesc)
{
return MakeError(EPropertyBagResult::PropertyNotFound);
}
TValueOrError<FStructView, EPropertyBagResult> RootValueResult = PropertyBag->GetValueStruct(RootDesc->Name);
if (!RootValueResult.IsValid())
{
return MakeError(RootValueResult.GetError());
}
FStructView CurrentView = RootValueResult.GetValue();
for (int32 i = FirstNestSegmentIndex; i < SourcePath.GetNumSegments(); ++i)
{
const FName SubPropertyName = SourcePath.GetSegment(i).GetName();
const FProperty* SubProperty = CurrentView.GetScriptStruct()->FindPropertyByName(SubPropertyName);
if (!SubProperty)
{
return MakeError(EPropertyBagResult::PropertyNotFound);
}
if (const FStructProperty* StructProp = CastField<FStructProperty>(SubProperty))
{
CurrentView = FStructView(StructProp->Struct, static_cast<uint8*>(StructProp->ContainerPtrToValuePtr<void>(CurrentView.GetMemory())));
}
else
{
void* Value = SubProperty->ContainerPtrToValuePtr<void>(CurrentView.GetMemory());
return MakeValue(*(static_cast<const T*>(Value)));
}
}
void* CurrentViewMemory = CurrentView.GetMemory();
return MakeValue(*(static_cast<T*>(CurrentViewMemory)));
}
这样,主体的逻辑就完成了。
P.S. 最终实现的变量访问虽说可以提供如原生变量一般的访问,但实则很不安全,类型不匹配时运行时才会暴露出问题,实际开发中还是更推荐如 UMG 中的拓展一般,继承多个子类来各自完成变量的访问逻辑。
Comments 2 条评论
东神,强
我滴妈,太强了,非常有帮助