UE4 C++ GameplayAbilitiesのAttributeSetをJsonに書き込んだり読み込んだり

やあ

ゲームのセーブ、ロードの実装中にAttributeSetをJsonに保存したいなーって思って調べたよ。

前置き

Json関連のものを使えるようにするためにモジュールを追加する。

PublicDependencyModuleNames.AddRange(new string[] { 
    "Json", "JsonUtilities"
});

Jsonの読み書きに使うヘッダー。

/* 必須 */
#include "Json.h"
/* ファイルの読み込みや書き込みに使う */
#include "Misc/FileHelper.h"
/* UStructからFJsonObjectに変えたりするときに使う */
#include "JsonUtilities/Public/JsonObjectConverter.h"

FJsonObject
名前と値を保持してくれる。

TSharedPtr<FJsonObject> JsonObj = MakeShareable(new FJsonObject);

例えば、キャラクターの体力を保持したい場合。

JsonObj->SetNumberField(”Health”, 60);

この時、中身は

{
    "Health" : 60
}

になる。

ネストしたい場合。
例、キャラクターのIDの中に、体力の情報を入れる。

TSharedPtr<FJsonObject> RootJsonObj = MakeShareable(new FJsonObject);
TSharedPtr<FJsonObject> ElementJsonObj = MakeShareable(new FJsonObject);
ElementJsonObj->SetNumberField("Health", 60);

RootJsonObj->SetObjectField("CharacterID", ElementJsonObj);

この時、中身は

{
    "CharacterID" : {
        "Health" : 60
    }
}

になる。

作ったFJsonObjectを保存する。

FString OutPutString;
/* OutPutStringに書き込むように指定する */
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutPutString);
/* JsonObjに保持している名前と値をWriterに書き込む */
FJsonSerializer::Serialize(JsonObj.ToSharedRef(), Writer);
/* Project/Saved/ に保存する */
FFileHelper::SaveStringToFile(OutPutString, *(FPaths::ProjectSavedDir() + "Filename.json"));

Jsonファイルを読み込む。

