zl程序教程

您现在的位置是:首页 >  Javascript

当前栏目

【ue4】包含基类指针成员变量的UOject与json文件互转

2023-02-18 16:38:55 时间

前言

在使用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格式字符之前的转换。

我们可以使用FJsonObjectSetXXXField()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的成员变量时也不需要修改这两个方法,十分优雅,完全符合我们的需求。

UObjectJson时调用了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()));
}

容器类型

对于 TArrayTSetTMap等容器类型,通常会遍历该容器的每一个元素,然后对单个元素继续递归调用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需要分别对其KeyValue进行处理。

结构类型

对于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++类型的类或结构体,即不带UCLASSUSTUCT的类型。

该功能主要依赖于UScriptStruct的三个内部类ICppStructOpsTCppStructOpsTAutoCppStructOps及其成员变量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

TCppStructOpsICppStructOps的子类,它实现了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()函数里用来根据名称查询并初始化UScriptStructCppStructOps成员变量。

void UScriptStruct::PrepareCppStructOps()
{
    if (!CppStructOps)
    {
        CppStructOps = GetDeferredCppStructOps().FindRef(GetFName());
    }
}

应用

我们最终在处理UStructProperty的属性时,就可以根据其成员变量StructCppStructOps来判断其是否为一个原生的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。 实现对应接口,如ExportTextItemImportTextItem等。

从小火柴到打火机

至此,我们已然可以灵活利用上文的方法将一个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"
    }
}

惨淡的事实

事实是FooInnerFooInner2只会输出它们所代表的资源路径字符串,即

{
    "ID": 1,
    "Name": "Foo",
    "FooInner": "FooInner'/Engine/Transient.FooInner_0'",
    "FooInner2": "FooInnerSub'/Engine/Transient.FooInnerSub_0'"
}

这是因为FooInnerFooInner2对应的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... 
    }
}

至此便实现了原来所不支持的含有基类指针但指向子类对象的成员变量的UObjectjson格式互相转换的功能。