icon안동민 개발노트

세이브 게임 시스템 구축


 언리얼 엔진에서 견고한 세이브 게임 시스템을 구현하는 것은 게임 개발의 중요한 부분입니다.

 이 절에서는 C++를 사용하여 세이브 게임 시스템을 구축하는 방법을 살펴보겠습니다.

USaveGame를 활용한 세이브 시스템

 USaveGame 클래스는 언리얼 엔진에서 제공하는 기본적인 세이브 게임 기능을 위한 클래스입니다.

UCLASS()
class MYGAME_API UMySaveGame : public USaveGame
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    FString PlayerName;
 
    UPROPERTY()
    int32 PlayerLevel;
 
    UPROPERTY()
    FVector PlayerLocation;
};
 
// 세이브 게임 저장
UMySaveGame* SaveGameObject = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
SaveGameObject->PlayerName = "John Doe";
SaveGameObject->PlayerLevel = 10;
SaveGameObject->PlayerLocation = FVector(100.0f, 200.0f, 300.0f);
 
if (UGameplayStatics::SaveGameToSlot(SaveGameObject, "SaveSlot1", 0))
{
    UE_LOG(LogTemp, Log, TEXT("Game Saved Successfully"));
}
 
// 세이브 게임 로드
UMySaveGame* LoadedGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot("SaveSlot1", 0));
if (LoadedGame)
{
    UE_LOG(LogTemp, Log, TEXT("Loaded Player Name: %s"), *LoadedGame->PlayerName);
}

복잡한 게임 상태 저장 및 로드

 더 복잡한 게임 상태를 저장하기 위해서 구조체와 배열을 활용할 수 있습니다.

USTRUCT()
struct FInventoryItem
{
    GENERATED_BODY()
 
    UPROPERTY()
    FString ItemName;
 
    UPROPERTY()
    int32 Quantity;
};
 
UCLASS()
class MYGAME_API UComplexSaveGame : public USaveGame
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    TArray<FInventoryItem> Inventory;
 
    UPROPERTY()
    TMap<FString, float> PlayerStats;
};
 
// 복잡한 데이터 저장
UComplexSaveGame* ComplexSave = Cast<UComplexSaveGame>(UGameplayStatics::CreateSaveGameObject(UComplexSaveGame::StaticClass()));
ComplexSave->Inventory.Add(FInventoryItem{"Sword", 1});
ComplexSave->Inventory.Add(FInventoryItem{"Potion", 5});
ComplexSave->PlayerStats.Add("Strength", 10.0f);
ComplexSave->PlayerStats.Add("Agility", 15.0f);
 
UGameplayStatics::SaveGameToSlot(ComplexSave, "ComplexSave", 0);
 
// 복잡한 데이터 로드
UComplexSaveGame* LoadedComplexSave = Cast<UComplexSaveGame>(UGameplayStatics::LoadGameFromSlot("ComplexSave", 0));
if (LoadedComplexSave)
{
    for (const FInventoryItem& Item : LoadedComplexSave->Inventory)
    {
        UE_LOG(LogTemp, Log, TEXT("Item: %s, Quantity: %d"), *Item.ItemName, Item.Quantity);
    }
}

비동기 세이브/로드 처리

 대용량 세이브 데이터를 처리할 때는 비동기 저장/로드를 사용하여 게임 성능에 미치는 영향을 최소화할 수 있습니다.

void AMyGameMode::AsyncSaveGame()
{
    UMySaveGame* SaveGameObject = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
    // SaveGameObject 설정...
 
    FAsyncSaveGameToSlotDelegate SavedDelegate;
    SavedDelegate.BindUObject(this, &AMyGameMode::OnGameSaved);
    UGameplayStatics::AsyncSaveGameToSlot(SaveGameObject, "AsyncSave", 0, SavedDelegate);
}
 
void AMyGameMode::OnGameSaved(const FString& SlotName, const int32 UserIndex, bool bSuccess)
{
    if (bSuccess)
    {
        UE_LOG(LogTemp, Log, TEXT("Game Saved Asynchronously"));
    }
}
 
void AMyGameMode::AsyncLoadGame()
{
    FAsyncLoadGameFromSlotDelegate LoadedDelegate;
    LoadedDelegate.BindUObject(this, &AMyGameMode::OnGameLoaded);
    UGameplayStatics::AsyncLoadGameFromSlot("AsyncSave", 0, LoadedDelegate);
}
 
void AMyGameMode::OnGameLoaded(const FString& SlotName, const int32 UserIndex, USaveGame* LoadedSaveGame)
{
    if (LoadedSaveGame)
    {
        UMySaveGame* LoadedGame = Cast<UMySaveGame>(LoadedSaveGame);
        // LoadedGame 사용...
    }
}

