본문 바로가기

체크포인트와 세이브 설계

SaveGame에 진행 상태를 정의하고 체크포인트 저장·슬롯 로드·실패 복구 흐름을 설계해 재개 기능을 구현합니다.

한 시간 동안 진행한 게임이 꺼졌는데 처음부터 다시 해야 한다면 플레이어는 대부분 게임을 그만둡니다. 보스 앞에서 쓰러질 때마다 레벨 처음으로 돌아가도 마찬가지입니다. 그래서 진행 상황을 기록하고 다시 불러오는 기능은 게임의 기본이지만, 막상 만들어 보면 생각보다 어긋날 곳이 많습니다. 저장했다고 표시했는데 파일이 없거나, 다른 레벨을 불러온 뒤 플레이어가 원점에 서 있거나, 주웠던 아이템이 다시 나타나는 식입니다.

이 글에서는 플레이어가 특정 지점에 닿으면 자동으로 저장하는 체크포인트(checkpoint)와, 원할 때 저장하고 불러오는 세이브/로드(save/load)를 언리얼 엔진의 SaveGame 클래스로 만듭니다. 저장할 데이터를 정하는 데서 시작해, 레벨이 바뀌어도 불러온 데이터를 잃지 않는 흐름과 실패했을 때 기존 저장을 지키는 방법까지 설계합니다.


체크포인트와 세이브/로드

체크포인트는 게임플레이 중 특정 지점에 도달했을 때 플레이어의 현재 상태, 예를 들어 위치, 체력, 인벤토리를 자동으로 저장하는 지점입니다. 플레이어가 쓰러지면 마지막 체크포인트에서 다시 시작하게 해서, 너무 많은 진행을 잃고 좌절하지 않도록 돕습니다. 체크포인트 저장은 보통 플레이어가 따로 무언가를 누르지 않아도 자동으로 일어납니다.

세이브/로드 시스템은 플레이어가 원할 때 진행 상황을 파일로 저장하고, 나중에 그 파일을 불러와 게임을 이어 가게 하는 시스템입니다. 그러려면 전역 변수, 레벨 상태, 퀘스트 진행도처럼 게임을 다시 이어 가는 데 필요한 상태를 저장하고 되살릴 수 있어야 합니다.

두 기능이 필요한 이유는 분명합니다. 플레이어는 원하는 때 게임을 멈추고 다시 시작할 수 있어야 하고, 공들여 쌓은 진행과 성과를 잃지 않아야 합니다. 실패했을 때 잃는 진행이 적으면 다시 도전하기 쉬워지고 게임에 들어오는 문턱도 낮아집니다. 개발하는 동안에도 특정 지점부터 테스트하거나 버그를 재현할 때 저장 파일이 쓸모 있습니다.


SaveGame 클래스: 저장할 값을 담는 그릇

언리얼 엔진은 게임 데이터를 저장하고 불러올 때 SaveGame 클래스를 씁니다. 일반 블루프린트처럼 변수를 선언하고 값을 넣을 수 있는 데이터 그릇입니다. SaveGame은 월드에 존재하지 않는 순수한 데이터 클래스라서 액터가 아니며, 틱도 없고 메시나 콜리전 같은 컴포넌트도 없습니다. 게임에서 중요한 데이터를 담는 용도로만 씁니다.

여기서 꼭 알아 둘 점은 SaveGame 오브젝트의 변수를 바꾸는 것과 파일을 쓰는 것이 별개의 일이라는 것입니다. 값을 채운 뒤 Save Game to Slot이나 비동기 저장 노드를 호출해야 데이터가 직렬화(serialization)되어 파일에 기록됩니다. 메모리에 있는 SaveGame 변수만 바꿔서는 디스크의 파일이 갱신되지 않습니다.


저장할 데이터 정하기

먼저 무엇을 저장할지 정하고, 그 값을 담을 SaveGame 블루프린트를 만듭니다. 콘텐츠 브라우저에서 마우스 오른쪽 버튼을 눌러 Blueprint Class를 고르고, All Classes에서 SaveGame을 검색해 부모 클래스로 선택한 뒤 BP_MySaveGame처럼 이름을 짓습니다. 그리고 BP_MySaveGame을 열어 게임의 상태를 나타내는 변수를 추가합니다.

