【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(&#34;Trying to assign null object value to non-nullable \&#34;%s\&#34;&#34;), *GetFullName());
}
}
那么就走完了,一次Instanced的property赋值就走完了,大家各找各妈各回各家。
踩坑
上面那一坨看着好像也挺尽善尽美的啊,没啥问题啊,那我到底踩了个啥坑呢?
我们回到FObjectPropertyBase::ParseObjectPropertyValue 这个函数里,看到下面这一行
Buffer = FPropertyHelpers::ReadToken(Buffer, /* out */ Temp, true);
看着也没啥问题,跑着正常情况下也没啥问题,但是问题是,这个函数会自动截断带有空格的字符串,这是我屎尿未及的,看到下面两个例子。
InBuffer=&#34;/Game/BP_TestObject.BP_TestObject:Custom Graph.BP_SomeCustomObject_C_0&#34;
Temp=&#34;/Game/BP_TestObject.BP_TestObject:Custom&#34;
Buffer=&#34;Graph.BP_SomeCustomObject_C_0&#34;
InBuffer=&#34;/Game/BP_TestObject.BP_TestObject:CustomGraph.BP_SomeCustomObject_C_0&#34;
Temp=&#34;/Game/BP_TestObject.BP_TestObject:CustomGraph.BP_SomeCustomObject_C_0&#34;
Buffer=&#34;&#34;也就是说如果你空格在末尾还好,如果你的引用里有一个空格,那么你百分之一千是会赋值失败的。而如果你是自定义了一个蓝图类,并且新制作了一个Graph用来实现一些你自己的可视化需求,那么你应该会用到这么一个函数
FBluepriMyCustomUtils::CreateNewGraph(BlueprintBeingEdited, TEXT(&#34;Custom Graph&#34;), UMyCustomEditorGraph::StaticClass(), UMyCustomGraphSchema::StaticClass());
问题就是这里,第二个参数GraphName 看着像是一个普通的String,但是,他绝对不能有空格或者其他能导致截断的字符 。真是令人安心的问题呢,整个注释里一句话都没讲这里的问题,也不会报错也不会有问题,甚至SetPerObjectValues 这个返回值都是Success,不行,血压高了
(某种意义上UE的代码好坚强,这种情况下还能以一个奇怪的角度正常运行)
结语
硬编码字符串的时候,别加空格,宁可用下划线也别加空格,空格会让人不幸,鬼知道UE还在哪里用了这一套序列化再反序列化的操作来传值。果然&#34;虚幻引擎是一大堆无法理解的、如魔法与巫术混合的代码的集合体&#34;,那我们下个坑再见。
参考
[*]^Property Specifiershttps://docs.unrealengine.com/5.0/en-US/unreal-engine-uproperty-specifiers/
好家伙,这坑够深
页:
[1]