icon안동민 개발노트

JSON, XML 파일 입출력 기초


 언리얼 엔진에서 JSON과 XML 형식의 데이터를 처리하는 것은 게임 개발에서 중요한 부분입니다.

 이 절에서는 C++를 사용하여 이러한 데이터 형식을 다루는 방법을 살펴보겠습니다.

JSON 처리

 언리얼 엔진은 JSON 처리를 위한 내장 라이브러리를 제공합니다.

 JSON 읽기

bool ReadJSONFile(const FString& FilePath, TSharedPtr<FJsonObject>& JsonObject)
{
    FString JsonString;
    if (FFileHelper::LoadFileToString(JsonString, *FilePath))
    {
        TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
        if (FJsonSerializer::Deserialize(Reader, JsonObject))
        {
            return true;
        }
    }
    return false;
}

 JSON 쓰기

bool WriteJSONFile(const FString& FilePath, TSharedPtr<FJsonObject> JsonObject)
{
    FString OutputString;
    TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
    if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer))
    {
        return FFileHelper::SaveStringToFile(OutputString, *FilePath);
    }
    return false;
}

 구조체와 JSON 변환

USTRUCT()
struct FPlayerData
{
    GENERATED_BODY()
 
    UPROPERTY()
    FString Name;
 
    UPROPERTY()
    int32 Level;
 
    UPROPERTY()
    float Health;
};
 
// 구조체를 JSON으로 변환
TSharedPtr<FJsonObject> StructToJson(const FPlayerData& PlayerData)
{
    TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
    JsonObject->SetStringField("Name", PlayerData.Name);
    JsonObject->SetNumberField("Level", PlayerData.Level);
    JsonObject->SetNumberField("Health", PlayerData.Health);
    return JsonObject;
}
 
// JSON을 구조체로 변환
FPlayerData JsonToStruct(TSharedPtr<FJsonObject> JsonObject)
{
    FPlayerData PlayerData;
    JsonObject->TryGetStringField("Name", PlayerData.Name);
    JsonObject->TryGetNumberField("Level", PlayerData.Level);
    JsonObject->TryGetNumberField("Health", PlayerData.Health);
    return PlayerData;
}

XML 처리

 XML 처리를 위해 언리얼 엔진은 FXmlFile 클래스를 제공합니다.

 XML 읽기

bool ReadXMLFile(const FString& FilePath, FXmlFile& XmlFile)
{
    return XmlFile.LoadFile(FilePath);
}

 XML 쓰기

bool WriteXMLFile(const FString& FilePath, const FXmlFile& XmlFile)
{
    return XmlFile.SaveFile(FilePath);
}

 구조체와 XML 변환

// 구조체를 XML 노드로 변환
FXmlNode* StructToXmlNode(FXmlNode* ParentNode, const FPlayerData& PlayerData)
{
    FXmlNode* PlayerNode = ParentNode->CreateChildNode("Player");
    PlayerNode->CreateChildNode("Name", PlayerData.Name);
    PlayerNode->CreateChildNode("Level", FString::FromInt(PlayerData.Level));
    PlayerNode->CreateChildNode("Health", FString::SanitizeFloat(PlayerData.Health));
    return PlayerNode;
}
 
// XML 노드를 구조체로 변환
FPlayerData XmlNodeToStruct(const FXmlNode* PlayerNode)
{
    FPlayerData PlayerData;
    PlayerData.Name = PlayerNode->FindChildNode("Name")->GetContent();
    PlayerData.Level = FCString::Atoi(*PlayerNode->FindChildNode("Level")->GetContent());
    PlayerData.Health = FCString::Atof(*PlayerNode->FindChildNode("Health")->GetContent());
    return PlayerData;
}

실제 게임 개발에서의 활용

 게임 설정

 JSON을 사용한 게임 설정 관리

class UGameSettings : public UObject
{
    GENERATED_BODY()
 
public:
    void LoadSettings();
    void SaveSettings();
 
    UPROPERTY()
    float MusicVolume;
 
    UPROPERTY()
    float SFXVolume;
 
    UPROPERTY()
    int32 GraphicsQuality;
};
 
void UGameSettings::LoadSettings()
{
    FString SettingsPath = FPaths::ProjectConfigDir() + "GameSettings.json";
    TSharedPtr<FJsonObject> JsonObject;
    if (ReadJSONFile(SettingsPath, JsonObject))
    {
        JsonObject->TryGetNumberField("MusicVolume", MusicVolume);
        JsonObject->TryGetNumberField("SFXVolume", SFXVolume);
        JsonObject->TryGetNumberField("GraphicsQuality", GraphicsQuality);
    }
}
 
void UGameSettings::SaveSettings()
{
    TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
    JsonObject->SetNumberField("MusicVolume", MusicVolume);
    JsonObject->SetNumberField("SFXVolume", SFXVolume);
    JsonObject->SetNumberField("GraphicsQuality", GraphicsQuality);
 
    FString SettingsPath = FPaths::ProjectConfigDir() + "GameSettings.json";
    WriteJSONFile(SettingsPath, JsonObject);
}

 레벨 데이터

 XML을 사용한 레벨 데이터 관리

struct FLevelData
{
    FString LevelName;
    TArray<FVector> EnemySpawnPoints;
    TArray<FString> AvailableWeapons;
};
 
