Unreal Engine/Functional Implementation

언리얼 엔진 SaveGame 저장 시스템 구현하기 - GameInstance, 체크포인트, 자동 저장

보별 2026. 1. 29. 17:34

언리얼 엔진에서 Save 시스템은 진행 중인 게임 데이터를 저장하고, 다시 같은 상태로 복구하기 위한 핵심 기능이다.
특히 플레이어의 위치만 저장하는 수준이 아니라, 현재 진행 상황과 장비 상태까지 함께 복구해야 실제 플레이 흐름이 자연스럽게 이어진다.

이번에는 GameInstance를 활용한 중앙 집중식 데이터 관리, 자동 저장과 수동 슬롯 저장 구조, 그리고 무기별 탄약 상태까지 복구하는 Save 시스템을 직접 구현한 과정을 정리해보려고 한다.

내가 구성한 전체 흐름은 다음과 같다.

  • SaveGame: 실제 저장될 데이터 보관
  • GameInstance: 데이터 캐싱, 저장/로드 실행, 자동 저장 및 수동 슬롯 관리
  • Checkpoint: 플레이어가 닿았을 때 현재 상태를 수집해 자동 저장 요청

이번 시스템에서 저장 대상으로 잡은 데이터는 아래와 같다.

  • 플레이어 위치와 회전값
  • 현재 레벨 이름
  • Score 값
  • 획득한 무기 업그레이드 정보
  • 무기별 장전된 탄약 상태

참고로 이 글에서는 Score를 단순 점수가 아니라, 현재 플레이 상태를 나타내는 수치로 함께 다루고 있다.

 

1. SaveGame에 저장할 데이터 정의

가장 먼저 USaveGame을 상속받은 클래스를 만들고, 실제로 디스크에 저장할 데이터를 정리했다.

체크포인트 복구를 위해 플레이어의 위치, 회전값, 현재 레벨명을 구조체로 묶었고, 그 외에 점수 값과 무기 업그레이드 정보, 무기별 탄약 상태까지 함께 저장하도록 구성했다.

특히 탄약 데이터는 단순 배열이 아니라 TMap<EWeaponType, int32> 형태로 관리했다.
이렇게 해두면 무기 종류가 늘어나더라도 저장 구조를 크게 바꾸지 않아도 되기 때문에 확장성이 좋다.

MyGameSaveGame.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#pragma once
 
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MyGame/DataAssets/WeaponUpgradeData.h"
#include "MyGameSaveGame.generated.h"
 
/**
 * 플레이어의 체크포인트 상태를 나타내는 구조체
 */
USTRUCT(BlueprintType)
struct FPlayerCheckpointData
{
    GENERATED_BODY()
 
    UPROPERTY(BlueprintReadWrite)
    FVector Location = FVector::ZeroVector;
 
    UPROPERTY(BlueprintReadWrite)
    FRotator Rotation = FRotator::ZeroRotator;
 
    UPROPERTY(BlueprintReadWrite)
    FName CurrentLevelName;
};
 
/**
 * 게임 진행 상황을 저장하는 클래스
 */
UCLASS()
class MYGAME_API UMyGameSaveGame : public USaveGame
{
    GENERATED_BODY()
 
public:
    UPROPERTY(BlueprintReadWrite, Category = "SaveData")
    FString SaveSlotName;
 
    UPROPERTY(BlueprintReadWrite, Category = "SaveData")
    int32 UserIndex = 0;
 
    UPROPERTY(BlueprintReadWrite, Category = "SaveData")
    FDateTime SaveDate;
 
    /** 체크포인트 데이터 */
    UPROPERTY(BlueprintReadWrite, Category = "SaveData")
    FPlayerCheckpointData CheckpointData;
 
    /** 현재 점수 */
    UPROPERTY(BlueprintReadWrite, Category = "SaveData")
    int32 Score = 0;
 
    /** 획득한 무기 업그레이드 목록 */
    UPROPERTY(BlueprintReadWrite, Category = "SaveData")
    TArray<FWeaponUpgrade> AcquiredUpgrades;
 
    /** 각 무기 타입별 장전된 탄약 수 */
    UPROPERTY(BlueprintReadWrite, Category = "SaveData")
    TMap<EWeaponType, int32> WeaponAmmoStates;
};
cs

 

2. 무기 탄약 상태 저장 및 복구

