TheLudGamer 发表于 2022-10-27 19:21

【UnrealEngine5】InstancedProperty创建机制

一切的起因

起因是自己自定义的blueprint类型不管怎么样都没法在detail面板里选择instanced类型的uproperty,不管怎么选类型最后只能变成None。为此折腾了好久,原本以为是自己水平有问题,写的编辑器Detail面板有问题,最后发现原因之后血压高了必须得输出一下,要不然憋着难受。讲的不对的地方请尽管指出。



讲的不对的地方请尽管指出,我是垃圾

Instanced UProperty

Instanced
(Unreal Engine reflection specifier)
Object (UCLASS) properties only. When an instance of this class is created, it will be given a unique copy of the Object assigned to this property in defaults. Used for instancing subobjects defined in class default properties. Implies EditInline and Export.意思很明显了,Instanced是一个只能加在UClass的对象变量上面的修饰符,可以给你一份你选定类型的实例化对象。隐式声明了EditInline和Export两个修饰符。
UCLASS(Blueprintable,BlueprintType,EditInlineNew)
class TOYBOX_API UCustomObject : public UObject
{
        GENERATED_BODY()
public:
        UPROPERTY(EditAnywhere)
        bool TestVar;

        UPROPERTY(EditAnywhere)
        FGuid UUID;
};

UCLASS(Blueprintable,BlueprintType)
class TOYBOX_API UTestObject : public UObject
{
        GENERATED_BODY()
public:
        UPROPERTY(EditAnywhere,Instanced)
        UCustomObject* Object;
};
这是一个经典的UE定义一个Instanced的UProperty,在detail面板里可以直接选择他的子类并且生成实例化的对象。




说一个比较好玩的事情,如果把VisibleAnywhere和 Instanced 一起使用的话,他不会显示成树状结构,而是直接影藏掉property本身,把他的子树的properties合并到外面来(其实UE很多地方就是这样实现的,把好几个不同的Object的属性拼在一起)。像下面这样:



Object那一层消失了,但是下面的内容会直接展开在外面那层

基本上正常使用的时候就到此为止了,剩下的全部可以交给UE自己解决了。如果不是这次出了问题,我还真不一定会去看这里的代码,算是因祸得福。
EditInline

在某种意义上Instanced 修饰符可以算是EditInline和Export 的组合体,不过EditInline已经被废弃掉了(经典UE历史遗留问题,注意不要和放在UCLASS里的EditInlineNew修饰符搞混淆),直接在UE5里用的话会报错(所以我就没看)。
TestObject.h(18): [] EditInline is deprecated. Remove it, or use Instanced instead.Export

Export是一个只对object指针变量有效的修饰符,他的功能是你在复制的时候会把整个指针对应的对象也序列化掉,如果没加的话差不多是这样:
Begin Object Class=/Script/Something.SomeClass Name="ClassName_1"
    Object=BP_SomeCustomObject_C'"BP_SomeCustomObject_C_0"'
End Object里面是纯纯的一个引用,如果在粘贴的时候这个引用挂逼了或者你跨文件复制,就会发现他没法复制,会填一个nullptr进去(这个其实和我遇到的问题有一定关系,原理类似)。如果加了Export,那么复制的时候这个就会变成下面这样,这个对象会被包含在里面:
Begin Object Class=/Script/Something.SomeClass Name="ClassName_1"
    Begin Object Class=/Game/BP_SomeCustomObject.BP_SomeCustomObject_C_0 Name="BP_SomeCustomObject_C_0"
    End Object
    Begin Object Name="BP_SomeCustomObject_C_0"
      UUID=CCD50F004F634B0A6F5741BFDB40E79C
      TestVar=false
    End Object
    Object=BP_SomeCustomObject_C'"BP_SomeCustomObject_C_0"'
End ObjectSPropertyEditorEditInline

进入正题,中间的找的部分由于我是靠自己的绿皮天赋全局搜索找到的,所以就不写出来误人子弟了。
// UnrealEngine\Engine\Source\Editor\PropertyEditor\Private\UserInterface\PropertyEditor\SPropertyEditorEditInline.hE:\UnrealEngine\Engine\Source\Editor\PropertyEditor\Private\UserInterface\PropertyEditor\SPropertyEditorEditInline.hE:\UnrealEngine\Engine\Source\Editor\PropertyEditor\Private\UserInterface\PropertyEditor\SPropertyEditorEditInline.h
class SPropertyEditorEditInline : public SCompoundWidget
{
public:
        //...这里只放我认为比较要紧的,剩下的自己看代码去
        //传进来的Property是否支持EditInline
        static bool Supports( const TSharedRef< class FPropertyEditor >& InPropertyEditor );
        static bool Supports( const FPropertyNode* InTreeNode, int32 InArrayIdx );
        //...
private:
        //...
        /**
       * Callback function from the Class Picker for when a Class is picked.
       * 可以说是最核心的地方,在选择Class后新建对象
       * @param InClass                        The class picked in the Class Picker
       */
        void OnClassPicked(UClass* InClass);
        //...
};
SpropertyEditorInline是承载Instanced的Property的Slate,基本上和Instanced相关的逻辑都能从这里找到入口。
Support()