분류변수 예
플레이어PlayerLocation(Vector), PlayerHealth(Float), PlayerInventory(구조체 배열), CurrentWeapon(열거형 또는 String)
게임 진행CurrentLevelName(Name), QuestProgress(Map 또는 구조체), UnlockedAbilities(열거형 배열)
월드 상태DestroyedEnemies(GUID 배열), CollectedItems(GUID 배열)

SaveGame에 두는 변수는 저장할 수 있는 타입이어야 합니다. 일반적으로 Integer, Float, Boolean, String, Vector, Rotator, Transform 같은 기본 타입과 열거형, 구조체, 그리고 이것들의 배열은 저장할 수 있습니다.

조심할 것은 오브젝트 참조입니다. 액터 참조를 SaveGame에 넣어도, 그 액터와 모든 컴포넌트의 상태를 통째로 저장했다가 다시 만들어 주지는 않습니다. 그래서 월드 상태는 저장용 GUID나 직접 정한 ID와 복원할 데이터를 따로 저장하고, 불러올 때 그 ID로 대상을 찾거나 새로 만듭니다. 실행 중에 얻는 GetUniqueID 값은 게임을 다시 실행하면 같은 대상을 가리킨다는 보장이 없으므로 영구 ID로 쓰지 않습니다.


체크포인트 액터 만들기

체크포인트는 플레이어가 닿으면 저장을 요청하는 액터입니다. 새 액터 블루프린트 BP_Checkpoint를 만들고, 플레이어를 감지할 Box Collision 컴포넌트와 눈에 보이게 할 Static Mesh를 추가합니다. 박스와 플레이어 캐릭터 양쪽 컴포넌트의 쿼리와 Overlap 응답, Generate Overlap Events를 설정한 뒤, Box Collision에 On Component Begin Overlap 이벤트를 만듭니다.

BP_Checkpoint에는 Boolean 변수 두 개를 둡니다. 이미 활성화되었는지를 나타내는 bIsActivated와, 비동기 저장을 쓸 때 저장이 진행 중인지를 나타내는 bIsSaving이며, 둘 다 false로 시작합니다. 저장의 성공이나 실패 결과를 체크포인트로 돌려주는 경로도 함께 연결합니다. 저장을 요청했다는 사실만으로 체크포인트를 활성화하지 않기 위해서입니다.

On Component Begin Overlap에서는 Other Actor를 Cast To BP_PlayerCharacter로 캐스트합니다. 캐스트가 성공하면 Get Game Mode를 호출해 GM_MyGame으로 캐스트하고, 뒤에서 만들 SaveGame 커스텀 이벤트를 호출하면서, 입력 RequestingCheckpoint에 자기 자신(Self)을 넘깁니다. 커스텀 이벤트는 호출한 쪽에 값을 돌려주지 않으므로, 게임 모드는 이 참조로 저장 결과를 체크포인트에 알립니다. 저장 로직을 체크포인트에 직접 만들 수도 있지만, 저장은 여러 곳에서 요청하므로 게임 모드에 한 번만 두는 편이 관리하기 쉽습니다.

중복 저장도 막아야 합니다. 이벤트가 들어오면 먼저 bIsActivated를 검사하고, 저장이 성공했다는 결과를 받은 뒤에만 bIsActivated를 true로 바꿉니다. 비동기 저장이라면 요청을 보낼 때 bIsSaving을 true로 바꿔 진행 중인 동안 들어오는 요청을 막고, 완료되면 다시 false로 돌립니다. 저장이 실패했다면 bIsActivated가 그대로 false이므로 플레이어가 다시 닿았을 때 재시도할 수 있습니다.


저장과 불러오기를 게임 모드에 만들기