다음으로 총기 정보를 관리하는 컴포넌트에서, 현재 소지 중인 모든 무기의 탄약 상태를 한 번에 가져오거나 저장된 데이터를 기준으로 복구할 수 있도록 함수를 분리했다.

저장 시에는 현재 인벤토리에 있는 무기들의 장탄 수를 TMap으로 변환해서 넘기고, 복구 시에는 저장된 맵을 순회 하면서 각 무기의 탄약 값을 다시 적용하도록 만들었다.

또한 현재 손에 들고 있는 무기라면 단순히 내부 값만 바꾸는 것이 아니라, 즉시 UI까지 갱신되도록 처리했다.
이 부분을 넣지 않으면 실제 데이터는 복구되더라도 화면에는 이전 탄약 값이 남아 있는 문제가 생길 수 있다.

MyGame_GunInfoComponent.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/** 모든 무기의 장전된 탄약 수를 맵으로 반환 */
TMap<EWeaponType, int32> UMyGame_GunInfoComponent::GetAllWeaponAmmoStates() const
{
    TMap<EWeaponType, int32> OutMap;
 
    // CurrentWeaponStats: 플레이어가 소지한 무기 목록
    for (const auto& Elem : CurrentWeaponStats)
    {
        OutMap.Add(Elem.Key, Elem.Value.CurrentAmmo);
    }
    return OutMap;
}
 
/** 저장된 탄약 맵을 받아 복구 */
void UMyGame_GunInfoComponent::RestoreAllWeaponAmmoStates(const TMap<EWeaponType, int32>& SavedAmmoMap)
{
    for (const auto& Elem : SavedAmmoMap)
    {
        EWeaponType Type = Elem.Key;
        int32 SavedAmmo = Elem.Value;
 
        // 인벤토리에 해당 무기가 있다면 탄약 수 복구
        if (FGunInfo* Info = CurrentWeaponStats.Find(Type))
        {
            Info->CurrentAmmo = SavedAmmo;
 
            // 현재 들고 있는 무기라면 즉시 반영 및 UI 갱신
            if (GunInfo.WeaponType == Type)
            {
                GunInfo.CurrentAmmo = SavedAmmo;
                BroadcastGunInfo(); 
            }
        }
    }
}
cs

 

3. GameInstance를 활용한 중앙 집중식 저장 관리

이번 Save 시스템의 핵심은 GameInstance를 중심으로 데이터를 관리한 것이다.

SaveGame은 말 그대로 저장 파일에 기록될 데이터를 담는 역할에 가깝고, 실제 게임 흐름에서 언제 저장할지, 어떤 슬롯에 저장할지, 최근 저장 데이터를 어떻게 유지할지는 별도의 관리 지점이 필요했다.

그래서 GameInstance에서 아래 역할을 담당하도록 구성했다.

  • 슬롯 이름 상수 관리
  • 최근 저장 슬롯 판별
  • 메모리 캐시 유지
  • 저장/로드 함수 실행
  • 자동 저장과 수동 저장 분리

특히 탄약 상태처럼 자주 참조될 수 있는 데이터는 CachedAmmoMap에 캐싱해 두고, 로드 이후 플레이어 복구 시점에 다시 꺼내 쓸 수 있도록 했다.

MyGameGameInstance.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public:
    // 슬롯 이름 상수 정의
    const FString SLOT_AUTO = TEXT("SaveSlot_Auto"); // 자동 저장
    const FString SLOT_1 = TEXT("SaveSlot_1"); // 수동 슬롯 1
    const FString SLOT_2 = TEXT("SaveSlot_2");
    const FString SLOT_3 = TEXT("SaveSlot_3");
 
    /** 가장 최근 플레이한 슬롯 이름 반환; 이어하기 기능용 */
    UFUNCTION(BlueprintCallable, Category = "SaveSystem")
    FString GetLatestSaveSlotName();
 
    /** 저장된 탄약 맵을 반환하는 Getter 함수 */
    UFUNCTION(BlueprintPure, Category = "SaveSystem")
    TMap<EWeaponType, int32> GetCachedAmmoMap() const { return CachedAmmoMap; }
 
protected:
    /** 메모리 캐시용 변수 */
    UPROPERTY(Transient)
    TMap<EWeaponType, int32> CachedAmmoMap;
cs

