前言 在使用ue4时我们经常会碰到需要把UObject
类和json
文件互相转换的情形。
ue4本身封装了相当充足的处理json的接口,所以我们可以通过多种方式达到这一目的。比如对于UObject
的每一个成员属性,都手动调用生成json格式文本的接口,最终生成json格式的字符串保存到磁盘文件里,这种方法可以命名为钻木取火
。还比如我们可以依托于ue4的反射信息,在不必关心UObject
具体内容的情况下自动生成json格式,这种方法才是火柴取火
。
我们将以如下一个简单的UObject
为例,分别在ue4里使用钻木取火
和火柴取火
来实现其与json文件的互转。并在此之后尝试把这个火柴
看能不能优化成一个用起来得心应手的打火机
。
1 2 3 4 5 6 7 8 9 10 UCLASS ()class UFoo : public UObject{ GENERATED_UCLASS_BODY () UPROPERTY (EditAnywhere) int32 ID; UPROPERTY (EditAnywhere) FString Name; };
钻木取火 我们可以使用ue4自带的位于Engine\Source\Runtime\Json
处的json
模块来做这件事情,比较重要的是位于Engine\Source\Runtime\Json\Public\Dom\JsonObject.h
中的FJsonObject
类,它提供最原始的原始数据类型和Json格式字符之前的转换。
我们可以使用FJsonObject
的SetXXXField()
和TryGetXXXField()
方法在原始数据结果和Json格式进行转换,一个简洁的示例如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void SaveToJson (UFoo* Foo) { TSharedPtr<FJsonObject> FooJsonObj = MakeShareable (new FJsonObject ()); FooJsonObj->SetNumberField ("ID" , Foo->ID); FooJsonObj->SetStringField ("Name" , Foo->Name); FString JsonStr; const TSharedRef<TJsonWriter<TCHAR> > JsonWriter = TJsonWriterFactory<TCHAR>::Create (&JsonStr); FJsonSerializer::Serialize (JsonObj.ToSharedRef (), JsonWriter); FFileHelper::SaveStringToFile (JsonStr, TEXT ("Foo.json" )); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool LoadFromJson (UFoo* Foo, const FString& Path) { FString JsonStr; FFileHelper::LoadFileToString (JsonStr, *Path); TSharedPtr<FJsonObject> FooJsonObj = MakeShareable (new FJsonObject ()); const TSharedRef<TJsonReader<> > JsonReader = TJsonReaderFactory<>::Create (JsonStr); const bool tRet = FJsonSerializer::Deserialize (JsonReader, FooJsonObj); FooJsonObj->TryGetNumberField ("ID" , Foo->ID); FooJsonObj->TryGetStringField ("Name" , Foo->Name); return tRet; }
UFoo
类在ue4编辑器里的细节面板显示如下。
UFoo 最终生成的UFoo
的等价json
文件如下。
1 2 3 4 { "ID" : 1 , "Name" : "Foo" }
这种方式的缺点不言自明,不明而喻。首先对于每一个类,我们都需要繁琐地实现SaveToJson()
和LoadFromJson()
函数。其次当需要修改某个类的成员变量时,也需要同时修改这两个函数,毫无优雅可言。
所以我们急需一种可以抹去类型、无需关注内容修改的方式。
火柴取火 众所周知,ue4本身的反射信息很充足,我们可以通过一个类的UStruct
来获得该类的结构,包括其成员变量UProperty
和成员函数UFunction
,这里我们只关注成员变量,所以只要可以遍历类的反射信息里的UProperty
信息即可。
ue4也极其贴心地封装了这一部分接口,位于Engine\Source\Runtime\JsonUtilities\Private\JsonObjectConverter.cpp
下。
使用 一个与上一节钻木取火
中的示例对应的代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void SaveToJson (UFoo* Foo) { TSharedPtr<FJsonObject> FooJsonObj = MakeShareable (new FJsonObject ()); FJsonObjectConverter::UStructToJsonObject (Foo->GetClass (), Foo, FooJsonObj, 0 , 0 ); FString JsonStr; const TSharedRef<TJsonWriter<TCHAR> > JsonWriter = TJsonWriterFactory<TCHAR>::Create (&JsonStr); FJsonSerializer::Serialize (JsonObj.ToSharedRef (), JsonWriter); FFileHelper::SaveStringToFile (JsonStr, TEXT ("Foo.json" )); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bool LoadFromJson (UFoo* Foo, const FString& Path) { FString JsonStr; FFileHelper::LoadFileToString (JsonStr, *Path); TSharedPtr<FJsonObject> FooJsonObj = MakeShareable (new FJsonObject ()); const TSharedRef<TJsonReader<> > JsonReader = TJsonReaderFactory<>::Create (JsonStr); const bool tRet = FJsonSerializer::Deserialize (JsonReader, FooJsonObj); FJsonObjectConverter::JsonObjectToUStruct (FooJsonObj.ToSharedRef (), Foo->GetClass (), Foo, 0 , 0 ); return tRet; }
从代码中可以看出,使用了FJsonObjectConverter
新接口的方法,并不需要关心UFoo
的内容,甚至修改UFoo
的成员变量时也不需要修改这两个方法,十分优雅,完全符合我们的需求。
UObject
转Json
时调用了FJsonObjectConverter::UStructToJsonObject()
,该方法第一个参数需要传入承载了目标对象反射信息的UStruct
(这里UFoo是UObject类型,所以传入的是其UStruct的子类UClass类),这个参数可以让我们有能力遍历到该类的UProperty
成员变量。该方法的第二个参数即为目标对象的地址,它可以让我们有能力根据反射结构取到目标对象里对应的值。FJsonObjectConverter::JsonObjectToUStruct()
类似,话休絮繁。
实现 下面我们以FJsonObjectConverter::UStructToJsonObject()
来分析一下其实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 bool UStructToJsonObject ( const UStruct* StructDefinition, const void * Struct, TSharedRef<FJsonObject> OutJsonObject, int64 CheckFlags, int64 SkipFlags) { return UStructToJsonAttributes ( StructDefinition, Struct, OutJsonObject->Values, CheckFlags, SkipFlags ); } bool UStructToJsonAttributes ( const UStruct* StructDefinition, const void * Struct, TMap< FString, TSharedPtr<FJsonValue> >& OutJsonAttributes, int64 CheckFlags, int64 SkipFlags ) { for (TFieldIterator<UProperty> It (StructDefinition); It; ++It) { UProperty* Property = *It; if (CheckFlags != 0 && !Property->HasAnyPropertyFlags (CheckFlags)) { continue ; } if (Property->HasAnyPropertyFlags (SkipFlags)) { continue ; } FString VariableName = StandardizeCase (Property->GetName ()); const void * Value = Property->ContainerPtrToValuePtr <uint8>(Struct); TSharedPtr<FJsonValue> JsonValue = UPropertyToJsonValue ( Property, Value, CheckFlags, SkipFlags ); if (!JsonValue.IsValid ()) { return false ; } OutJsonAttributes.Add (VariableName, JsonValue); } return true ; }
该方法使用迭代器TFieldIterator<UProperty>
遍历传入的UStruct
里的所有UProperty
,代表了该类的每一个成员变量。
正如代码所示,对于每一个成员变量,可以使用Property->GetName()
获得其成员变量的名称,如Foo->ID
对应返回的便是字符串"ID"
,Foo->Name
对应返回的便是字符串"Name"
。可以使用Property->ContainerPtrToValuePtr()
获得当前对象的该成员变量的地址,以便后续从该地址处拿到该成员变量的值。
当获取到当前成员变量的地址之后,便可调用FJsonObjectConverter::UPropertyToJsonValue()
,通过该成员变量的结构信息Property
和该成员变量的地址Value
,进一步将该成员变量转化为FJsonValue
类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TSharedPtr<FJsonValue> UPropertyToJsonValue ( UProperty* Property, const void * Value, int64 CheckFlags, int64 SkipFlags ) { if (Property->ArrayDim == 1 ) { return ConvertScalarUPropertyToJsonValue (Property, Value, CheckFlags, SkipFlags); } TArray< TSharedPtr<FJsonValue> > Array; for (int Index = 0 ; Index != Property->ArrayDim; ++Index) { Array.Add (ConvertScalarUPropertyToJsonValue ( Property, (char *)Value + Index * Property->ElementSize, CheckFlags, SkipFlags )); } return MakeShareable (new FJsonValueArray (Array)); }
该方法只不过是根据Property->ArrayDim
来判断该成员变量是否为数组,从而保证最终只针对单个元素调用FJsonObjectConverter::ConvertScalarUPropertyToJsonValue()
进行单个元素转为FJsonValue
的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 TSharedPtr<FJsonValue> ConvertScalarUPropertyToJsonValue ( UProperty* Property, const void * Value, int64 CheckFlags, int64 SkipFlags ) { if (UEnumProperty* EnumProperty = Cast <UEnumProperty>(Property)) { } else if (UNumericProperty *NumericProperty = Cast <UNumericProperty>(Property)) { } else if (UBoolProperty *BoolProperty = Cast <UBoolProperty>(Property)) { } else if (UStrProperty *StringProperty = Cast <UStrProperty>(Property)) { } else if (UTextProperty *TextProperty = Cast <UTextProperty>(Property)) { } else if (UArrayProperty *ArrayProperty = Cast <UArrayProperty>(Property)) { } else if ( USetProperty* SetProperty = Cast <USetProperty>(Property) ) { } else if ( UMapProperty* MapProperty = Cast <UMapProperty>(Property) ) { } else if (UStructProperty *StructProperty = Cast <UStructProperty>(Property)) { } else { } return TSharedPtr <FJsonValue>(); }
该方法根据单个Property
的具体类型分别进行处理,大致分为以下几类。
单值类型 枚举、数值、布尔、字符串型元素的处理很简单,直接转换为对应的FJsonValue
类型即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 if (UEnumProperty* EnumProperty = Cast <UEnumProperty>(Property)){ } else if (UNumericProperty *NumericProperty = Cast <UNumericProperty>(Property)){ if (NumericProperty->IsFloatingPoint ()) { return MakeShareable (new FJsonValueNumber (NumericProperty->GetFloatingPointPropertyValue (Value))); } else if (NumericProperty->IsInteger ()) { return MakeShareable (new FJsonValueNumber (NumericProperty->GetSignedIntPropertyValue (Value))); } } else if (UBoolProperty *BoolProperty = Cast <UBoolProperty>(Property)){ return MakeShareable (new FJsonValueBoolean (BoolProperty->GetPropertyValue (Value))); } else if (UStrProperty *StringProperty = Cast <UStrProperty>(Property)){ return MakeShareable (new FJsonValueString (StringProperty->GetPropertyValue (Value))); } else if (UTextProperty *TextProperty = Cast <UTextProperty>(Property)){ return MakeShareable (new FJsonValueString (TextProperty->GetPropertyValue (Value).ToString ())); }
容器类型 对于 TArray
、TSet
、TMap
等容器类型,通常会遍历该容器的每一个元素,然后对单个元素继续递归调用FJsonObjectConverter::UPropertyToJsonValue()
进行处理。
而对于此处的遍历操作,引擎提供了对应的FXXXHelper
类来辅助遍历操作。该类可以比较方便地根据元素类型
+元素地址
来遍历。以TArray
为例,其处理过程大致如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 else if (UArrayProperty *ArrayProperty = Cast <UArrayProperty>(Property)){ TArray< TSharedPtr<FJsonValue> > Out; FScriptArrayHelper Helper (ArrayProperty, Value) ; for (int32 i=0 , n=Helper.Num (); i<n; ++i) { TSharedPtr<FJsonValue> Elem = UPropertyToJsonValue (ArrayProperty->Inner, Helper.GetRawPtr (i), CheckMetaName); if ( Elem.IsValid () ) { Out.Push (Elem); } } return MakeShareable (new FJsonValueArray (Out)); }
其余容器类似,注意TMap
需要分别对其Key
和Value
进行处理。
结构类型 对于USTRUCT
类型的结构体的UStructProperty
,我们可以根据其类型为UScriptStruct*
的成员变量Struct
获得其结构信息,然后递归调用FJsonObjectConverter::UStructToJsonObject()
来处理该结构体。
1 2 3 4 5 6 7 8 else if (UStructProperty *StructProperty = Cast <UStructProperty>(Property)){ TSharedRef<FJsonObject> Out = MakeShareable (new FJsonObject ()); if (UStructToJsonObject (StructProperty->Struct, Value, Out, CheckMetaName)) { return MakeShareable (new FJsonValueObject (Out)); } }
如下内容的UFoo
类,会生成其后内容的json
文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 USTRUCT (BlueprintType, Blueprintable)struct FFooStruct { GENERATED_USTRUCT_BODY () FFooStruct (): StructID (0 ), StructName (TEXT ("InnerStruct" )) { } UPROPERTY (EditAnywhere) int32 StructID; UPROPERTY (EditAnywhere) FString StructName; }; UCLASS (BlueprintType, Blueprintable)class UFoo : public UObject{ GENERATED_UCLASS_BODY () UPROPERTY (EditAnywhere) int32 ID; UPROPERTY (EditAnywhere) FString Name; UPROPERTY (EditAnywhere) TArray<int32> Arrays; UPROPERTY (EditAnywhere) TSet<int32> Sets; UPROPERTY (EditAnywhere) TMap<int32, FString> Maps; UPROPERTY (EditAnywhere) FFooStruct FooStruct; };
UFoo with containers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "ID" : 1 , "Name" : "Foo" , "Arrays" : [ 0 , 1 ] , "Sets" : [ 10 , 20 ] , "Maps" : { "0" : "aa" , "1" : "bb" } , "FooStruct" : { "StructID" : 0 , "StructName" : "InnerStruct" } }
上述过程主要的调用关系如下图所示。
UStructToJsonObject 支持c++原生类 UStructProperty
也支持处理声明的原生c++类型的类或结构体,即不带UCLASS
或USTUCT
的类型。
该功能主要依赖于UScriptStruct
的三个内部类ICppStructOps
、TCppStructOps
和TAutoCppStructOps
及其成员变量CppStructOps
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class UScriptStruct : public UStruct{ public : struct COREUOBJECT_API ICppStructOps { }; template <class CPPSTRUCT > struct TCppStructOps : public ICppStructOps { typedef TStructOpsTypeTraits<CPPSTRUCT> TTraits; TCppStructOps () : ICppStructOps (sizeof (CPPSTRUCT), alignof (CPPSTRUCT)) { } }; template <class CPPSTRUCT > struct TAutoCppStructOps { TAutoCppStructOps (FName InName) { DeferCppStructOps (InName,new TCppStructOps<CPPSTRUCT>); } }; #define IMPLEMENT_STRUCT(BaseName) \ static UScriptStruct::TAutoCppStructOps<F##BaseName> BaseName##_Ops(TEXT(#BaseName)); private : ICppStructOps* CppStructOps; };
ICppStructOps 其中ICppStructOps
为抽象接口,使得与原生类进行交互的对象不依赖具体实现。下面列举了一些它的关键接口,包括但不限于
是否有构造函数/析构函数
构造函数/析构函数
是否有导入/导出文本
导入/导出文本
其中HasXXX()
函数用来判断其接管的原生类是否有相应函数,然后再决定是不是要真正执行其对应的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct COREUOBJECT_API ICppStructOps{ ICppStructOps (int32 InSize, int32 InAlignment) : Size (InSize) , Alignment (InAlignment) { } virtual ~ICppStructOps () {} virtual bool HasZeroConstructor () = 0 ; virtual void Construct (void *Dest) =0 ; virtual bool HasDestructor () = 0 ; virtual void Destruct (void *Dest) = 0 ; virtual bool HasExportTextItem () = 0 ; virtual bool ExportTextItem (FString& ValueStr, const void * PropertyValue, const void * DefaultValue, class UObject* Parent, int32 PortFlags, class UObject* ExportRootScope) = 0 ; virtual bool HasImportTextItem () = 0 ; virtual bool ImportTextItem (const TCHAR*& Buffer, void * Data, int32 PortFlags, class UObject* OwnerObject, FOutputDevice* ErrorText) = 0 ; };
TCppStructOps TCppStructOps
是ICppStructOps
的子类,它实现了ICppStructOps
的接口。它同时是一个模板类,需要传入原生类的类型作为模板参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 template <class CPPSTRUCT >struct TCppStructOps : public ICppStructOps{ typedef TStructOpsTypeTraits<CPPSTRUCT> TTraits; TCppStructOps () : ICppStructOps (sizeof (CPPSTRUCT), alignof (CPPSTRUCT)) { } virtual bool HasZeroConstructor () override { return TTraits::WithZeroConstructor; } virtual void Construct (void *Dest) override { check (!TTraits::WithZeroConstructor); ConstructWithNoInitOrNot <CPPSTRUCT>(Dest); } virtual bool HasDestructor () override { return !(TTraits::WithNoDestructor || TIsPODType<CPPSTRUCT>::Value); } virtual void Destruct (void *Dest) override { check (!(TTraits::WithNoDestructor || TIsPODType<CPPSTRUCT>::Value)); ((CPPSTRUCT*)Dest)->~CPPSTRUCT (); } virtual bool HasExportTextItem () override { return TTraits::WithExportTextItem; } virtual bool ExportTextItem (FString& ValueStr, const void * PropertyValue, const void * DefaultValue, class UObject* Parent, int32 PortFlags, class UObject* ExportRootScope) override { check (TTraits::WithExportTextItem); return ExportTextItemOrNot (ValueStr, (const CPPSTRUCT*)PropertyValue, (const CPPSTRUCT*)DefaultValue, Parent, PortFlags, ExportRootScope); } virtual bool HasImportTextItem () override { return TTraits::WithImportTextItem; } virtual bool ImportTextItem (const TCHAR*& Buffer, void * Data, int32 PortFlags, class UObject* OwnerObject, FOutputDevice* ErrorText) override { check (TTraits::WithImportTextItem); return ImportTextItemOrNot (Buffer, (CPPSTRUCT*)Data, PortFlags, OwnerObject, ErrorText); } };
其实现的接口大部分都是转发调用了真正对象的对应接口。以ExportTextItemOrNot
为例,该函数实际转发了真正的对象PropertyValue
所实现的ExportTextItem
函数。
1 2 3 4 5 6 template <class CPPSTRUCT >FORCEINLINE typename TEnableIf<TStructOpsTypeTraits<CPPSTRUCT>::WithExportTextItem, bool >::Type ExportTextItemOrNot (FString& ValueStr, const CPPSTRUCT* PropertyValue, const CPPSTRUCT* DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) { return PropertyValue->ExportTextItem (ValueStr, *DefaultValue, Parent, PortFlags, ExportRootScope); }
除此之外,其中typedef TStructOpsTypeTraits<CPPSTRUCT> TTraits;
为特性类型模板,TCppStructOps
使用该模板类来判断原生类的某些特性是否开启,即作为某些HasXXX()
函数的判断条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <class CPPSTRUCT >struct TStructOpsTypeTraitsBase2 { enum { WithZeroConstructor = false , WithNoInitConstructor = false , WithNoDestructor = false , WithExportTextItem = false , WithImportTextItem = false , }; }; template <class CPPSTRUCT >struct TStructOpsTypeTraits : public TStructOpsTypeTraitsBase2<CPPSTRUCT>{ };
可以看到,在基类里所有的特性都是关闭的,我们需要对一个原生类型进行具体化TStructOpsTypeTraits
类型来决定其打开哪些特性。
TAutoCppStructOps TAutoCppStructOps
是用来辅助创建TCppStructOps
的,我们通过调用IMPLEMENT_STRUCT
来创建一个TCppStructOps
并将其放入一个静态全局TMap
里,并在UScriptStruct
构造时调用的PrepareCppStructOps()
函数里用来根据名称查询并初始化UScriptStruct
的CppStructOps
成员变量。
1 2 3 4 5 6 7 void UScriptStruct::PrepareCppStructOps () { if (!CppStructOps) { CppStructOps = GetDeferredCppStructOps ().FindRef (GetFName ()); } }
应用 我们最终在处理UStructProperty
的属性时,就可以根据其成员变量Struct
的CppStructOps
来判断其是否为一个原生的c++类型,并获得其相应的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 else if (UStructProperty *StructProperty = Cast <UStructProperty>(Property)){ UScriptStruct::ICppStructOps* TheCppStructOps = StructProperty->Struct->GetCppStructOps (); if (TheCppStructOps && TheCppStructOps->HasExportTextItem ()) { FString OutValueStr; TheCppStructOps->ExportTextItem (OutValueStr, Value, nullptr , nullptr , PPF_None, nullptr ); return MakeShareable (new FJsonValueString (OutValueStr)); } TSharedRef<FJsonObject> Out = MakeShareable (new FJsonObject ()); if (UStructToJsonObject (StructProperty->Struct, Value, Out, CheckMetaName)) { return MakeShareable (new FJsonValueObject (Out)); } }
例子 说了这么多,来看一个具体的例子。
我们经常会碰到一个FSoftObjectPath
类型来表示某资源的软链接。这个类其实并非是一个USTRUCT
,而只是一个普普通通的原生struct
。
1 2 3 4 5 6 7 8 9 struct COREUOBJECT_API FSoftObjectPath{ FSoftObjectPath () {} bool ExportTextItem (FString& ValueStr, FSoftObjectPath const & DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const ; bool ImportTextItem ( const TCHAR*& Buffer, int32 PortFlags, UObject* Parent, FOutputDevice* ErrorText ) ; }
但它却可以直接利用反射信息转换出json数据。
FSoftObjectPath 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "ID" : 1 , "Name" : "Foo" , "Arrays" : [ 0 , 1 ] , "Sets" : [ 10 , 20 ] , "Maps" : { "0" : "aa" , "1" : "bb" } , "FooStruct" : { "StructID" : 0 , "StructName" : "InnerStruct" } , "SoftObjectPath" : "/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial" }
这是因为该类具体化了TStructOpsTypeTraits
,并调用了IMPLEMENT_STRUCT
创建了CppStructOps
对象。同时该类实现了自己的ExportTextItem
接口供TCppStructOps
调用。即
1 2 3 4 5 6 7 8 9 10 11 template <>struct TStructOpsTypeTraits <FSoftObjectPath> : public TStructOpsTypeTraitsBase2<FSoftObjectPath>{ enum { WithZeroConstructor = true , WithExportTextItem = true , WithImportTextItem = true , }; }; IMPLEMENT_STRUCT (SoftObjectPath);
同理,对于任意我们自己定义的c++原生结构体或者类,只要做到以上三件事情,就可以优雅地利用反射信息将其实转化为json
格式。
显示具体化TStructOpsTypeTraits
。
调用IMPLEMENT_STRUCT
。
实现对应接口,如ExportTextItem
、ImportTextItem
等。
从小火柴到打火机 至此,我们已然可以灵活利用上文的方法将一个UObject
类对象优雅地转换成json
格式的文件,也可以相应转换回来,转换回来的流程与之类似,读者可对比理解,此处不做赘述。
但我们可以继续定制化一些内容,使其使用起来更更得心应手一些。
自定义过滤成员变量 在UStructToJsonAttributes()
一开始判断Property是否需要跳过的时候,我们可以根据该属性的meta数据进行过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bool UStructToJsonAttributes ( const UStruct* StructDefinition, const void * Struct, TMap< FString, TSharedPtr<FJsonValue> >& OutJsonAttributes, int64 CheckFlags, int64 SkipFlags ) { for (TFieldIterator<UProperty> It (StructDefinition); It; ++It) { UProperty* Property = *It; if (Property->HasMetaData (TEXT ("JsonIgnore" ))) { continue ; } } return true ; }
此时我们只需要给UFoo
的某个属性加上JsonIgnore
字段的元数据,那么该属性就不会在最终生成的json格式中出现。
1 2 3 4 5 6 7 8 9 10 UCLASS (BlueprintType, Blueprintable)class BALER_API UFoo : public UObject{ GENERATED_UCLASS_BODY () UPROPERTY (EditAnywhere, meta=(JsonIgnore = "" )) int32 ID; UPROPERTY (EditAnywhere) FString Name; };
使用DisplayName作为输出命名 同理,我们只需要从该属性的DisplayName
元数据里获得的字符串作为该json字段的名称,就可以使生成的json格式的属性字段用上我们自定义的命名了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 FString GetPropertyNameForJson (const UProperty* Property) { FString PropertyName = Property->GetName (); if (Property->HasMetaData (TEXT ("DisplayName" ))) { PropertyName = Property->GetMetaData (TEXT ("DisplayName" )); } return PropertyName; } bool UStructToJsonAttributes ( const UStruct* StructDefinition, const void * Struct, TMap< FString, TSharedPtr<FJsonValue> >& OutJsonAttributes, int64 CheckFlags, int64 SkipFlags ) { for (TFieldIterator<UProperty> It (StructDefinition); It; ++It) { UProperty* Property = *It; FString VariableName = GetPropertyNameForJson (Property); const void * Value = Property->ContainerPtrToValuePtr <uint8>(Struct); OutJsonAttributes.Add (VariableName, JsonValue); } return true ; }
此时我们只需要给UFoo
的某个属性加上DisplayName
字段的元数据,那么该属性在json
中的字段名就是我们自已定义的字段名了。
1 2 3 4 5 6 7 8 9 10 UCLASS (BlueprintType, Blueprintable)class BALER_API UFoo : public UObject{ GENERATED_UCLASS_BODY () UPROPERTY (EditAnywhere, meta=(DisplayName = "MyID" )) int32 ID; UPROPERTY (EditAnywhere, meta=(DisplayName = "MyName" )) FString Name; };
1 2 3 4 { "MyID" : 1 , "MyName" : "Foo" }
含有基类指针成员变量的情况 正文开始-_-。
考虑如下场景。我们在UFoo
类内有两个UFooInner*
类型的成员变量。其中一个成员变量指向UFooInner
类型,而另一个指向其子类UFooInnerSub
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 UCLASS (BlueprintType, Blueprintable)class BALER_API UFooInner : public UObject{ GENERATED_UCLASS_BODY () UPROPERTY (EditAnywhere) int32 InnerID; }; UCLASS (BlueprintType, Blueprintable)class BALER_API UFooInnerSub : public UFooInner{ GENERATED_UCLASS_BODY () UPROPERTY (EditAnywhere) FString InnerName; }; UCLASS (BlueprintType, Blueprintable)class BALER_API UFoo : public UObject{ GENERATED_UCLASS_BODY () UPROPERTY (EditAnywhere, meta=(IgnoreBalerConfig = "" )) int32 ID; UPROPERTY (EditAnywhere) FString Name; UPROPERTY (EditAnywhere) UFooInner* FooInner; UPROPERTY (EditAnywhere) UFooInner* FooInner2; };
即FooInner
指向一个UFooInner
类型的对象,而FooInner2
指向一个UFooInnnerSub
类型的对象,即
1 2 FooInner = NewObject <UFooInner>(); FooInner2 = NewObject <UFooInnerSub>();
这种情况下我们将UFoo
转为json
格式,会出现什么情况呢?
理想的结果 理想情况下, 我们希望最终转换成的json
格式如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 { "ID" : 1 , "Name" : "Foo" , "FooInner" : { "InnerID" : 10 } , "FooInner2" : { "InnerID" : 10 , "InnerName" : "FooInner" } }
惨淡的事实 事实是FooInner
和FooInner2
只会输出它们所代表的资源路径字符串,即
1 2 3 4 5 6 { "ID" : 1 , "Name" : "Foo" , "FooInner" : "FooInner'/Engine/Transient.FooInner_0'" , "FooInner2" : "FooInnerSub'/Engine/Transient.FooInnerSub_0'" }
这是因为FooInner
和FooInner2
对应的UProperty
都是UObjectProperty
类型的,它并不属于我们前面处理过的任何一种类型的Property
,所以它们会默认输出ExportTextItem()
的内容。
所以我们第一步需要添加对于UObjectProperty
类型的处理。我们通过GetObjectPropertyValue()
获得该Property
对应的对象,通过该对象的GetClass()
方法获取其UClass*
变量,UClass
继承自UStruct
,也可表示该属性的结构信息。然后将该UClass
变量作为输入,递归调用UStructToJsonObject()
即可处理该对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 TSharedPtr<FJsonValue> ConvertScalarUPropertyToJsonValue ( UProperty* Property, const void * Value, int64 CheckFlags, int64 SkipFlags ) { else if (UObjectProperty *ObjectProperty = Cast <UObjectProperty>(Property)) { UObject* t_Obj = ObjectProperty->GetObjectPropertyValue (Value); TSharedRef<FJsonObject> Out = MakeShareable (new FJsonObject ()); if (IsValid (t_Obj) && UStructToJsonObject (t_Obj->GetClass (), t_Obj, Out, CheckMetaName)) { return MakeShareable (new FJsonValueObject (Out)); } } }
至此,最终生成的json
格式和理想的结果果然一模一样!
但是! 我们无法从这个理想的json
格式再转回原来的对象了!
因为在调用JsonObjectToUStruct()
转回去时, 虽然ConvertScalarJsonValueToUPropertyWithContainer()
已经帮我们处理了UObjectProperty
类型的属性,如下文代码所示。但是我们已不知道FooInner2
是指向一个UFooInnerSub
类型的了,我们只能按照它声明的类型指它转为一个父类对象!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 bool ConvertScalarJsonValueToUPropertyWithContainer ( const TSharedPtr<FJsonValue>& JsonValue, UProperty* Property, void * OutValue, const UStruct* ContainerStruct, void * Container, int64 CheckFlags, int64 SkipFlags ) { else if (UObjectProperty *ObjectProperty = Cast <UObjectProperty>(Property)) { if (JsonValue->Type == EJson::Object) { UObject* Outer = GetTransientPackage (); if (ContainerStruct->IsChildOf (UObject::StaticClass ())) { Outer = (UObject*)Container; } UClass* PropertyClass = ObjectProperty->PropertyClass; UObject* createdObj = StaticAllocateObject (PropertyClass, Outer, NAME_None, EObjectFlags::RF_NoFlags, EInternalObjectFlags::None, false ); (*PropertyClass->ClassConstructor)(FObjectInitializer (createdObj, PropertyClass->ClassDefaultObject, false , false )); ObjectProperty->SetObjectPropertyValue (OutValue, createdObj); TSharedPtr<FJsonObject> Obj = JsonValue->AsObject (); check (Obj.IsValid ()); if (!JsonAttributesToUStructWithContainer (Obj->Values, ObjectProperty->PropertyClass, createdObj, ObjectProperty->PropertyClass, createdObj, CheckFlags & (~CPF_ParmFlags), SkipFlags)) { UE_LOG (LogJson, Error, TEXT ("JsonValueToUProperty - FJsonObjectConverter::JsonObjectToUStruct failed for property %s" ), *Property->GetNameCPP ()); return false ; } } } }
这是因为该过程会首先利用ObjectProperty->PropertyClass
并调用StaticAllocateObject
现场创建一个UObject。然后再使用对应的JsonOjbectValue
去调用JsonAttributesToUStructWithContainer()
递归地初始化这个UObject。但是这里的PropertyClass
是从该对象的类声明中来的,显然它并不携带任何关于FooInner2
是指向UFooInnerSub
的这种信息的,它只知道FooInner2
也是一个被声明为UFooInner
类型的指针。
所以转回去的结果就是FooInner2
也变成了一个指向UFooInner
对象的指针,它只携带原FooInner2
关于UFooInner
部分的数据,而其关于UFooInnerSub
的数据,则全部丢失了。
优雅地解决 要想解决这个问题,我们需要找到一种方法可以把FooInner2
是指向UFooInnerSub
的这一消息记录下来。
一种比较优雅的做法是,我们在转成json
格式的时候,对于这种UObjectProperty
类型的属性,都额外添加一条名为Type
的记录,记录的值为其Class
的名字。即
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 TSharedPtr<FJsonValue> ConvertScalarUPropertyToJsonValue ( UProperty* Property, const void * Value, int64 CheckFlags, int64 SkipFlags ) { else if (UObjectProperty *ObjectProperty = Cast <UObjectProperty>(Property)) { UObject* t_Obj = ObjectProperty->GetObjectPropertyValue (Value); TSharedRef<FJsonObject> Out = MakeShareable (new FJsonObject ()); if (IsValid (t_Obj) && UStructToJsonObject (t_Obj->GetClass (), t_Obj, Out, CheckMetaName)) { FString ClassName = t_Obj->GetClass ()->GetPathName (); Out->Values.Add (TEXT ("Type" ), MakeShareable (new FJsonValueString (ClassName))); return MakeShareable (new FJsonValueObject (Out)); } } }
此时UFoo
转换成的json
格式内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "ID" : 1 , "Name" : "Foo" , "FooInner" : { "InnerID" : 10 , "Type" : "/Script/Engine.FooInner" } , "FooInner2" : { "InnerID" : 10 , "InnerName" : "FooInner" , "Type" : "/Script/Baler.FooInnerSub" } }
这就使得json
格式内容有了FooInner2
是指向UFooInnerSub
的这种信息。
然后我们在json
格式转回UFoo
时,便可通过Type
字段,调用FindObject<UClass>()
来获得其真正的UClass
,并用其创建真正的UFooInnerSub
类型的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 bool ConvertScalarJsonValueToUPropertyWithContainer ( const TSharedPtr<FJsonValue>& JsonValue, UProperty* Property, void * OutValue, const UStruct* ContainerStruct, void * Container, int64 CheckFlags, int64 SkipFlags ) { else if (UObjectProperty *ObjectProperty = Cast <UObjectProperty>(Property)) { if (JsonValue->Type == EJson::Object) { UObject* Outer = GetTransientPackage (); if (ContainerStruct->IsChildOf (UObject::StaticClass ())) { Outer = (UObject*)Container; } UClass* PropertyClass = ObjectProperty->PropertyClass; TSharedPtr<FJsonObject> Obj = JsonValue->AsObject (); FString ClassName = Obj->GetStringField (TEXT ("Type" )); if (!ClassName.IsEmpty ()) { UClass* FoundClass = FindObject <UClass>(ANY_PACKAGE, *ClassName); if (FoundClass) { PropertyClass = FoundClass; } } UObject* createdObj = StaticAllocateObject (PropertyClass, Outer, NAME_None, EObjectFlags::RF_NoFlags, EInternalObjectFlags::None, false ); (*PropertyClass->ClassConstructor)(FObjectInitializer (createdObj, PropertyClass->ClassDefaultObject, false , false )); ObjectProperty->SetObjectPropertyValue (OutValue, createdObj); check (Obj.IsValid ()); if (!JsonAttributesToUStructWithContainer (Obj->Values, ObjectProperty->PropertyClass, createdObj, ObjectProperty->PropertyClass, createdObj, CheckFlags & (~CPF_ParmFlags), SkipFlags)) { UE_LOG (LogJson, Error, TEXT ("JsonValueToUProperty - FJsonObjectConverter::JsonObjectToUStruct failed for property %s" ), *Property->GetNameCPP ()); return false ; } } } }
至此便实现了原来所不支持的含有基类指针但指向子类对象
的成员变量的UObject
与json
格式互相转换的功能。