저장과 불러오기 로직은 보통 싱글플레이어 게임이라면 게임 모드에, 멀티플레이어에서 클라이언트 쪽에 저장한다면 플레이어 컨트롤러에 둡니다. 이 글은 로컬 플레이어가 한 명인 싱글플레이어 게임 모드를 기준으로 설명합니다. 게임 모드, 스테이트 블루프린트에서 보았듯 원격 클라이언트에는 게임 모드가 없으므로, 이 방식을 멀티플레이어 클라이언트의 로컬 저장에 그대로 쓰지 않습니다.

레벨을 넘어 데이터를 보관할 곳 준비하기

다른 레벨에 저장된 게임을 불러오려면, 레벨이 바뀌는 동안 불러온 데이터를 들고 있을 객체가 필요합니다. 레벨을 열면 그 레벨의 게임 모드와 액터가 모두 새로 만들어지기 때문입니다. 그래서 먼저 프로젝트의 게임 인스턴스(Game Instance)를 상속한 블루프린트를 만들고, Project Settings > Maps & Modes의 Game Instance Class로 지정합니다. 게임 인스턴스는 게임이 실행되는 동안 레벨이 바뀌어도 하나가 계속 유지됩니다. 여기에 BP_MySaveGame 오브젝트 참조 타입의 변수 PendingSave를 만들고 기본값은 비워 둡니다.

복원은 ApplyPendingSave라는 함수로 직접 만듭니다. 이 함수는 새 게임 모드가 플레이어를 만든 직후, 예를 들어 기본 RestartPlayer 흐름이 끝난 뒤 호출되는 OnRestartPlayer 이벤트에서 현재 폰이 유효한지 확인한 다음 호출하도록 연결합니다. 여러 액터의 BeginPlay 중 어느 것이 먼저 실행될지에 기대지 않기 위해서입니다. 아래에 나오는 Get Health와 Set Health, 인벤토리 복원도 이 프로젝트의 플레이어 캐릭터에 직접 만든 함수이며, 엔진이 이런 이름의 노드를 자동으로 만들어 주지는 않습니다.

SaveGame 이벤트로 저장하기

GM_MyGame을 열고 String 변수 Save Slot Name을 만들어 MyGameSaveSlot 같은 값을 넣습니다. 세이브 파일을 여러 개 관리한다면 슬롯 번호를 덧붙이는 식으로 슬롯 이름을 나눕니다. 그리고 BP_Checkpoint 오브젝트 참조 입력 RequestingCheckpoint를 받는 SaveGame 커스텀 이벤트를 만들어 다음 순서로 잇습니다.

먼저 Does Save Game Exist 노드의 Slot Name에 Save Slot Name을 연결해, 이 슬롯에 기존 세이브 파일이 있는지 확인합니다. 파일이 있으면(True) Load Game from Slot으로 기존 SaveGame 오브젝트를 불러와 Return Value를 Cast To BP_MySaveGame으로 캐스트하고, 오브젝트가 유효하고 캐스트가 성공했을 때만 다음으로 넘어갑니다. 파일이 있어도 불러오기가 실패할 수 있으며, 그럴 때는 저장을 멈추고 오류를 알립니다. 기존 파일을 조용히 빈 데이터로 덮어쓰지 않기 위해서입니다. 파일이 없으면(False) Create Save Game Object의 Save Game Class에 BP_MySaveGame을 골라 새 오브젝트를 만들고, 마찬가지로 Cast To BP_MySaveGame으로 캐스트합니다.

두 경로가 만나는 지점부터는 공통 로직입니다. 유효한 SaveGame과 Get Player Pawn(인덱스 0)으로 얻은 플레이어 참조를 확인한 뒤, Get Actor Location으로 현재 위치를 읽어 PlayerLocation에 넣습니다. 체력은 Get Player Pawn을 플레이어 캐릭터로 캐스트한 뒤 Get Health로 읽어 PlayerHealth에 넣고, 다른 데이터도 같은 방식으로 BP_MySaveGame의 변수에 채웁니다. 현재 맵은 Get Current Level Name으로 읽고, 반환된 String을 Name으로 바꿔 CurrentLevelName에 기록합니다. 에디터에서 플레이할 때 레벨 이름 앞에 붙는 PIE 접두사를 지우는 Remove Prefix String 설정도 확인해, 다음 실행에서 실제로 열 수 있는 맵 이름이 저장되게 합니다. 이름 변수를 선언만 하고 값을 채우지 않은 채 다른 맵을 불러오려 해서는 안 됩니다.