// UnrealEngine\Engine\Source\Editor\PropertyEditor\Private\UserInterface\PropertyEditor\SPropertyEditorEditInline.cpp
bool SPropertyEditorEditInline::Supports( const FPropertyNode* InTreeNode, int32 InArrayIdx )
{
        return InTreeNode
                && InTreeNode->HasNodeFlags(EPropertyNodeFlags::EditInlineNew)
                && InTreeNode->FindObjectItemParent()
                && !InTreeNode->IsPropertyConst();
}

bool SPropertyEditorEditInline::Supports( const TSharedRef< class FPropertyEditor >& InPropertyEditor )
{
        const TSharedRef< FPropertyNode > PropertyNode = InPropertyEditor->GetPropertyNode();
        return SPropertyEditorEditInline::Supports( &PropertyNode.Get(), PropertyNode->GetArrayIndex() );
}
Support函数在DetailView刷新的时候会被拿来遍历每一个Property,支持的就会显示为对应的Slate样式(这段是大段大段的if语句,有c内味了)。这段代码也简单的批爆,也就是说一个property要能被instanced编辑,要满足下面的条件:

[*]首先是得有EditInlineNew的Flag(经典屎山问题,外面叫一个名,代码里叫另一个名)
[*]其次是必须Outer不为nullptr,这个也很好理解,NewObject一定得有个Outer
[*]Property不是Const的,这个更好理解,Const还赋啥值
OnClassPicked()

void SPropertyEditorEditInline::OnClassPicked(UClass* InClass)
{
        TArray<FObjectBaseAddress> ObjectsToModify;
        TArray<FString> NewValues;

        const TSharedRef< FPropertyNode > PropertyNode = PropertyEditor->GetPropertyNode();
        FObjectPropertyNode* ObjectNode = PropertyNode->FindObjectItemParent();
        if( ObjectNode )
        {
                // ...一坨预处理,绿皮技能发动——俺寻思这不重要
                for ( TPropObjectIterator Itor( ObjectNode->ObjectIterator() ) ; Itor ; ++Itor )
                {
                        // 这个for循环俺寻思应该是拿来兼容array的
                        FString NewValue;
                        if (InClass)
                        {
                                FStringView CurClassName, CurObjectName;
                                if (PrevPerObjectValues.IsValidIndex(NewValues.Num()) && PrevPerObjectValues != FName(NAME_None).ToString())
                                {
                                        ExtractClassAndObjectNames(PrevPerObjectValues, CurClassName, CurObjectName);
                                }

                                if (CurObjectName == NewObjectName && InClass->GetName() == CurClassName)
                                {
                                        // 如果旧的和新的没啥区别就不改他
                                        NewValue = MoveTemp(PrevPerObjectValues);
                                        PrevPerObjectValues.Reset();
                                }
                                else
                                {
                                        // 真正新建对象的地方
                                        UObject*                Object = Itor->Get();
                                        UObject*                UseOuter = (InClass->IsChildOf(UClass::StaticClass()) ? Cast<UClass>(Object)->GetDefaultObject() : Object);
                                        EObjectFlags        MaskedOuterFlags = UseOuter ? UseOuter->GetMaskedFlags(RF_PropagateToSubObjects) : RF_NoFlags;
                                        // ...又是一坨处理,为了防止太长,绿皮技能发动——俺寻思这不重要
                                        UObject* NewUObject = NewObject<UObject>(UseOuter, InClass, *NewObjectName, MaskedOuterFlags, NewObjectTemplate);
                                        // 新建完传的是PathName,这个Object就直接丢内存里了,等后面再说
                                        NewValue = NewUObject->GetPathName();
                                }
                        }
                        else
                        {
                                NewValue = FName(NAME_None).ToString();
                        }
                        NewValues.Add(MoveTemp(NewValue));
                }
                // 这一步才是赋值的地方
                PropertyHandle->SetPerObjectValues(NewValues);
                // ... 一些后处理的代码,然后刷新
                PropertyNode->RequestRebuildChildren();
                // 把子树给关起来(防止露馅?)
                ComboButton->SetIsOpen(false);
        }
}
这个函数算是整个类的核心中的核心,350行代码一共150行在讲这个函数,恐怖如斯。其实可以看出来NewObject其实已经在这里做完了,不在这里做的是把值赋值过去那一步,那一步在PropertyHandle->SetPerObjectValues(NewValues); 。其实这里已经可以看出端倪了,赋值用的是Path而不是直接赋值地址过去,参考上面Export那里,如果这个Path找不到那就寄了,这也是我遇到的问题的原因。
赋值