저장 함수에서는 체크포인트 데이터, 점수 값, 탄약 맵을 인자로 받아 메모리 캐시를 먼저 갱신한 뒤 SaveGame 객체에 복사하도록 했다.
로드 함수에서는 디스크에서 저장 파일을 읽어온 뒤, 다시 GameInstance 내부 캐시에 반영하도록 구성했다.

이렇게 하면 체크포인트 저장 직후 사망했을 때, 혹은 레벨을 다시 열었을 때도 동일한 데이터를 기준으로 복구를 진행할 수 있다.

MyGameGameInstance.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/** 체크포인트 저장 함수; 탄약 맵(AmmoMap)을 인자로 받아 저장 */
void UMyGameGameInstance::SaveCheckpoint(const FPlayerCheckpointData& NewData, int32 CurrentScore, const TMap<EWeaponType, int32>& AmmoMap, const FString& SlotName)
{
    // 메모리 캐시 업데이트
    this->CachedAmmoMap = AmmoMap; // 탄약 상태 캐싱
    this->bHasValidCheckpoint = true;
    
    // SaveGame 객체 생성 및 비동기 저장
    if (UMyGameSaveGame* SaveObj = Cast<UMyGameSaveGame>(...))
    {
        // ... 기존 데이터 저장 ...
        SaveObj->WeaponAmmoStates = this->CachedAmmoMap; // SaveGame에 전달
        
        // ... AsyncSaveGameToSlot 호출 ...
    }
}
 
/** 디스크 로드 함수 */
void UMyGameGameInstance::LoadGameFromSlot(const FString& SlotName)
{
    // ...
    if (LoadedData)
    {
        // ... 기존 데이터 복구 ...
        this->CachedAmmoMap = LoadedData->WeaponAmmoStates; // 탄약 상태 메모리로 복구
        // ... 레벨 열기 ...
    }
}
cs

 

4. 체크포인트 액터에서 자동 저장 처리

자동 저장은 플레이어가 특정 지점에 도달했을 때 자연스럽게 발생해야 한다고 생각했다.
그래서 체크포인트 액터를 만들고, 플레이어가 오버랩되면 현재 상태를 수집해서 GameInstance에 전달하도록 구현했다.

여기서 수집하는 값은 다음과 같다.

  • 현재 위치
  • 현재 회전값
  • 현재 레벨 이름
  • 현재 Score 값
  • 현재 보유 무기 탄약 상태

그리고 이 저장은 항상 자동 저장 슬롯에 기록되도록 고정했다.
즉, 체크포인트는 플레이 도중의 진행 상황을 안전하게 이어가기 위한 장치이고, 별도의 수동 저장 슬롯은 사용자가 원하는 시점 저장용으로 분리하는 구조다.

또한 부활 직후 짧은 시간 안에 다시 체크포인트가 중복 발동하는 문제를 막기 위해, 월드 시간 기준으로 예외 처리도 추가했다.

그리고 Player를 인식했을 때 자동으로 저장하도록 하는 체크포인트 액터를 만들어주었다.
Player가 겹쳤을 때 Player가 소지한 Data를 수집하여 GameInstance에 넘기는데, 이 때는 무조건 자동 저장 슬롯(SLOT_AUTO)을 사용하도록 해주었다.

MyGameCheckpoint.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void ARichPaPaCheckpoint::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
    bool bFromSweep, const FHitResult& SweepResult)
{
    if (bHasTriggered && bTriggerOnce) return;
 
    ARichPaPa_Player* PlayerCharacter = Cast<ARichPaPa_Player>(OtherActor);
    if (!PlayerCharacter) return;
 
    // 부활 직후 중복 발동 방지
    if (GetWorld()->GetTimeSeconds() < 0.5f) return;
 
    URichPaPaGameInstance* GI = Cast<URichPaPaGameInstance>(GetGameInstance());
    if (GI)
    {
        // 저장 당시 Player의 위치 정보 수집
        FPlayerCheckpointData NewData;
        NewData.Location = PlayerCharacter->GetActorLocation();
        NewData.Rotation = PlayerCharacter->GetActorRotation();
        NewData.CurrentLevelName = FName(*UGameplayStatics::GetCurrentLevelName(this));
 
        // 저장 당시 Score 수집
        int32 RealTimeScore = 0;
 
        if (ARichPaPa_GameModeBase* GM = Cast<ARichPaPa_GameModeBase>(GetWorld()->GetAuthGameMode()))
        {
            RealTimeScore = GM->GetScore();
        }
        else
        {
            RealTimeScore = GI->GetSavedScore();
        }
 
        // Player로부터 현재 탄약 상태 수집
        TMap<EWeaponType, int32> CurrentAmmoMap;
        if (PlayerCharacter->GunInfoComp)
        {
            CurrentAmmoMap = PlayerCharacter->GunInfoComp->GetAllWeaponAmmoStates();
        }
 
        // 자동 저장 슬롯에 
        GI->SaveCheckpoint(NewData, RealTimeScore, CurrentAmmoMap, GI->SLOT_AUTO);
    }
}
cs

 