마지막으로 Save Game to Slot을 호출합니다. SaveGame Object에는 값을 채운 BP_MySaveGame을, Slot Name에는 Save Slot Name을 연결하고, 사용자가 한 명이면 User Index는 보통 0으로 둡니다. 이 노드는 성공 여부를 Boolean으로 돌려주므로, 참일 때만 "게임 저장 완료!"를 출력하고, RequestingCheckpoint가 유효하면 그 체크포인트의 bIsActivated를 true로 바꿉니다. 거짓이면 실패를 표시하고 체크포인트도 활성화하지 않습니다. 저장, 존재 확인, 불러오기의 모든 노드에서 슬롯 이름과 User Index를 같게 써야 같은 파일을 가리킨다는 점도 잊지 않습니다.

LoadGame 이벤트로 불러오기

UI 버튼에서 호출할 LoadGame 커스텀 이벤트를 만듭니다. 먼저 Does Save Game Exist로 슬롯에 파일이 있는지 확인하고, 없으면 Print String으로 "저장된 게임이 없습니다!"를 출력하고 끝냅니다. 파일이 있으면 Load Game from Slot으로 불러와 Cast To BP_MySaveGame으로 캐스트합니다. 불러오기나 캐스트가 실패하면 오류를 표시하고 끝냅니다. 파일이 있다는 사실이 그 안의 데이터가 올바르다는 뜻은 아니기 때문입니다.

불러오기와 캐스트가 성공했다면, 그 SaveGame 참조를 먼저 게임 인스턴스의 PendingSave에 넣습니다. 같은 레벨이든 다른 레벨이든 이 대입을 마친 뒤에 다음으로 진행합니다. 저장된 CurrentLevelName이 지금 레벨과 다르면 Open Level (by Name)을 호출합니다. 이때 지금의 게임 모드는 레벨과 함께 사라지므로, Open Level 다음 실행 선에서 새 플레이어를 복원하려 하지 않습니다. 대신 새 레벨의 게임 모드가 폰 스폰을 마친 뒤 ApplyPendingSave를 호출합니다. 같은 레벨이라면 레벨을 다시 열지 않고, 현재 폰이 준비되었는지 확인한 뒤 바로 ApplyPendingSave를 호출합니다.

ApplyPendingSave는 게임 인스턴스의 PendingSave가 유효한지 확인하고 그 데이터를 읽습니다. 유효한 플레이어에 Set Actor Location으로 PlayerLocation을 적용하고, 캐스트한 뒤 Set Health로 PlayerHealth를 적용하며, 저장용 ID로 월드의 수집 아이템과 적의 상태를 되살립니다. 적용을 마치면 PendingSave를 비우고 "게임 불러오기 완료!"를 표시합니다. Open Level을 부르면 자동으로 연결되는 범용 On Level Loaded 이벤트가 있다고 가정하지 않는다는 점이 이 설계의 핵심입니다.

레벨 이동 동안 유지할 저장 데이터

로드/캐스트 성공→GameInstance PendingSave 보관→다른 레벨 열기→새 Pawn 준비→적용→참조 비움의 구체적 상태이다.

LOCAL SAVE · WORLD TRANSITION

레벨을 열기 전에 저장 데이터를 GameInstance에 보관합니다

이 흐름은 로컬 싱글플레이에서 다른 레벨로 이동하는 경우입니다. 이전 GameMode는 사라지므로 새 GameMode가 Pawn 준비를 확인하고 보관한 데이터를 명시적으로 적용합니다.