여러 세이브 슬롯 관리

 여러 세이브 슬롯을 관리하기 위해 슬롯 정보를 저장하고 관리하는 시스템을 구현할 수 있습니다.

UCLASS()
class MYGAME_API USaveSlotInfo : public UObject
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    FString SlotName;
 
    UPROPERTY()
    FDateTime SaveTime;
 
    UPROPERTY()
    FString PlayerName;
 
    UPROPERTY()
    int32 PlayerLevel;
};
 
UCLASS()
class MYGAME_API USaveManager : public UObject
{
    GENERATED_BODY()
 
public:
    TArray<USaveSlotInfo*> GetAllSaveSlots();
    void CreateNewSaveSlot(const FString& SlotName);
    void DeleteSaveSlot(const FString& SlotName);
};
 
TArray<USaveSlotInfo*> USaveManager::GetAllSaveSlots()
{
    TArray<USaveSlotInfo*> SaveSlots;
    TArray<FString> SlotNames;
    TArray<int32> UserIndexes;
    UGameplayStatics::GetSaveGameSlotNames(SlotNames, UserIndexes);
 
    for (const FString& SlotName : SlotNames)
    {
        UMySaveGame* SaveGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
        if (SaveGame)
        {
            USaveSlotInfo* SlotInfo = NewObject<USaveSlotInfo>();
            SlotInfo->SlotName = SlotName;
            SlotInfo->SaveTime = SaveGame->SaveTime;
            SlotInfo->PlayerName = SaveGame->PlayerName;
            SlotInfo->PlayerLevel = SaveGame->PlayerLevel;
            SaveSlots.Add(SlotInfo);
        }
    }
 
    return SaveSlots;
}

자동 저장 기능 구현

 자동 저장 기능은 타이머를 사용하여 구현할 수 있습니다.

UCLASS()
class MYGAME_API AAutoSaveGameMode : public AGameModeBase
{
    GENERATED_BODY()
 
public:
    AAutoSaveGameMode();
 
    virtual void BeginPlay() override;
 
private:
    FTimerHandle AutoSaveTimerHandle;
    
    UFUNCTION()
    void PerformAutoSave();
};
 
AAutoSaveGameMode::AAutoSaveGameMode()
{
    PrimaryActorTick.bCanEverTick = true;
}
 
void AAutoSaveGameMode::BeginPlay()
{
    Super::BeginPlay();
    
    // 5분마다 자동 저장
    GetWorldTimerManager().SetTimer(AutoSaveTimerHandle, this, &AAutoSaveGameMode::PerformAutoSave, 300.0f, true);
}
 
void AAutoSaveGameMode::PerformAutoSave()
{
    // 세이브 게임 로직 구현
    UE_LOG(LogTemp, Log, TEXT("Auto Save Performed"));
}

세이브 데이터 암호화 및 압축

 세이브 데이터의 보안을 강화하기 위해 암호화와 압축을 적용할 수 있습니다.

UCLASS()
class MYGAME_API UEncryptedSaveGame : public USaveGame
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    TArray<uint8> EncryptedData;
 
    void EncryptData(const TArray<uint8>& RawData, const FString& EncryptionKey);
    TArray<uint8> DecryptData(const FString& EncryptionKey);
};
 
void UEncryptedSaveGame::EncryptData(const TArray<uint8>& RawData, const FString& EncryptionKey)
{
    // 실제 암호화 로직 구현 (예: AES 암호화)
    // EncryptedData에 암호화된 데이터 저장
}
 
TArray<uint8> UEncryptedSaveGame::DecryptData(const FString& EncryptionKey)
{
    // 실제 복호화 로직 구현
    // 복호화된 데이터 반환
}

세이브 데이터 버전 관리

 게임 업데이트로 인한 세이브 데이터 구조 변경을 관리하기 위해 버전 관리 시스템을 구현할 수 있습니다.

UCLASS()
class MYGAME_API UVersionedSaveGame : public USaveGame
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    int32 SaveVersion;
 
    virtual void Serialize(FArchive& Ar) override
    {
        Super::Serialize(Ar);
 
        Ar << SaveVersion;
 
        if (SaveVersion >= 1)
        {
            // Version 1 데이터 직렬화
        }
 
        if (SaveVersion >= 2)
        {
            // Version 2에서 추가된 데이터 직렬화
        }
 
        // 최신 버전으로 업데이트
        SaveVersion = 2;
    }
};

클라우드 저장소 연동

 온라인 서브시스템을 활용하여 클라우드 저장소와 연동할 수 있습니다.