FObjectPropertyBase::ParseObjectPropertyValue

一路追下来,可以看到就是套娃,一路套到这里才又从String变回了Object真正开始赋值。


这个函数里大段大段的都是对字符串做处理,因此就不贴出来了,核心就一行
out_ResolvedValue = FObjectPropertyBase::FindImportedObject(Property, OwnerObject, ObjectClass, RequiredMetaClass, Temp.ToString(), PortFlags, InSerializeContext, bAllowAnyPackage);
在这里实现了从String转换为Object的工作
FObjectPropertyBase::ImportText_Internal()

从上面的函数跳出来,回到这里,我们现在手头有Object,有UProperty,那么调一行赋值就完事了
// UnrealEngine\Engine\Source\Runtime\CoreUObject\Private\UObject\PropertyBaseObject.cpp
SetObjectPropertyValue(PointerToValuePtr(ContainerOrPropertyPtr, PropertyPointerType), Result);

// UnrealEngine\Engine\Source\Runtime\CoreUObject\Private\UObject\PropertyObject.cpp
void FObjectProperty::SetObjectPropertyValue(void* PropertyValueAddress, UObject* Value) const
{
        if (Value || !HasAnyPropertyFlags(CPF_NonNullable))
        {
                SetPropertyValue(PropertyValueAddress, Value);
        }
        else
        {
                UE_LOG(LogProperty, Verbose /*Warning*/, TEXT("Trying to assign null object value to non-nullable \"%s\""), *GetFullName());
        }
}
那么就走完了,一次Instanced的property赋值就走完了,大家各找各妈各回各家。
踩坑

上面那一坨看着好像也挺尽善尽美的啊,没啥问题啊,那我到底踩了个啥坑呢?
我们回到FObjectPropertyBase::ParseObjectPropertyValue 这个函数里,看到下面这一行
Buffer = FPropertyHelpers::ReadToken(Buffer, /* out */ Temp, true);
看着也没啥问题,跑着正常情况下也没啥问题,但是问题是,这个函数会自动截断带有空格的字符串,这是我屎尿未及的,看到下面两个例子。
InBuffer="/Game/BP_TestObject.BP_TestObject:Custom Graph.BP_SomeCustomObject_C_0"
Temp="/Game/BP_TestObject.BP_TestObject:Custom"
Buffer="Graph.BP_SomeCustomObject_C_0"

InBuffer="/Game/BP_TestObject.BP_TestObject:CustomGraph.BP_SomeCustomObject_C_0"
Temp="/Game/BP_TestObject.BP_TestObject:CustomGraph.BP_SomeCustomObject_C_0"
Buffer=""也就是说如果你空格在末尾还好,如果你的引用里有一个空格,那么你百分之一千是会赋值失败的。而如果你是自定义了一个蓝图类,并且新制作了一个Graph用来实现一些你自己的可视化需求,那么你应该会用到这么一个函数
FBluepriMyCustomUtils::CreateNewGraph(BlueprintBeingEdited, TEXT("Custom Graph"), UMyCustomEditorGraph::StaticClass(), UMyCustomGraphSchema::StaticClass());
问题就是这里,第二个参数GraphName 看着像是一个普通的String,但是,他绝对不能有空格或者其他能导致截断的字符 。真是令人安心的问题呢,整个注释里一句话都没讲这里的问题,也不会报错也不会有问题,甚至SetPerObjectValues 这个返回值都是Success,不行,血压高了


(某种意义上UE的代码好坚强,这种情况下还能以一个奇怪的角度正常运行)
结语

硬编码字符串的时候,别加空格,宁可用下划线也别加空格,空格会让人不幸,鬼知道UE还在哪里用了这一套序列化再反序列化的操作来传值。果然"虚幻引擎是一大堆无法理解的、如魔法与巫术混合的代码的集合体",那我们下个坑再见。


参考


[*]^Property Specifiershttps://docs.unrealengine.com/5.0/en-US/unreal-engine-uproperty-specifiers/

Baste 发表于 2022-10-27 19:24

好家伙,这坑够深
页: [1]
查看完整版本: 【UnrealEngine5】InstancedProperty创建机制