레벨 이동 동안 유지할 저장 데이터실행은 로드 확인, GameInstance 보관, 다른 레벨 열기, Pawn 준비, 적용 순서다. 점선은 GameInstance에 보관한 데이터가 새 월드의 적용 함수로 전달되는 관계다.Load Game From Slot유효성·캐스트 확인GameInstancePendingSave 보관다른 레벨이면Open Level새 월드·Pawn 준비ApplyPendingSave적용 후 참조 비우기이전 월드새 월드보관한 데이터
레벨이동동안보관한데이터와실행실선은로드확인→GI보관→OpenLevel→새Pawn준비→적용순서다. 오른쪽점선은GI보관참조가적용함수에전달되는데이터관계다.Load GameFrom Slot유효성·캐스트 확인GameInstancePendingSave 보관다른 레벨이면Open Level새 월드Pawn 준비ApplyPendingSave적용 후 참조 비우기

실선은 실행 순서, 점선은 레벨 이동 동안 보관한 데이터의 전달입니다.

같은 레벨이면 이동을 생략합니다

같은 레벨도 로드·캐스트 성공 뒤 GameInstance의 PendingSave에 보관하는 단계는 공통입니다. Open Level만 생략하고 현재 Pawn과 적용 대상의 준비를 확인한 뒤 ApplyPendingSave가 보관한 데이터를 읽도록 호출합니다. 적용 완료 후 PendingSave를 비웁니다. 로드 실패·캐스트 실패 때는 적용하거나 기존 저장 파일을 덮어쓰지 않습니다.

새 월드에서 적용 완료를 확인합니다

체력·인벤토리·Transform을 적용한 뒤 PendingSave 참조를 비웁니다. Open Level 다음 실행 핀에 이전 월드의 적용 로직을 이어 붙이지 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목레벨 이동 동안 보관한 데이터와 실행
관계·조건 설명실선은 로드 확인→GI 보관→OpenLevel→새 Pawn 준비→적용 순서다. 오른쪽 점선은 GI에 보관한 참조가 적용 함수에 전달되는 데이터 관계다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
로드 보관

Load와 Cast에 성공한 데이터를 GameInstance에 둔다.

  • 필드: PendingSave
  • 조건: Load/Cast 성공
  • 레벨이 바뀌어도 남을 소유 위치가 필요하다.
레벨·Pawn 준비

새 레벨을 열고 적용할 Pawn을 기다린다.

  • 상태: 새 레벨 로드
  • 대상: 새 Pawn 준비
  • OpenLevel 호출 직후 이미 Pawn이 준비됐다고 가정하지 않는다.
적용·참조 정리

준비된 Pawn에 적용하고 PendingSave를 비운다.

  • 결과: 저장값 적용
  • PendingSave: 비움
  • 적용 경계를 지나 뒤늦게 같은 데이터를 재적용하지 않게 한다.

레벨 이동 동안 유지할 저장 데이터

로드/캐스트 성공→GameInstance PendingSave 보관→다른 레벨 열기→새 Pawn 준비→적용→참조 비움의 구체적 상태이다.

LOCAL SAVE · WORLD TRANSITION

레벨을 열기 전에 저장 데이터를 GameInstance에 보관합니다

이 흐름은 로컬 싱글플레이에서 다른 레벨로 이동하는 경우입니다. 이전 GameMode는 사라지므로 새 GameMode가 Pawn 준비를 확인하고 보관한 데이터를 명시적으로 적용합니다.

레벨 이동 동안 유지할 저장 데이터실행은 로드 확인, GameInstance 보관, 다른 레벨 열기, Pawn 준비, 적용 순서다. 점선은 GameInstance에 보관한 데이터가 새 월드의 적용 함수로 전달되는 관계다.Load Game From Slot유효성·캐스트 확인GameInstancePendingSave 보관다른 레벨이면Open Level새 월드·Pawn 준비ApplyPendingSave적용 후 참조 비우기이전 월드새 월드보관한 데이터
레벨이동동안보관한데이터와실행실선은로드확인→GI보관→OpenLevel→새Pawn준비→적용순서다. 오른쪽점선은GI보관참조가적용함수에전달되는데이터관계다.Load GameFrom Slot유효성·캐스트 확인GameInstancePendingSave 보관다른 레벨이면Open Level새 월드Pawn 준비ApplyPendingSave적용 후 참조 비우기

실선은 실행 순서, 점선은 레벨 이동 동안 보관한 데이터의 전달입니다.