void AMyGameMode::SaveToCloud(const FString& SlotName)
{
    IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
    if (OnlineSub)
    {
        IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface();
        if (IdentityInterface.IsValid())
        {
            TSharedPtr<const FUniqueNetId> UserId = IdentityInterface->GetUniquePlayerId(0);
            if (UserId.IsValid())
            {
                UMySaveGame* SaveGameObject = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
                // SaveGameObject 설정...
 
                TArray<uint8> SaveData;
                FMemoryWriter MemoryWriter(SaveData, true);
                SaveGameObject->Serialize(MemoryWriter);
 
                IOnlineStoragePtr StorageInterface = OnlineSub->GetStorageInterface();
                if (StorageInterface.IsValid())
                {
                    StorageInterface->UploadToCloud(*SlotName, SaveData, *UserId);
                }
            }
        }
    }
}

보안 고려사항

  1. 체크섬 검증 : 세이브 데이터의 무결성을 보장하기 위해 체크섬을 사용합니다.
  2. 안티치트 시스템 연동 : 세이브 데이터 조작을 방지하기 위해 안티치트 시스템과 연동합니다.
  3. 서버 측 검증 : 온라인 게임의 경우, 중요한 데이터는 서버에서 검증합니다.

성능 최적화 전략

  1. 증분 저장 : 변경된 데이터만 저장하여 저장 시간과 용량을 줄입니다.
  2. 데이터 압축 : 대용량 데이터의 경우 압축 알고리즘을 사용하여 저장 공간을 절약합니다.
  3. 비동기 처리 : 세이브/로드 작업을 백그라운드 스레드에서 처리합니다.

오픈 월드 게임에서의 세이브 데이터 관리

  1. 청크 기반 저장 : 월드를 청크로 나누어 필요한 부분만 로드/저장합니다.
USTRUCT()
struct FChunkData
{
    GENERATED_BODY()
 
    UPROPERTY()
    TArray<FActorData> ActorsInChunk;
 
    UPROPERTY()
    TArray<FItemData> ItemsInChunk;
};
 
UCLASS()
class MYGAME_API UOpenWorldSaveGame : public USaveGame
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    TMap<FIntVector, FChunkData> WorldChunks;
 
    void SaveChunk(const FIntVector& ChunkCoord, const FChunkData& ChunkData)
    {
        WorldChunks.Add(ChunkCoord, ChunkData);
    }
 
    FChunkData LoadChunk(const FIntVector& ChunkCoord)
    {
        return WorldChunks.FindRef(ChunkCoord);
    }
};
 
// 게임 플레이 중 청크 저장
void AMyOpenWorldGameMode::SaveCurrentChunk()
{
    FIntVector CurrentChunkCoord = GetCurrentChunkCoordinate();
    FChunkData ChunkData;
    // 현재 청크의 액터와 아이템 데이터를 수집하여 ChunkData에 저장
 
    UOpenWorldSaveGame* SaveGame = Cast<UOpenWorldSaveGame>(UGameplayStatics::LoadGameFromSlot("OpenWorldSave", 0));
    if (!SaveGame)
    {
        SaveGame = Cast<UOpenWorldSaveGame>(UGameplayStatics::CreateSaveGameObject(UOpenWorldSaveGame::StaticClass()));
    }
 
    SaveGame->SaveChunk(CurrentChunkCoord, ChunkData);
    UGameplayStatics::SaveGameToSlot(SaveGame, "OpenWorldSave", 0);
}
  1. 스트리밍 세이브 : 플레이어 주변 데이터만 메모리에 유지하고 나머지는 디스크에서 스트리밍합니다.
UCLASS()
class MYGAME_API AStreamingSaveManager : public AActor
{
    GENERATED_BODY()
 
public:
    AStreamingSaveManager();
 
    UFUNCTION(BlueprintCallable)
    void UpdateLoadedChunks(const FVector& PlayerLocation);
 
private:
    UPROPERTY()
    TMap<FIntVector, FChunkData> LoadedChunks;
 
    UPROPERTY(EditAnywhere)
    float ChunkLoadDistance = 1000.0f;
 
    void LoadChunkFromDisk(const FIntVector& ChunkCoord);
    void UnloadChunk(const FIntVector& ChunkCoord);
};
 