FString LoadJsonStringData;
/* LoadJsonStringDataにJsonの中身を読み込む */
if (FFileHelper::LoadFileToString(LoadJsonStringData, "FileFullPath")
{
    TSharedPtr<FJsonObject> LoadedJsonObj = MakeShareable(new FJsonObject);
    /* Jsonの読み込み先をLoadJsonStringDataにする */
    const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(LoadJsonStringData);
    /* ReaderからLoadJsonObjに */
    if (FJsonSerializer::Deserialize(Reader, LoadJsonObj))
    {
        TSharedPtr<FJsonObject> CharacterData = LoadJsonObj->GetObjectField("CharacterID");
        float Health = CharacterData->GetNumberField("Health");
    }
}

ここまで理解できればすぐにできるよ。

やる

こんな感じの構造を想定してるよ。

{
    "CharacterA": [
        {
            "Health":
            {
                "baseValue": 100,
                "currentValue": 100
            }
        },
        {
            "MaxHealth":
            {
                "baseValue": 100,
                "currentValue": 100
            }
        }
    ],
    "CharacterB": [
        {
            "Health":
            {
    // 以下省略
}

Jsonに書き込む

#include "Json.h"
#include "Misc/FileHelper.h"
#include "JsonUtilities/Public/JsonObjectConverter.h"
#include "Kismet/GameplayStatics.h"

void UHogeSaveGame::BeginSave(const UObject* _WorldContextObject)
{
    TArray<AActor*> OutActors;
    UGameplayStatics::GetAllActorsOfClass(_WorldContextObject, AHogeCharacter::StaticClass(), OutActors);

    TSharedPtr<FJsonObject> SaveJsonObj = MakeShareable(new FJsonObject());
    for (AActor* Actor : OutActors)
    {
        AHogeCharacter* Character = Cast<AHogeCharacter>(Actor);
        if (Character)
        {
            SaveAttributeSet(Character, SaveJsonObj);
        }
    }
    FString OutPutString;
    const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutPutString);
    FJsonSerializer::Serialize(SaveJsonObj.ToSharedRef(), Writer);
    FFileHelper::SaveStringToFile(OutPutString, *FileFullPath);
}

void UHogeSaveGame::SaveAttributeSet(AHogeCharacter* InTarget, TSharedPtr<FJsonObject>& SaveJsonObj)
{
    if (!InTarget) return;
    /* 念のため、AbilitySystemComponentを持っているかを調べる */
    if (!InTarget->GetAbilitySystemComponent()) return;

    if (!InTarget->HogeAttributeSet) return;

    UHogeAttributeSet* HogeAttributeSet = InTarget->HogeAttributeSet;
    if (!HogeAttributeSet) return;

    /* AttributeSetの中身をひとまとめにするための配列 */
    TArray<TSharedPtr<FJsonValue>> Values;
    for (TFieldIterator<FProperty> It(HogeAttributeSet->GetClass(), EFieldIteratorFlags::IncludeSuper); It; ++It)
    {
        FProperty* Property = *It;
        FStructProperty* StructProperty = CastField<FStructProperty>(Property);
        if (!StructProperty) continue;
        /* StructPropertyをHogeAttributeSetからFGameplayAttributeDataにする */
        FGameplayAttributeData* DataPtr = StructProperty->ContainerPtrToValuePtr<FGameplayAttributeData>(HogeAttributeSet);
        if (!DataPtr) continue;
        /* FGameplayAttributeDataからFJsonObject型に変換する */
        TSharedPtr<FJsonObject> Obj = FJsonObjectConverter::UStructToJsonObject<FGameplayAttributeData>(*DataPtr);
        if (!Obj) continue;

        /* プロパティに値に名前を付けるためのJsonObj */
        TSharedPtr<FJsonObject> JsonObj_ForPropertyName = MakeShareable(new FJsonObject());
        /* JsonObj_ForPropertyNameの中身は
        {
           "Health" : {
               "baseValue" : 100,
               "currentValue" : 100
           }
        }
        */
        JsonObj_ForPropertyName->SetObjectField(Property->GetName(), Obj);
        /* JsonObjectをJsonValueに変換する */
        TSharedRef<FJsonValueObject> JsonValue = MakeShareable(new FJsonValueObject(JsonObj_ForPropertyName));
        /* プロパティを配列に追加 */
        Values.Add(JsonValue);
    }
    /* CharacterIDにAttributeSetの中身を保持する */
    SaveJsonObj->SetArrayField(”CharacterID”, Values);
    }
}

これでAttributeSetの中身をJsonに保存することができたよ。

Jsonから読み込み

void UCharacterSaveGame::LoadAttributeSet(AHogeCharacter* InTarget)
{
    FString LoadJsonStringData;
    if (FFileHelper::LoadFileToString(LoadJsonStringData, *FileFullPath))
    {
        TSharedPtr<FJsonObject> LoadJsonObj = MakeShareable(new FJsonObject());
        const TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(LoadJsonStringData);
        if (FJsonSerializer::Deserialize(JsonReader, LoadJsonObj))
        {
            UHogeAttributeSet* HogeAttributeSet = InTarget->HogeAttributeSet;
            
            TArray<TSharedPtr<FJsonValue>> Values = LoadJsonObj->GetArrayField(CharacterID);
            TMap<FString, TSharedPtr<FJsonValue>> ValueWithPropertyName;
            /* CharacterIDごとにAttributeSetのプロパティをLoadJsonObjから読み込んでおく */
            for (TSharedPtr<FJsonValue>& Value : Values)
            {
                ValueWithPropertyName.Append(Value->AsObject()->Values);
            }
            for (TFieldIterator<FProperty> It(HogeAttributeSet->GetClass(), EFieldIteratorFlags::IncludeSuper); It; ++It)
            {
                FProperty* Property = *It;
                FStructProperty* StructProperty = CastField<FStructProperty>(Property);
                if (!StructProperty) continue;

                FGameplayAttributeData* DataPtr = StructProperty->ContainerPtrToValuePtr<FGameplayAttributeData>(MainStatusAttributeSet);
                if (!DataPtr) continue;

                TSharedPtr<FJsonValue>* PropertyValue = ValueWithPropertyName.Find(Property->GetName());
                if (!PropertyValue) continue;

                FGameplayAttributeData OutData;
                /* こんなデータをFGameplayAttributeDataに読み込む
               {
                   "Health" : {
                       "baseValue" : 100,
                       "currentValue" : 100
                   }
               }
                */
                TSharedPtr<FJsonObject> AttributeDataObj = (*PropertyValue)->AsObject();
                FJsonObjectConverter::JsonObjectToUStruct<FGameplayAttributeData>(AttributeDataObj.ToSharedRef(), &OutData, false, false);
                 /* HogeAttributeSetに値を設定する。 */
                DataPtr->SetBaseValue(OutData.GetBaseValue());
                DataPtr->SetCurrentValue(OutData.GetCurrentValue());
                 /* 注意 : 更新イベントは呼ばれないので手動で呼び出す必要があるよ */
            }
        }
    }
}

これでJsonからAttributeSetに値を読み込むことができるよ。

今回は終わり。
書いていて思ったんだけど、スタンドアローンならJsonに書き込む必要あんまりない?