같은 레벨이면 이동을 생략합니다

같은 레벨도 로드·캐스트 성공 뒤 GameInstance의 PendingSave에 보관하는 단계는 공통입니다. Open Level만 생략하고 현재 Pawn과 적용 대상의 준비를 확인한 뒤 ApplyPendingSave가 보관한 데이터를 읽도록 호출합니다. 적용 완료 후 PendingSave를 비웁니다. 로드 실패·캐스트 실패 때는 적용하거나 기존 저장 파일을 덮어쓰지 않습니다.

새 월드에서 적용 완료를 확인합니다

체력·인벤토리·Transform을 적용한 뒤 PendingSave 참조를 비웁니다. Open Level 다음 실행 핀에 이전 월드의 적용 로직을 이어 붙이지 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목레벨 이동 동안 보관한 데이터와 실행
관계·조건 설명실선은 로드 확인→GI 보관→OpenLevel→새 Pawn 준비→적용 순서다. 오른쪽 점선은 GI에 보관한 참조가 적용 함수에 전달되는 데이터 관계다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
로드 보관

Load와 Cast에 성공한 데이터를 GameInstance에 둔다.

  • 필드: PendingSave
  • 조건: Load/Cast 성공
  • 레벨이 바뀌어도 남을 소유 위치가 필요하다.
레벨·Pawn 준비

새 레벨을 열고 적용할 Pawn을 기다린다.

  • 상태: 새 레벨 로드
  • 대상: 새 Pawn 준비
  • OpenLevel 호출 직후 이미 Pawn이 준비됐다고 가정하지 않는다.
적용·참조 정리

준비된 Pawn에 적용하고 PendingSave를 비운다.

  • 결과: 저장값 적용
  • PendingSave: 비움
  • 적용 경계를 지나 뒤늦게 같은 데이터를 재적용하지 않게 한다.

위 그림은 로컬 싱글플레이에서 다른 레벨의 저장을 불러오는 흐름을 이전 월드와 새 월드로 나누어 보여 줍니다. 실선은 실행 순서로, Load Game From Slot에서 유효성과 캐스트를 확인하고, 게임 인스턴스의 PendingSave에 보관한 뒤, 다른 레벨이면 Open Level을 호출하고, 새 월드에서 폰이 준비되면 ApplyPendingSave로 적용하고 참조를 비웁니다. 오른쪽 점선은 실행이 아니라 데이터의 전달로, 게임 인스턴스에 보관한 참조가 레벨 이동을 건너 새 월드의 적용 함수로 넘어간다는 뜻입니다.

그림의 주석은 두 가지를 덧붙입니다. 같은 레벨이라면 Open Level만 생략하고, 로드와 캐스트에 성공한 뒤 PendingSave에 보관하는 단계와 현재 폰의 준비를 확인한 뒤 ApplyPendingSave를 호출하는 단계는 그대로 거칩니다. 그리고 새 월드에서 체력, 인벤토리, Transform을 적용한 뒤에 PendingSave를 비우며, Open Level 다음 실행 핀에 이전 월드의 적용 로직을 이어 붙이지 않습니다. 로드나 캐스트가 실패하면 적용하지도 않고 기존 저장 파일을 덮어쓰지도 않습니다.

단계 그림은 같은 흐름을 세 단계로 따라갑니다. 로드 보관 단계에서 Load와 Cast에 성공한 데이터를 게임 인스턴스에 두고, 레벨과 폰 준비 단계에서 새 레벨을 열고 적용할 폰을 기다리며, 적용과 참조 정리 단계에서 준비된 폰에 데이터를 적용하고 PendingSave를 비웁니다.

메뉴 버튼에 연결하기

필요하다면 메인 메뉴나 게임 중 메뉴의 저장, 불러오기 버튼에 이 두 이벤트를 연결합니다. 버튼의 On Clicked 이벤트에서 Get Game Mode를 호출해 Cast To GM_MyGame이 성공한 분기에서만 SaveGame이나 LoadGame을 호출합니다.


세이브 시스템을 설계할 때 따질 것