5. 사망 후 부활 또는 로드 시 플레이어 상태 복구

저장만 잘 되어 있어도, 실제 게임에서는 복구 과정이 정확해야 의미가 있다.

그래서 플레이어가 죽은 뒤 부활할 때, 혹은 레벨이 다시 로드되었을 때 호출되는 복구 함수를 만들고, GameInstance에 캐싱된 데이터를 기준으로 상태를 되돌리도록 했다.

복구 순서는 아래와 같다.

  1. 유효한 체크포인트가 있는지 확인
  2. 현재 레벨과 저장된 레벨이 같은지 검사
  3. 위치와 회전값 복구
  4. 컨트롤러 회전값 동기화
  5. Score 복구
  6. 무기별 탄약 상태 복구

위치 이동에는 TeleportPhysics를 사용해서, 복구 순간에 불필요한 물리 충돌 문제가 생기지 않도록 했다.
또한 회전값만 바꾸는 것이 아니라 컨트롤러 회전도 함께 맞춰줘야 실제 조작 방향이 어긋나지 않는다.

마지막으로 Player가 죽은 뒤 부활하거나 레벨이 로드될 때 호출되는 복구 함수를 추가하였다.
GameInstance의 Getter를 통해 저장한 정보를 가져와 적용하도록 하였다.

MyGame_Player.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/** Checkpoint로부터 플레이어 상태 복원 */
void ARichPaPa_Player::RestoreStateFromCheckpoint()
{
    URichPaPaGameInstance* GI = Cast<URichPaPaGameInstance>(GetGameInstance());
    if (!GI) return;
 
    if (GI->HasValidCheckpoint())
    {
        FPlayerCheckpointData CheckpointData;
 
        if (GI->LoadCheckpointFromMemory(CheckpointData))
        {
            // 현재 레벨과 체크포인트 레벨이 같은지 확인
            FName CurrentLevelName = FName(*UGameplayStatics::GetCurrentLevelName(this));
 
            if (CheckpointData.CurrentLevelName == CurrentLevelName)
            {
                // 위치 및 회전 복구; 텔레포트 타입을 사용하여 물리 충돌 문제 방지
                SetActorLocation(CheckpointData.Location, false, nullptr, ETeleportType::TeleportPhysics);
                SetActorRotation(CheckpointData.Rotation, ETeleportType::TeleportPhysics);
 
                if (GetController())
                {
                    GetController()->SetControlRotation(CheckpointData.Rotation);
                }
 
                // Score 복구
                int32 RestoredScore = GI->GetSavedScore();
 
                if (ARichPaPa_GameModeBase* GM = Cast<ARichPaPa_GameModeBase>(GetWorld()->GetAuthGameMode()))
                {
                    GM->SetScore(RestoredScore);
                }
 
                // 무기 별 장탄수 복구
                if (GunInfoComp)
                {                   
                    GunInfoComp->RestoreAllWeaponAmmoStates(GI->GetCachedAmmoMap());
                }
            }
        }
    }
}
cs

 

이번에 구현한 Save 시스템은 단순히 플레이어 위치만 저장하는 수준이 아니라, 현재 진행 상태를 다시 이어서 플레이할 수 있도록 만드는 구조에 초점을 맞췄다.

정리하면 다음과 같다.

  • SaveGame에는 실제 저장할 데이터를 정리
  • GameInstance에서는 저장과 로드 흐름을 중앙 관리
  • Checkpoint에서는 플레이어 상태를 수집해 자동 저장 실행
  • 플레이어 복구 함수에서는 위치, 회전, Score, 탄약 상태까지 복원

특히 무기 탄약 상태를 TMap으로 관리하도록 해둔 덕분에, 이후 무기 종류가 늘어나더라도 저장 구조를 크게 갈아엎지 않고 확장할 수 있게 만들었다.