저장 및 로드 기본 구현
SaveGame 클래스를 정의하고 슬롯에 데이터를 저장·불러와 게임을 다시 실행해도 상태가 유지되도록 구현합니다.
게임을 껐다 다시 켰는데 점수와 위치가 처음으로 돌아가 있다면, 플레이어는 그동안의 진행을 잃어버린 셈입니다. 블루프린트 변수는 게임이 실행되는 동안 메모리에만 있으므로, 게임을 종료하면 함께 사라집니다. 점수나 설정뿐 아니라 플레이어의 위치, 인벤토리, 퀘스트 진행도, 월드의 상태까지 다음 실행에서 이어 가려면 이 값을 디스크에 기록하고 다시 읽어 오는 기능이 필요합니다.
언리얼 엔진은 이를 위해 저장 게임 객체(SaveGame object)를 제공합니다. 이 글에서는 저장할 데이터를 담는 SaveGame 블루프린트를 만들고, 값을 채워 슬롯에 기록한 뒤, 다음 실행에서 슬롯을 읽어 검증한 값만 월드에 적용하는 과정을 차례로 구현합니다.
SaveGame 객체는 메모리의 데이터 묶음입니다
저장 게임 객체는 저장하고 싶은 데이터를 변수로 담는 특별한 종류의 객체입니다. SaveGame 클래스를 부모로 블루프린트를 만들고, 그 안에 점수나 위치 같은 변수를 정의하면 됩니다. 파일을 열고 쓰는 일은 엔진이 맡으므로, 개발자는 무엇을 언제 저장하고 불러올지에만 집중할 수 있습니다. 공식 문서에 따르면 개발용 플랫폼에서는 저장 파일이 .sav 확장자로 프로젝트의 Saved\SaveGames 폴더에 만들어지고, 콘솔 같은 다른 플랫폼에서는 저장 위치가 달라집니다.
여기서 먼저 구분해 둘 것이 있습니다. SaveGame 객체 자체는 메모리에 있는 객체일 뿐이고, 만들었다고 저절로 디스크에 기록되지 않습니다. 현재 값을 객체에 채운 뒤 슬롯에 저장하는 노드가 성공해야 비로소 디스크에 기록되며, 다음 실행에서는 그 기록으로 새 메모리 객체를 다시 만들어 냅니다. 앱을 종료한 뒤에도 객체가 살아 있는 것이 아니라, 기록해 둔 값으로 새 객체를 복원하는 것입니다.
월드 상태와 저장 객체·슬롯의 경계
월드 값 복사→SaveGame 메모리 준비→슬롯 기록 성공과 이후 별도 로드→검증→준비된 월드 적용의 상태를 구분한다. 객체 생성만으로 기록됐다고 표시하지 않는다.
COPY → WRITE · READ → APPLY
메모리 객체를 만들고 값을 채운 뒤 슬롯에 저장합니다
저장은 월드 값 → SaveGame 메모리 객체 → 슬롯 기록입니다. 로드는 반대 방향으로 새 메모리 객체를 복원한 뒤 검증한 값을 준비된 월드에 적용합니다.
로드는 저장값을 메모리 객체로 복원합니다. 월드에 적용하는 작업과 저장 성공 여부 확인은 호출자의 로직입니다.
저장 완료를 판정할 때
Create SaveGame Object만으로 기록되지 않습니다. Slot Name과 User Index를 지정하고 저장 결과가 성공일 때 완료로 처리합니다.
복원 값을 적용할 때
슬롯 존재만으로 로드 성공·클래스 호환이 보장되지 않습니다. 캐스트 성공과 값 범위를 확인한 뒤 점수·위치·UI에 적용합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 저장할 때 월드 값을 메모리 객체에 복사하고 슬롯에 기록한다. 로드할 때 슬롯에서 메모리 객체를 복원하고 검증한 값을 월드에 적용한다. 저장과 로드의 화살표는 서로 반대 방향이다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 메모리 복사 월드 값을 SaveGame 객체에 옮긴다. |
|
|
| 슬롯 기록 SaveGameToSlot의 성공을 확인한다. |
|
|
| 별도 로드·적용 나중에 Load 결과를 검증해 준비된 월드에 적용한다. |
|
|
월드 상태와 저장 객체·슬롯의 경계
월드 값 복사→SaveGame 메모리 준비→슬롯 기록 성공과 이후 별도 로드→검증→준비된 월드 적용의 상태를 구분한다. 객체 생성만으로 기록됐다고 표시하지 않는다.
COPY → WRITE · READ → APPLY
메모리 객체를 만들고 값을 채운 뒤 슬롯에 저장합니다
저장은 월드 값 → SaveGame 메모리 객체 → 슬롯 기록입니다. 로드는 반대 방향으로 새 메모리 객체를 복원한 뒤 검증한 값을 준비된 월드에 적용합니다.
로드는 저장값을 메모리 객체로 복원합니다. 월드에 적용하는 작업과 저장 성공 여부 확인은 호출자의 로직입니다.
저장 완료를 판정할 때
Create SaveGame Object만으로 기록되지 않습니다. Slot Name과 User Index를 지정하고 저장 결과가 성공일 때 완료로 처리합니다.
복원 값을 적용할 때
슬롯 존재만으로 로드 성공·클래스 호환이 보장되지 않습니다. 캐스트 성공과 값 범위를 확인한 뒤 점수·위치·UI에 적용합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 저장할 때 월드 값을 메모리 객체에 복사하고 슬롯에 기록한다. 로드할 때 슬롯에서 메모리 객체를 복원하고 검증한 값을 월드에 적용한다. 저장과 로드의 화살표는 서로 반대 방향이다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 메모리 복사 월드 값을 SaveGame 객체에 옮긴다. |
|
|
| 슬롯 기록 SaveGameToSlot의 성공을 확인한다. |
|
|
| 별도 로드·적용 나중에 Load 결과를 검증해 준비된 월드에 적용한다. |
|
|
위 그림은 저장과 로드가 서로 반대 방향의 처리라는 점을 보여 줍니다. 저장할 때는 월드 상태의 값을 SaveGame 메모리 객체로 복사한 뒤 슬롯에 기록합니다. 로드할 때는 슬롯에서 메모리 객체를 복원하고, 검증한 값을 월드에 적용합니다. 그림의 두 설명 상자처럼 Create SaveGame Object만으로는 아무것도 기록되지 않으므로 슬롯 이름과 사용자 인덱스를 지정해 저장한 결과가 성공일 때만 완료로 처리하고, 슬롯이 있다는 사실만으로는 로드 성공이나 클래스 호환을 알 수 없으므로 캐스트 성공과 값의 범위를 확인한 뒤 점수와 위치, UI에 적용합니다.
단계 그림은 이 흐름을 세 단계로 나눕니다. 먼저 월드의 값을 SaveGame 객체로 옮기고, 다음으로 Save Game to Slot의 결과가 성공인지 확인합니다. 마지막 단계는 별도의 시점, 예를 들어 다음 실행에서 일어나며 Load 결과를 검증해 준비된 월드에 적용합니다. 로드한 값을 월드에 적용하는 일과 저장 성공 여부를 확인하는 일은 노드가 대신해 주지 않고, 호출하는 쪽의 로직이 해야 합니다.
저장할 데이터를 담을 SaveGame 블루프린트
먼저 어떤 데이터를 저장할지 정합니다. 콘텐츠 브라우저의 빈 곳을 오른쪽 클릭해 Blueprint Class를 고르고, 부모 클래스 선택 창의 All Classes를 펼쳐 SaveGame을 검색해 선택합니다. 이름은 BP_MySaveGame으로 짓습니다.
BP_MySaveGame을 열고 마이 블루프린트(My Blueprint) 패널에서 저장할 변수를 추가한 뒤 컴파일하고 저장합니다. 이 글의 예제는 다음 네 변수를 씁니다.
| 변수 | 타입 | 저장하는 값 |
|---|---|---|
PlayerScore | Integer | 현재 점수 |
PlayerLocation | Vector | 플레이어의 위치 |
UnlockedLevels | Integer 배열 | 열린 레벨 번호 목록 |
PlayerName | Text | 플레이어 이름 |
이제 BP_MySaveGame은 게임 상태를 담을 수 있는 데이터 그릇이 되었습니다. 이 객체에는 로직을 넣지 않고 값만 담는 것이 일반적입니다.
저장하기: 객체 만들기, 값 복사, 슬롯에 기록
저장은 보통 플레이어의 저장 입력, 체크포인트 도달이나 레벨 전환 같은 이벤트, 또는 게임을 종료하는 시점에 실행합니다. 여기서는 입력을 받도록 구성된 블루프린트(예: BP_PlayerCharacter)에서 저장 입력에 반응하는 이벤트를 출발점으로 삼습니다. BP_GameManager처럼 입력을 받지 않는 블루프린트라면 체크포인트 같은 다른 이벤트에서 시작합니다.
첫 단계는 Create SaveGame Object 노드로 메모리 객체를 만드는 것입니다. 저장 이벤트의 실행 선을 이 노드에 잇고, Save Game Class 핀에 BP_MySaveGame을 지정합니다. 이 노드는 지정한 클래스의 새 인스턴스를 메모리에 만들어 반환하며, 반환 값의 타입이 Save Game Class 핀에 지정한 클래스로 정해집니다. 그래서 Return Value 핀에서 바로 끌어 Set PlayerScore 같은 노드를 추가할 수 있고, 따로 Cast To BP_MySaveGame을 거칠 필요가 없습니다.
다음으로 현재 값을 객체에 복사합니다. Create SaveGame Object의 실행 출력에서 각 Set 노드로 차례로 실행 선을 잇고, GameInstance에 있는 점수와 유효한 플레이어 캐릭터의 Get Actor Location 값을 저장 객체의 변수에 넣습니다. GameManager에서 이 로직을 실행한다면 Get Actor Location의 Target에 GameManager 자신이 아니라 플레이어 참조를 연결해야 플레이어의 위치가 저장됩니다.
마지막으로 값을 다 채운 뒤 Set 노드 뒤에 Save Game to Slot 노드를 잇습니다. SaveGame Object 핀에는 Create SaveGame Object의 Return Value를 연결하고, Slot Name 핀에는 저장 파일을 구분할 슬롯 이름을 문자열로 넣습니다(예: SaveSlot1). User Index는 저장하는 플랫폼 사용자의 인덱스로, 일부 플랫폼에서는 무시되며 네트워크 플레이어 ID와는 다른 개념입니다. 이 예제는 사용자가 한 명이므로 저장과 로드 모두 0을 씁니다. Save Game to Slot은 성공하면 true, 실패하면 false를 돌려주므로, Return Value를 Branch에 연결해 성공했을 때만 완료 메시지를 보여 주고 실패했을 때는 재시도나 안내를 처리합니다.
불러오기: 슬롯 확인, 복원, 검증, 적용
로드는 보통 게임을 시작할 때(Event BeginPlay)나 메인 메뉴의 이어하기 버튼을 눌렀을 때 실행합니다. 저장 파일이 항상 정상적으로 있다고 가정하지 않는 것이 로드 로직의 핵심이며, 그래서 단계마다 실패할 수 있는 지점을 나누어 처리합니다.
먼저 Does Save Game Exist 노드로 슬롯이 있는지 확인합니다. 반드시 필요한 노드는 아니지만, 첫 실행처럼 저장 파일이 없는 경우를 새 게임으로 자연스럽게 보낼 수 있습니다. Slot Name과 User Index는 저장할 때와 똑같이 넣고, Return Value를 Branch의 Condition에 연결합니다. True이면 로드로 진행하고, False이면 새 게임을 시작하거나 저장 파일이 없다는 안내를 보여 줍니다.
Branch의 True 뒤에는 Load Game from Slot 노드를 잇고, 역시 저장할 때와 같은 Slot Name과 User Index를 넣습니다. 이 노드는 파일을 읽어 SaveGame 객체로 복원하는 데 성공하면 그 객체를 반환하고, 실패하면 유효한 객체를 얻지 못할 수 있습니다. 슬롯이 있다는 확인은 로드가 성공한다는 뜻도, 그 파일이 지금의 BP_MySaveGame으로 저장되었다는 뜻도 아닙니다.
Load Game from Slot의 반환 값은 부모 타입인 SaveGame이므로, 이번에는 Cast To BP_MySaveGame이 필요합니다. 로드 노드의 실행 출력을 캐스트의 실행 입력에 잇고, 캐스트가 성공한 경로에서만 As BP My Save Game 핀으로 저장된 변수를 읽습니다. Cast Failed 경로는 새 게임의 기본값을 쓰거나 오류를 안내하는 쪽으로 보냅니다.
읽은 값은 범위를 확인한 뒤 현재 GameInstance와 준비된 플레이어 액터에 적용합니다. 예를 들어 Set Actor Location 노드로 플레이어의 위치를 로드한 PlayerLocation으로 옮깁니다. Load Game from Slot은 값을 메모리 객체로 돌려줄 뿐, 월드의 위치나 UI를 저절로 바꾸지 않습니다. 적용하는 노드를 직접 연결해야 화면에 반영됩니다.
처음부터 끝까지 한 번 확인하기
따라 해 볼 때는 저장부터 로드까지를 한 번에 이어서 확인합니다. 테스트용 슬롯을 정하고 저장 성공 메시지를 확인한 뒤, Stop으로 플레이를 끝내고 새로 Play해서 같은 슬롯을 로드하고, 적용한 뒤의 점수를 출력합니다. 같은 플레이 세션 안에서만 확인하면 메모리에 남은 값을 저장 파일에서 읽은 값으로 착각할 수 있으므로, 플레이를 끝냈다가 다시 시작하는 단계를 빼지 않습니다.
이때 기존 플레이 데이터를 덮어쓰지 않도록 별도의 슬롯 이름을 씁니다. 또 저장 입력을 받는 액터와 GameInstance 참조, 로드한 값을 적용할 플레이어가 준비되어 있는지도 미리 확인합니다. 이 중 하나라도 빠지면 저장과 로드 노드가 정상이어도 결과가 화면에 나타나지 않습니다.
저장 설계에서 주의할 점
SaveGame 객체에는 정말로 저장해야 할 데이터만 넣습니다. 잠깐 쓰는 임시 값이나 게임 안에서 언제든 다시 계산할 수 있는 값은 저장하지 않는 편이 좋습니다. 저장 데이터가 작을수록 기록과 복원이 빠르고, 나중에 구조를 바꿀 때 신경 쓸 것도 줄어듭니다.
저장하거나 불러올 데이터가 크면 그 순간 게임이 잠깐 멈추는 프레임 드롭이 생길 수 있습니다. 이를 줄이려면 Async Save Game to Slot이나 Async Load Game from Slot 같은 비동기 노드를 씁니다. 다만 비동기 노드도 저장할 값을 준비하고 직렬화하고 완료를 처리하는 비용이 있으므로, 프레임 지연이 전혀 없다고 볼 수는 없습니다. 비동기 노드를 쓸 때는 완료되었을 때 실행되는 핀과 성공 여부를 기준으로 다음 처리를 연결합니다.
여러 저장 파일을 지원하려면 파일마다 고유한 Slot Name을 씁니다. 로드한 객체는 캐스트의 성공과 Cast Failed 실행 핀으로 나누어 처리하고, 값을 적용할 대상 액터가 유효한지도 확인해 Accessed None 오류를 피합니다. Is Valid는 참조가 유효한지 따로 검사하는 노드이며, 캐스트 노드의 출력 핀 이름이 아닙니다.
게임을 업데이트하면 SaveGame 객체의 구조가 바뀔 수 있습니다. 변수를 추가하거나 이름을 바꾼 뒤 예전 저장 파일을 불러오면 값이 예상과 다르게 채워질 수 있으므로, SaveGame 객체에 버전 번호 변수를 두고 로드할 때 그 번호를 보고 변환하는 방법을 고려합니다. 저장할 데이터가 많은 게임일수록 데이터를 어떻게 묶을지와 어느 시점에 저장하고 불러올지를 미리 설계해 두어야 합니다.
정리
- SaveGame 객체는 저장할 데이터를 담는 메모리 객체이며,
Save Game to Slot이 성공해야 디스크에 기록됩니다. Create SaveGame Object의 반환 값은Save Game Class핀에 지정한 클래스 타입이므로 캐스트 없이 변수를 설정할 수 있습니다.Save Game to Slot은 성공 여부를 불리언으로 돌려주므로, 성공했을 때만 완료로 처리합니다.- 로드는
Does Save Game Exist,Load Game from Slot,Cast To, 값 검증, 월드 적용의 순서로 나누어 각 단계의 실패를 처리합니다. - 로드 노드는 값을 복원할 뿐 월드를 바꾸지 않으며, 저장한 뒤 메모리에서 바뀐 값은 다시 저장하기 전까지 슬롯에 반영되지 않습니다.
연습 문제
1. 저장한 시점과 지금의 값
현재 점수 40을 SaveGame 객체에 복사하고, SaveSlot1과 User Index 0으로 저장에 성공했습니다. 그 뒤 메모리의 점수만 70으로 바꾸고 다시 저장하지 않았습니다. 같은 슬롯을 성공적으로 로드하고 클래스와 값을 검사한 뒤 점수를 적용하면 얼마가 될까요? 또 슬롯 존재 확인은 True였는데 로드 결과의 캐스트가 실패했다면, 40을 적용해도 될까요?
해설 보기
로드와 검사에 성공했다면 마지막으로 기록한 40을 적용합니다. 저장한 뒤 메모리에서 바꾼 70은 다시 저장하지 않았으므로 슬롯에 반영되지 않습니다. 슬롯은 저장에 성공한 순간의 값을 담고 있을 뿐입니다.
두 번째 조건에서는 유효한 저장 객체와 지금의 클래스에 맞는 값을 얻었는지 확인하지 못했습니다. 그러니 "아마 40이었을 것"이라고 추정해 적용하지 않고, 실패 안내나 새 게임 정책으로 분기합니다.
2. 만들기만 하고 저장하지 않음
저장 버튼을 누르면 Create SaveGame Object로 객체를 만들고 Set PlayerScore로 점수를 넣은 뒤 "저장 완료"를 출력하도록 만들었습니다. Save Game to Slot 노드는 연결하지 않았습니다. 게임을 껐다 켜서 Does Save Game Exist로 같은 슬롯을 확인하면 결과는 어떻게 될까요?
해설 보기
처음 저장하는 경우라면 슬롯에 기록된 파일이 없으므로 False가 나옵니다. 예전에 같은 슬롯에 저장한 적이 있다면 그때 기록한 파일이 남아 있어 True가 나오지만, 그 파일에는 이번 점수가 들어 있지 않습니다. 어느 경우든 이번 저장은 일어나지 않았습니다. Create SaveGame Object는 메모리에 객체를 만들 뿐이고, 그 객체는 게임을 끄면 사라집니다.
또 "저장 완료" 메시지는 실제 저장 결과와 상관없이 출력되었으므로 플레이어를 속인 셈입니다. Save Game to Slot을 연결하고, 그 반환 값이 true일 때만 완료 메시지를 보여 주도록 고칩니다.
3. 변수를 추가한 뒤의 예전 저장 파일
BP_MySaveGame에 PlayTime(Float) 변수를 새로 추가하고 기본값을 0으로 두었습니다. 변수를 추가하기 전에 만든 저장 파일을 불러오면 PlayTime에는 어떤 값이 들어 있을까요? 이런 변화에 대비하려면 무엇을 해 두면 좋을까요?
해설 보기
예전 저장 파일에는 PlayTime 값이 기록되어 있지 않으므로, 복원한 객체의 PlayTime은 클래스에 정한 기본값 0으로 남습니다. 로드는 실패하지 않지만, 0이 "플레이 시간이 0"이라는 뜻인지 "기록이 없었다"는 뜻인지는 구별할 수 없습니다.
이런 경우에 대비해 SaveGame 객체에 SaveVersion 같은 정수 변수를 두고 저장할 때마다 현재 버전을 기록합니다. 로드할 때 버전이 예전 것이면 빠진 값을 어떻게 채울지 정한 변환 처리를 거친 뒤 적용합니다.
SaveGame 객체로 저장하고 불러오는 방법과 저장 파일의 위치는 언리얼 엔진 공식 문서의 게임 저장과 불러오기에서 확인할 수 있습니다.
다음 글 간단한 데이터 직렬화와 역직렬화 기법에서는 저장 노드 안에서 일어나는 직렬화와 역직렬화의 원리를 살펴보고, 어떤 데이터는 그대로 저장되고 어떤 데이터는 따로 처리해야 하는지 알아봅니다.