저장할 범위부터 정합니다. 모든 것을 저장할 필요는 없으며, 한 번만 재생되는 이펙트나 잠깐 쓰는 계산 변수처럼 다시 만들거나 쉽게 계산할 수 있는 데이터는 저장하지 않아도 됩니다. 복잡한 액터는 저장용 ID, 클래스, Transform, 필요한 상태 값을 보관했다가 되살립니다. 레벨에 직접 배치한 액터에도 다시 실행하거나 맵을 고쳐도 유지되는 ID를 붙여야 하며, 표시 이름이나 실행 중에만 유효한 고유 번호로 영구 식별을 대신하지 않습니다.

적용 시점도 중요합니다. 저장된 값은 월드와 폰이 준비된 뒤에 적용해야 하므로, 여러 액터의 BeginPlay 순서가 우연히 맞기를 기대하지 말고 스폰이 끝나는 지점에서 복원 함수를 호출합니다. 성능도 고려합니다. 너무 많은 데이터를 너무 자주 저장하면 게임이 멈칫할 수 있으므로, 필요한 데이터만 저장하고 저장 빈도를 조절합니다.

게임을 업데이트하면 세이브 파일의 구조가 바뀔 수 있습니다. BP_MySaveGame에 SaveGameVersion 같은 Integer 변수를 두면, 불러올 때 버전이 다른 것을 알아채고 이전 버전의 데이터를 새 구조로 바꾸는 로직을 넣을 수 있습니다. 보안 면에서는 일반 해시만으로는 악의적인 변조를 막을 수 없다는 점을 알아 둡니다. 변조를 막아야 한다면 인증된 무결성 검증과 믿을 수 있는 키, 서버의 권한을 따로 설계하며, 플레이어 컴퓨터에 있는 로컬 파일을 완전히 믿지 않습니다. 마지막으로 "저장 중..." 표시나 로딩 바처럼 저장과 불러오기가 진행 중임을 알리는 UI를 두면 플레이어가 작업이 끝나기 전에 게임을 끄는 일을 줄일 수 있습니다.

저장과 불러오기가 어긋날 때

증상원인고치는 방법
저장 버튼을 눌러도 다음 실행에서 데이터가 사라짐Save Slot Name이 서로 다르거나 Save Game to Slot을 호출하지 않음저장과 불러오기가 같은 슬롯 이름을 쓰는지 확인하고, 저장 결과 Boolean과 오류 경로를 기록해 성공 여부를 구분합니다.
불러온 직후 플레이어가 원점으로 이동Open Level 뒤에 복원 로직이 다시 실행되지 않음레벨을 옮겨야 한다면 새 레벨의 폰 생성이 끝나는 지점에서 PendingSave의 위치를 적용합니다.
수집 아이템의 상태가 되살아나지 않음액터 참조 자체를 저장하려 함액터 참조 대신 고유 ID 배열을 저장하고, 불러올 때 ID를 맞춰 월드 상태를 다시 적용합니다.

정리

  • 체크포인트는 특정 지점에서 자동으로 저장하고, 세이브/로드는 플레이어가 원할 때 저장하고 불러옵니다.
  • SaveGame은 월드에 없는 데이터 클래스이며, Save Game to Slot을 호출해야 파일에 기록됩니다. 이 노드의 Boolean 결과로 성공 여부를 확인합니다.
  • 액터 참조 대신 영구 ID와 복원할 데이터를 저장하고, 실행 중의 GetUniqueID는 영구 ID로 쓰지 않습니다.
  • 체크포인트는 저장이 성공한 뒤에만 활성화하고, 실패하면 다시 시도할 수 있게 둡니다.
  • 다른 레벨을 불러올 때는 불러온 데이터를 게임 인스턴스의 PendingSave에 보관하고, 새 레벨의 폰이 준비된 뒤 ApplyPendingSave로 적용합니다.
  • 파일이 있어도 불러오기나 캐스트가 실패할 수 있으며, 그럴 때는 적용하지도 덮어쓰지도 않습니다.

연습 문제

1. 다른 레벨의 저장 불러오기