void AStreamingSaveManager::UpdateLoadedChunks(const FVector& PlayerLocation)
{
    FIntVector PlayerChunkCoord = GetChunkCoordinate(PlayerLocation);
 
    // 로드해야 할 청크 결정
    TArray<FIntVector> ChunksToLoad;
    for (int32 X = -1; X <= 1; X++)
    {
        for (int32 Y = -1; Y <= 1; Y++)
        {
            ChunksToLoad.Add(PlayerChunkCoord + FIntVector(X, Y, 0));
        }
    }
 
    // 불필요한 청크 언로드
    TArray<FIntVector> ChunksToUnload;
    for (auto& Elem : LoadedChunks)
    {
        if (!ChunksToLoad.Contains(Elem.Key))
        {
            ChunksToUnload.Add(Elem.Key);
        }
    }
 
    // 청크 로드 및 언로드 수행
    for (const FIntVector& ChunkCoord : ChunksToLoad)
    {
        if (!LoadedChunks.Contains(ChunkCoord))
        {
            LoadChunkFromDisk(ChunkCoord);
        }
    }
 
    for (const FIntVector& ChunkCoord : ChunksToUnload)
    {
        UnloadChunk(ChunkCoord);
    }
}
 
void AStreamingSaveManager::LoadChunkFromDisk(const FIntVector& ChunkCoord)
{
    // 디스크에서 청크 데이터 로드
    UOpenWorldSaveGame* SaveGame = Cast<UOpenWorldSaveGame>(UGameplayStatics::LoadGameFromSlot("OpenWorldSave", 0));
    if (SaveGame)
    {
        FChunkData ChunkData = SaveGame->LoadChunk(ChunkCoord);
        LoadedChunks.Add(ChunkCoord, ChunkData);
        // 청크 데이터를 월드에 스폰
    }
}
 
void AStreamingSaveManager::UnloadChunk(const FIntVector& ChunkCoord)
{
    // 청크 데이터를 메모리에서 제거하고 필요시 디스크에 저장
    if (FChunkData* ChunkData = LoadedChunks.Find(ChunkCoord))
    {
        // 청크 내 액터들을 제거하거나 비활성화
        LoadedChunks.Remove(ChunkCoord);
    }
}
  1. 우선순위 기반 저장 : 중요한 데이터를 먼저 저장하고 덜 중요한 데이터는 나중에 저장합니다.
UENUM()
enum class ESaveDataPriority : uint8
{
    Critical,
    High,
    Medium,
    Low
};
 
USTRUCT()
struct FSaveDataItem
{
    GENERATED_BODY()
 
    UPROPERTY()
    ESaveDataPriority Priority;
 
    UPROPERTY()
    TArray<uint8> Data;
};
 
UCLASS()
class MYGAME_API UPrioritySaveGame : public USaveGame
{
    GENERATED_BODY()
 
public:
    UPROPERTY()
    TArray<FSaveDataItem> SaveItems;
 
    void AddSaveData(const TArray<uint8>& Data, ESaveDataPriority Priority);
    void SaveGameWithPriority();
};
 
void UPrioritySaveGame::AddSaveData(const TArray<uint8>& Data, ESaveDataPriority Priority)
{
    FSaveDataItem NewItem;
    NewItem.Priority = Priority;
    NewItem.Data = Data;
    SaveItems.Add(NewItem);
}
 
void UPrioritySaveGame::SaveGameWithPriority()
{
    // 우선순위에 따라 정렬
    SaveItems.Sort([](const FSaveDataItem& A, const FSaveDataItem& B) {
        return static_cast<uint8>(A.Priority) < static_cast<uint8>(B.Priority);
    });
 
    // 우선순위 순으로 저장
    for (const FSaveDataItem& Item : SaveItems)
    {
        // 각 아이템 저장 로직
        // 고우선순위 아이템은 즉시 디스크에 쓰고, 저우선순위 아이템은 버퍼링 후 일괄 저장 가능
    }
}

 이러한 기법들을 조합하여 사용하면 대규모 오픈 월드 게임에서도 효율적인 세이브 시스템을 구현할 수 있습니다.

 청크 기반 저장은 필요한 데이터만 메모리에 로드하여 메모리 사용량을 줄이고 스트리밍 세이브는 플레이어 주변의 데이터만 실시간으로 관리하여 성능을 최적화합니다.

 우선순위 기반 저장은 중요한 데이터의 안전한 저장을 보장하면서도 전체 저장 프로세스의 효율성을 높입니다.

 이러한 시스템을 구현할 때는 다음 사항들을 고려해야 합니다.

  1.  멀티스레딩 : 청크 로딩/언로딩 및 저장 작업을 별도의 스레드에서 수행하여 메인 게임 스레드의 성능 영향을 최소화합니다.

  2.  캐싱 : 자주 접근하는 청크나 데이터는 메모리에 캐시하여 디스크 I/O를 줄입니다.

  3.  압축 : 청크 데이터를 압축하여 저장 공간을 절약하고 로딩/저장 시간을 단축합니다.

  4.  델타 압축 : 이전 저장 상태와의 차이만 저장하여 저장 크기와 시간을 더욱 줄입니다.

  5.  비동기 저장 : 게임 플레이에 영향을 주지 않도록 백그라운드에서 저장 작업을 수행합니다.