当前栏目
【ue4】包含基类指针成员变量的UOject与json文件互转
前言
在使用ue4时我们经常会碰到需要把UObject
类和json
文件互相转换的情形。
ue4本身封装了相当充足的处理json的接口,所以我们可以通过多种方式达到这一目的。比如对于UObject
的每一个成员属性,都手动调用生成json格式文本的接口,最终生成json格式的字符串保存到磁盘文件里,这种方法可以命名为钻木取火
。还比如我们可以依托于ue4的反射信息,在不必关心UObject
具体内容的情况下自动生成json格式,这种方法才是火柴取火
。
我们将以如下一个简单的UObject
为例,分别在ue4里使用钻木取火
和火柴取火
来实现其与json文件的互转。并在此之后尝试把这个火柴
看能不能优化成一个用起来得心应手的打火机
。
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格式进行转换,一个简洁的示例如下。
// UObject -> Json
void SaveToJson(UFoo* Foo)
{
// fill FJsonObject
TSharedPtr<FJsonObject> FooJsonObj = MakeShareable(new FJsonObject());
FooJsonObj->SetNumberField("ID", Foo->ID);
FooJsonObj->SetStringField("Name", Foo->Name);
// convert FJsonObject to FString
FString JsonStr;
const TSharedRef<TJsonWriter<TCHAR> > JsonWriter = TJsonWriterFactory<TCHAR>::Create(&JsonStr);
FJsonSerializer::Serialize(JsonObj.ToSharedRef(), JsonWriter);
// save FString to file
FFileHelper::SaveStringToFile(JsonStr, TEXT("Foo.json"));
}
// Json -> UObject
bool LoadFromJson(UFoo* Foo, const FString& Path)
{
// load FString from file
FString JsonStr;
FFileHelper::LoadFileToString(JsonStr, *Path);
// convert FString to FJsonObject
TSharedPtr<FJsonObject> FooJsonObj = MakeShareable(new FJsonObject());
const TSharedRef<TJsonReader<> > JsonReader = TJsonReaderFactory<>::Create(JsonStr);
const bool tRet = FJsonSerializer::Deserialize(JsonReader, FooJsonObj);
// fill UObject from FJsonObject
FooJsonObj->TryGetNumberField("ID", Foo->ID);
FooJsonObj->TryGetStringField("Name", Foo->Name);
return tRet;
}
UFoo
类在ue4编辑器里的细节面板显示如下。
UFoo
最终生成的UFoo
的等价json
文件如下。
{
"ID": 1,
"Name": "Foo"
}
这种方式的缺点不言自明,不明而喻。首先对于每一个类,我们都需要繁琐地实现SaveToJson()
和LoadFromJson()
函数。其次当需要修改某个类的成员变量时,也需要同时修改这两个函数,毫无优雅可言。
所以我们急需一种可以抹去类型、无需关注内容修改的方式。
火柴取火
众所周知,ue4本身的反射信息很充足,我们可以通过一个类的UStruct
来获得该类的结构,包括其成员变量UProperty
和成员函数UFunction
,这里我们只关注成员变量,所以只要可以遍历类的反射信息里的UProperty
信息即可。
ue4也极其贴心地封装了这一部分接口,位于Engine\Source\Runtime\JsonUtilities\Private\JsonObjectConverter.cpp
下。
使用
一个与上一节钻木取火
中的示例对应的代码如下所示。
// UObject -> Json
void SaveToJson(UFoo* Foo)
{
// fill FJsonObject
TSharedPtr<FJsonObject> FooJsonObj = MakeShareable(new FJsonObject());
//FooJsonObj->SetNumberField("ID", Foo->ID);
//FooJsonObj->SetStringField("Name", Foo->Name);
FJsonObjectConverter::UStructToJsonObject(Foo->GetClass(), Foo, FooJsonObj, 0, 0);
// convert FJsonObject to FString
FString JsonStr;
const TSharedRef<TJsonWriter<TCHAR> > JsonWriter = TJsonWriterFactory<TCHAR>::Create(&JsonStr);
FJsonSerializer::Serialize(JsonObj.ToSharedRef(), JsonWriter);
// save FString to file
FFileHelper::SaveStringToFile(JsonStr, TEXT("Foo.json"));
}
// Json -> UObject
bool LoadFromJson(UFoo* Foo, const FString& Path)
{
// load FString from file
FString JsonStr;
FFileHelper::LoadFileToString(JsonStr, *Path);
// convert FString to FJsonObject
TSharedPtr<FJsonObject> FooJsonObj = MakeShareable(new FJsonObject());
const TSharedRef<TJsonReader<> > JsonReader = TJsonReaderFactory<>::Create(JsonStr);
const bool tRet = FJsonSerializer::Deserialize(JsonReader, FooJsonObj);
// fill UObject from FJsonObject
//FooJsonObj->TryGetNumberField("ID", Foo->ID);
//FooJsonObj->TryGetStringField("Name", Foo->Name);
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()
来分析一下其实现。
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;
// check to see if we should ignore this property
if (CheckFlags != 0 && !Property->HasAnyPropertyFlags(CheckFlags)) { continue; }
if (Property->HasAnyPropertyFlags(SkipFlags)) { continue; }
FString VariableName = StandardizeCase(Property->GetName());
const void* Value = Property->ContainerPtrToValuePtr<uint8>(Struct);
// convert the property to a FJsonValue
TSharedPtr<FJsonValue> JsonValue = UPropertyToJsonValue(
Property, Value, CheckFlags, SkipFlags
);
if (!JsonValue.IsValid()) { return false; }
// set the value on the output object
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
类型。
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
的操作。
TSharedPtr<FJsonValue> ConvertScalarUPropertyToJsonValue(
UProperty* Property, const void* Value,
int64 CheckFlags, int64 SkipFlags
) {
if (UEnumProperty* EnumProperty = Cast<UEnumProperty>(Property))
{
// todo...
}
else if (UNumericProperty *NumericProperty = Cast<UNumericProperty>(Property))
{
// todo...
}
else if (UBoolProperty *BoolProperty = Cast<UBoolProperty>(Property))
{
// todo...
}
else if (UStrProperty *StringProperty = Cast<UStrProperty>(Property))
{
// todo...
}
else if (UTextProperty *TextProperty = Cast<UTextProperty>(Property))
{
// todo...
}
else if (UArrayProperty *ArrayProperty = Cast<UArrayProperty>(Property))
{
// todo...
}
else if ( USetProperty* SetProperty = Cast<USetProperty>(Property) )
{
// todo...
}
else if ( UMapProperty* MapProperty = Cast<UMapProperty>(Property) )
{
// todo...
}
else if (UStructProperty *StructProperty = Cast<UStructProperty>(Property))
{
// todo...
}
else
{
// todo...
}
return TSharedPtr<FJsonValue>();
}
该方法根据单个Property
的具体类型分别进行处理,大致分为以下几类。
单值类型
枚举、数值、布尔、字符串型元素的处理很简单,直接转换为对应的FJsonValue
类型即可。
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
为例,其处理过程大致如下。
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() )
{
// add to the array
Out.Push(Elem);
}
}
return MakeShareable(new FJsonValueArray(Out));
}
其余容器类似,注意TMap
需要分别对其Key
和Value
进行处理。
结构类型
对于USTRUCT
类型的结构体的UStructProperty
,我们可以根据其类型为UScriptStruct*
的成员变量Struct
获得其结构信息,然后递归调用FJsonObjectConverter::UStructToJsonObject()
来处理该结构体。
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
文件。
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
{
"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
。
class UScriptStruct : public UStruct
{
public:
/** Interface to template to manage dynamic access to C++ struct construction and destruction **/
struct COREUOBJECT_API ICppStructOps
{
};
/** Template to manage dynamic access to C++ struct construction and destruction **/
template<class CPPSTRUCT>
struct TCppStructOps : public ICppStructOps
{
typedef TStructOpsTypeTraits<CPPSTRUCT> TTraits;
TCppStructOps()
: ICppStructOps(sizeof(CPPSTRUCT), alignof(CPPSTRUCT))
{
}
};
/** Template for noexport classes to autoregister before main starts **/
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:
/** Holds the Cpp ctors and dtors, sizeof, etc. Is not owned by this and is not released. **/
ICppStructOps* CppStructOps;
};
ICppStructOps
其中ICppStructOps
为抽象接口,使得与原生类进行交互的对象不依赖具体实现。下面列举了一些它的关键接口,包括但不限于
是否有构造函数/析构函数 构造函数/析构函数 是否有导入/导出文本 导入/导出文本
其中HasXXX()
函数用来判断其接管的原生类是否有相应函数,然后再决定是不是要真正执行其对应的函数。
struct COREUOBJECT_API ICppStructOps
{
ICppStructOps(int32 InSize, int32 InAlignment)
: Size(InSize)
, Alignment(InAlignment)
{
}
virtual ~ICppStructOps() {}
/** return true if memset can be used instead of the constructor **/
virtual bool HasZeroConstructor() = 0;
/** Call the C++ constructor **/
virtual void Construct(void *Dest)=0;
/** return false if this destructor can be skipped **/
virtual bool HasDestructor() = 0;
/** Call the C++ destructor **/
virtual void Destruct(void *Dest) = 0;
/** return true if this struct can export **/
virtual bool HasExportTextItem() = 0;
/**
* export this structure
* @return true if the copy was exported, otherwise it will fall back to UStructProperty::ExportTextItem
*/
virtual bool ExportTextItem(FString& ValueStr, const void* PropertyValue, const void* DefaultValue, class UObject* Parent, int32 PortFlags, class UObject* ExportRootScope) = 0;
/** return true if this struct can import **/
virtual bool HasImportTextItem() = 0;
/**
* import this structure
* @return true if the copy was imported, otherwise it will fall back to UStructProperty::ImportText
*/
virtual bool ImportTextItem(const TCHAR*& Buffer, void* Data, int32 PortFlags, class UObject* OwnerObject, FOutputDevice* ErrorText) = 0;
};
TCppStructOps
TCppStructOps
是ICppStructOps
的子类,它实现了ICppStructOps
的接口。它同时是一个模板类,需要传入原生类的类型作为模板参数。
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); // don't call this if we have indicated it is not necessary
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); // don't call this if we have indicated it is not necessary
return ImportTextItemOrNot(Buffer, (CPPSTRUCT*)Data, PortFlags, OwnerObject, ErrorText);
}
};
其实现的接口大部分都是转发调用了真正对象的对应接口。以ExportTextItemOrNot
为例,该函数实际转发了真正的对象PropertyValue
所实现的ExportTextItem
函数。
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()
函数的判断条件。
/** type traits to cover the custom aspects of a script struct **/
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
成员变量。
void UScriptStruct::PrepareCppStructOps()
{
if (!CppStructOps)
{
CppStructOps = GetDeferredCppStructOps().FindRef(GetFName());
}
}
应用
我们最终在处理UStructProperty
的属性时,就可以根据其成员变量Struct
的CppStructOps
来判断其是否为一个原生的c++类型,并获得其相应的结果。
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
。
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
{
"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
调用。即
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数据进行过滤。
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;
// ...
// skip by meta data
if (Property->HasMetaData(TEXT("JsonIgnore")))
{
continue;
}
// ...
}
return true;
}
此时我们只需要给UFoo
的某个属性加上JsonIgnore
字段的元数据,那么该属性就不会在最终生成的json格式中出现。
UCLASS(BlueprintType, Blueprintable)
class BALER_API UFoo : public UObject
{
GENERATED_UCLASS_BODY()
UPROPERTY(EditAnywhere, meta=(JsonIgnore = ""))
int32 ID;
UPROPERTY(EditAnywhere)
FString Name;
};
{
"Name": "Foo"
}
使用DisplayName作为输出命名
同理,我们只需要从该属性的DisplayName
元数据里获得的字符串作为该json字段的名称,就可以使生成的json格式的属性字段用上我们自定义的命名了。
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;
// get name from meta display name
FString VariableName = GetPropertyNameForJson(Property);
const void* Value = Property->ContainerPtrToValuePtr<uint8>(Struct);
//...
OutJsonAttributes.Add(VariableName, JsonValue);
}
return true;
}
此时我们只需要给UFoo
的某个属性加上DisplayName
字段的元数据,那么该属性在json
中的字段名就是我们自已定义的字段名了。
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;
};
{
"MyID": 1,
"MyName": "Foo"
}
含有基类指针成员变量的情况
正文开始-_-。
考虑如下场景。我们在UFoo
类内有两个UFooInner*
类型的成员变量。其中一个成员变量指向UFooInner
类型,而另一个指向其子类UFooInnerSub
。
// ==============================
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
类型的对象,即
FooInner = NewObject<UFooInner>();
FooInner2 = NewObject<UFooInnerSub>();
这种情况下我们将UFoo
转为json
格式,会出现什么情况呢?
理想的结果
理想情况下, 我们希望最终转换成的json
格式如下。
{
"ID": 1,
"Name": "Foo",
"FooInner":
{
"InnerID": 10
},
"FooInner2":
{
"InnerID": 10,
"InnerName": "FooInner"
}
}
惨淡的事实
事实是FooInner
和FooInner2
只会输出它们所代表的资源路径字符串,即
{
"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()
即可处理该对象。
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
类型的了,我们只能按照它声明的类型指它转为一个父类对象!
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()); // should not fail if Type == EJson::Object
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;
}
}
//else...
}
}
这是因为该过程会首先利用ObjectProperty->PropertyClass
并调用StaticAllocateObject
现场创建一个UObject。然后再使用对应的JsonOjbectValue
去调用JsonAttributesToUStructWithContainer()
递归地初始化这个UObject。但是这里的PropertyClass
是从该对象的类声明中来的,显然它并不携带任何关于FooInner2
是指向UFooInnerSub
的这种信息的,它只知道FooInner2
也是一个被声明为UFooInner
类型的指针。
所以转回去的结果就是FooInner2
也变成了一个指向UFooInner
对象的指针,它只携带原FooInner2
关于UFooInner
部分的数据,而其关于UFooInnerSub
的数据,则全部丢失了。
优雅地解决
要想解决这个问题,我们需要找到一种方法可以把FooInner2
是指向UFooInnerSub
的这一消息记录下来。
一种比较优雅的做法是,我们在转成json
格式的时候,对于这种UObjectProperty
类型的属性,都额外添加一条名为Type
的记录,记录的值为其Class
的名字。即
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
格式内容为
{
"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
类型的对象。
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()); // should not fail if Type == EJson::Object
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;
}
}
//else...
}
}
至此便实现了原来所不支持的含有基类指针但指向子类对象
的成员变量的UObject
与json
格式互相转换的功能。
相关文章
- 基于 hugging face 预训练模型的实体识别智能标注方案:生成doccano要求json格式
- python读取json格式文件大量数据,以及python字典和列表嵌套用法详解
- 图学习【参考资料2】-知识补充与node2vec代码注解
- Paddle Graph Learning (PGL)图学习之图游走类deepwalk、node2vec模型[系列四]
- Java爬虫框架Jsoup学习记录
- Android开发——获得Json数据,并显示图片
- nodejs事件轮询详述
- JSON and Microsoft Technologies(翻译)
- JS常用的几种设计模式
- pm2:在生产环境中运行 nodejs 应用
- 一些你需要掌握的 tsconfig.json 常用配置项
- 用 nodejs 实现 http 服务版本的 hello world
- 聊聊 JavaScript 的几种模块系统
- Node.js 是怎么找到模块的?
- Node.js 是如何做 GC (垃圾回收)的?
- React 源码:ReactElement 和 FiberNode 是什么?
- JS 当中的函数柯里化和高阶函数
- 数据提取-JsonPath
- Selenium与PhantomJS
- 数据提取-PyQuery