Level A에서 불러온 슬롯에는 Level B, 위치 (300, 200, 0) cm, 체력 60이 기록되어 있습니다. Load Game from Slot과 캐스트는 성공했고, 아직 Level B의 폰은 없습니다. 데이터를 어느 객체에 보관하고, 위치와 체력은 언제 적용해야 할까요? 다른 시도에서 슬롯 파일은 있지만 캐스트가 실패했다면, 기본값으로 새 세이브를 만들어 같은 슬롯에 덮어써도 될까요?

해설 보기

Level A가 교체되기 전에, 프로젝트 설정에서 지정한 게임 인스턴스의 PendingSave에 불러온 결과를 보관합니다. 게임 모드와 레벨의 액터는 레벨과 함께 바뀌지만 게임 인스턴스는 남기 때문입니다. Level B의 게임 모드가 새 폰이 준비된 것을 확인한 뒤 ApplyPendingSave를 호출해 위치 (300, 200, 0) cm와 체력 60을 적용하고, 적용에 성공하면 PendingSave를 비웁니다. 이전 게임 모드의 Open Level 다음 실행 선에서 새 폰을 이미 얻었다고 가정하지 않습니다.

캐스트가 실패했다면 적용도 덮어쓰기도 멈추고 오류를 알립니다. 기본값으로 덮어쓰면 플레이어의 기존 진행이 사라질 수 있습니다. 파일이 있는지, 불러온 결과가 유효한지, 클래스가 맞는지는 각각 따로 확인해야 하는 조건입니다.

2. 실패한 저장과 체크포인트

플레이어가 체크포인트에 닿았는데 디스크 공간이 부족해 Save Game to Slot이 false를 돌려줬습니다. 그런데 그래프는 저장을 요청하자마자 bIsActivated를 true로 바꾸도록 되어 있습니다. 어떤 문제가 생길까요?

해설 보기

체크포인트가 이미 활성화된 것으로 표시되므로, 플레이어가 다시 닿아도 저장을 재시도하지 않습니다. 그런데 실제 파일은 기록되지 않았으므로, 게임을 다시 실행하면 이 체크포인트 이전의 저장에서 시작하게 됩니다. 플레이어는 저장된 줄 알았던 진행을 잃습니다.

bIsActivated는 Save Game to Slot의 결과가 true일 때만 바꾸고, false라면 실패를 표시한 뒤 그대로 false로 두어 다시 닿았을 때 재시도할 수 있게 해야 합니다. 비동기 저장이라면 진행 중에는 bIsSaving으로 중복 요청만 막고, 완료 결과에 따라 활성화 여부를 정합니다.

3. 다시 나타나는 아이템

수집한 아이템을 기록하려고 BP_MySaveGame에 아이템 액터 참조의 배열을 저장했습니다. 게임을 다시 실행해 불러오니 이미 주운 아이템이 모두 다시 놓여 있습니다. 왜 그럴까요? 어떻게 바꿔야 할까요?

해설 보기

액터 참조는 그 실행에서 메모리에 있던 액터를 가리킬 뿐이라서, 게임을 다시 실행하면 같은 액터를 찾아 주지 못합니다. 레벨이 새로 열리면 배치된 아이템이 모두 다시 만들어지고, 저장된 참조로는 어느 것을 지워야 할지 알 수 없습니다.

각 아이템에 맵을 고치거나 다시 실행해도 바뀌지 않는 ID를 붙이고, 주운 아이템의 ID를 CollectedItems 배열에 저장합니다. 불러올 때는 ApplyPendingSave에서 레벨의 아이템을 돌며, ID가 배열에 있는 아이템을 제거하거나 수집된 상태로 바꿉니다.

SaveGame으로 저장하고 불러오는 기본 흐름과 슬롯 이름, 사용자 인덱스의 의미는 언리얼 엔진 공식 문서의 게임 저장과 불러오기에서 확인할 수 있습니다.

다음 글 게임플레이 능력 시스템 구현에서는 공격, 스킬, 버프 같은 능력을 비용과 쿨다운까지 포함해 관리하는 게임플레이 능력 시스템을 블루프린트로 다룹니다.