FLevelData LoadLevelData(const FString& LevelName)
{
    FString LevelDataPath = FPaths::ProjectContentDir() + "LevelData/" + LevelName + ".xml";
    FXmlFile XmlFile;
    FLevelData LevelData;
 
    if (ReadXMLFile(LevelDataPath, XmlFile))
    {
        FXmlNode* RootNode = XmlFile.GetRootNode();
        LevelData.LevelName = RootNode->GetAttribute("Name");
 
        FXmlNode* SpawnPointsNode = RootNode->FindChildNode("EnemySpawnPoints");
        for (FXmlNode* SpawnPoint : SpawnPointsNode->GetChildrenNodes())
        {
            FVector SpawnLocation;
            SpawnLocation.X = FCString::Atof(*SpawnPoint->GetAttribute("X"));
            SpawnLocation.Y = FCString::Atof(*SpawnPoint->GetAttribute("Y"));
            SpawnLocation.Z = FCString::Atof(*SpawnPoint->GetAttribute("Z"));
            LevelData.EnemySpawnPoints.Add(SpawnLocation);
        }
 
        FXmlNode* WeaponsNode = RootNode->FindChildNode("AvailableWeapons");
        for (FXmlNode* Weapon : WeaponsNode->GetChildrenNodes())
        {
            LevelData.AvailableWeapons.Add(Weapon->GetContent());
        }
    }
 
    return LevelData;
}

 로컬라이제이션

 JSON을 사용한 간단한 로컬라이제이션 시스템

class ULocalizationManager : public UObject
{
    GENERATED_BODY()
 
public:
    void LoadLanguage(const FString& LanguageCode);
    FString GetLocalizedString(const FString& Key);
 
private:
    TMap<FString, FString> LocalizedStrings;
};
 
void ULocalizationManager::LoadLanguage(const FString& LanguageCode)
{
    FString LanguagePath = FPaths::ProjectContentDir() + "Localization/" + LanguageCode + ".json";
    TSharedPtr<FJsonObject> JsonObject;
    if (ReadJSONFile(LanguagePath, JsonObject))
    {
        for (auto& Elem : JsonObject->Values)
        {
            LocalizedStrings.Add(Elem.Key, Elem.Value->AsString());
        }
    }
}
 
FString ULocalizationManager::GetLocalizedString(const FString& Key)
{
    return LocalizedStrings.FindRef(Key);
}

성능 최적화 전략

  1. 큰 파일의 경우 청크 단위로 읽기 / 쓰기
  2. 자주 접근하는 데이터는 메모리에 캐시
  3. 데이터 구조를 최적화하여 파싱 시간 단축

비동기 파일 입출력

void AsyncLoadJSON(const FString& FilePath, TFunction<void(TSharedPtr<FJsonObject>)> Callback)
{
    AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, [FilePath, Callback]()
    {
        TSharedPtr<FJsonObject> JsonObject;
        if (ReadJSONFile(FilePath, JsonObject))
        {
            AsyncTask(ENamedThreads::GameThread, [JsonObject, Callback]()
            {
                Callback(JsonObject);
            });
        }
    });
}

데이터 무결성 보장

  1. 체크섬 사용
  2. 버전 관리
  3. 백업 및 복원 메커니즘 구현
bool VerifyJSONIntegrity(const FString& FilePath)
{
    TSharedPtr<FJsonObject> JsonObject;
    if (ReadJSONFile(FilePath, JsonObject))
    {
        FString StoredChecksum;
        if (JsonObject->TryGetStringField("Checksum", StoredChecksum))
        {
            JsonObject->RemoveField("Checksum");
            FString JsonString;
            TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonString);
            FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
 
            FString CalculatedChecksum = FMD5::HashAnsiString(*JsonString);
            return StoredChecksum == CalculatedChecksum;
        }
    }
    return false;
}

JSON vs XML: 장단점 및 사용 시나리오

 JSON

 장점

  • 간결하고 읽기 쉬움
  • 파싱 속도가 빠름
  • JavaScript와의 호환성이 좋음

 단점

  • 주석을 지원하지 않음
  • 스키마 검증이 XML에 비해 덜 엄격함

 사용 시나리오

  • 웹 API와의 데이터 교환
  • 간단한 설정 파일
  • 클라이언트-서버 통신

 XML

 장점

  • 풍부한 메타데이터 지원
  • 엄격한 스키마 검증 가능
  • 복잡한 데이터 구조 표현에 유리

 단점

  • JSON에 비해 파일 크기가 큼
  • 파싱이 상대적으로 느림

 사용 시나리오

  • 복잡한 문서 구조가 필요한 경우
  • 엄격한 데이터 유효성 검사가 필요한 경우
  • 레거시 시스템과의 호환성이 필요한 경우

 JSON과 XML은 각각의 장단점이 있으며, 프로젝트의 요구사항에 따라 적절한 형식을 선택해야 합니다. 일반적으로 간단하고 빠른 데이터 교환이 필요한 경우 JSON을, 복잡한 데이터 구조와 엄격한 검증이 필요한 경우 XML을 선택하는 것이 좋습니다. 언리얼 엔진에서는 두 형식 모두 지원하므로, 개발자는 프로젝트의 특성에 맞는 최적의 선택을 할 수 있습니다.