안동민 개발노트

본문 시작

체크포인트와 세이브 설계

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

지금까지는 게임 핵심 로직과 네트워크 동기화를 다뤘습니다.

이제 플레이어가 중단 후 재개할 수 있도록 진행 상황 저장/불러오기 시스템을 설계해야 합니다.

이 기능은 플레이어 경험에 직접 영향을 주므로 게임 디자인 단계부터 신중히 고려해야 합니다.

이번 절에서는 체크포인트(Checkpoint)와 세이브/로드(Save/Load) 구현의 기본 개념과 설계 고려 사항을 정리합니다.


체크포인트와 세이브 시스템이란 무엇인가?

  • 체크포인트(Checkpoint)
    • 게임플레이 중 특정 지점에 도달했을 때, 플레이어의 현재 상태(위치, 체력, 인벤토리 등)를 자동으로 저장하는 지점입니다.
    • 주로 플레이어가 사망했을 때 마지막 체크포인트에서 다시 시작하도록 하여, 플레이어가 좌절하지 않고 게임을 계속 진행할 수 있도록 돕습니다.
    • 체크포인트는 일반적으로 플레이어의 명시적인 행동 없이 자동으로 이루어집니다.
  • 세이브/로드(Save/Load) 시스템
    • 플레이어가 원할 때 게임의 진행 상황을 파일로 저장하고, 나중에 이 파일을 불러와 게임을 재개할 수 있도록 하는 시스템입니다.
    • 게임의 모든 중요한 상태(전역 변수, 레벨 상태, 퀘스트 진행도 등)를 저장하고 불러올 수 있어야 합니다.

왜 세이브/로드 시스템이 필요한가?

  • 플레이어 편의성: 플레이어가 원하는 시간에 게임을 중단하고 다시 시작할 수 있도록 합니다.
  • 진행 상황 유지: 플레이어가 공들여 쌓은 게임 진행 상황과 성과를 잃지 않도록 보장합니다.
  • 게임 경험 개선: 플레이어가 실패했을 때 너무 많은 진행 상황을 잃지 않도록 하여 재시도를 독려하고 게임 진입 장벽을 낮춥니다.
  • 디버깅 및 개발: 개발 과정에서도 특정 지점부터 테스트하거나 재현하는 데 유용합니다.

언리얼 엔진의 세이브 시스템

언리얼 엔진은 게임 데이터를 저장하고 불러오는 데 SaveGame 클래스를 사용합니다.

이 클래스는 마치 일반적인 블루프린트처럼 변수를 선언하고 값을 저장할 수 있는 데이터 컨테이너 역할을 합니다.

SaveGame 클래스의 특징
  • 비-액터(Non-Actor) 클래스: SaveGame 클래스는 월드에 존재하지 않는 순수 데이터 클래스입니다. 액터가 아니므로 틱(Tick)이 없고, 메시나 콜리전 같은 컴포넌트도 없습니다.
  • 직렬화(Serialization): 데이터를 채운 뒤 Save Game To Slot 또는 비동기 저장 노드를 호출해야 파일이 기록됩니다. 메모리의 SaveGame 변수 변경만으로 디스크가 갱신되지는 않습니다.
  • 데이터 컨테이너: 게임의 중요한 데이터를 담는 용도로만 사용됩니다.

세이브/로드 시스템 설계 및 구현 단계

저장할 데이터 정의

먼저 게임에서 어떤 데이터들을 저장해야 할지 결정하고, 이를 담을 SaveGame 블루프린트를 만듭니다.

새 블루프린트 클래스 생성: 콘텐츠 브라우저에서 마우스 오른쪽 버튼 클릭 > 블루프린트 클래스(Blueprint Class) > All Classes에서 SaveGame을 검색하여 선택합니다.

  • 이름을 지정합니다. (예: BP_MySaveGame)

저장할 변수 추가: BP_MySaveGame 블루프린트를 열고, 게임의 상태를 나타내는 필요한 변수들을 추가합니다.

  • 플레이어 관련: PlayerLocation (Vector), PlayerHealth (Float), PlayerInventory (Array of Structs), CurrentWeapon (Enum/String)
  • 게임 진행 관련: CurrentLevelName (Name), QuestProgress (Map/Struct), UnlockedAbilities (Array of Enum)
  • 월드 상태 관련: DestroyedEnemies (Array of GUIDs), CollectedItems (Array of GUIDs)
  • 중요: SaveGame 클래스에 저장할 변수들은 SaveGame 클래스 자체가 지원하는 변수 타입이어야 합니다. 일반적으로 기본 변수 타입(Int, Float, Bool, String, Vector, Rotator, Transform 등), Enum, Struct, 그리고 이들의 배열 타입은 지원됩니다. 오브젝트 참조의 직렬화는 액터와 모든 컴포넌트의 상태를 통째로 저장·재생성하는 기능이 아닙니다. 저장용 GUID나 명시적 ID와 복원할 데이터를 따로 저장하고, 로드 시 ID로 대상을 찾거나 생성합니다. 런타임 GetUniqueID는 재실행 후에도 같은 대상을 가리키는 영구 ID가 아닙니다.

체크포인트 시스템 구현

체크포인트 액터를 만들고, 플레이어가 체크포인트에 도달하면 자동으로 게임 상태를 저장하도록 합니다.

BP_Checkpoint 액터 생성
  • 새로운 액터 블루프린트(예: BP_Checkpoint)를 만들고, Box Collision 컴포넌트와 Static Mesh (시각적 표시용)를 추가합니다.
  • 양쪽 컴포넌트의 Query/Overlap 응답과 Generate Overlap Events를 설정한 뒤, Box Collision에 On Component Begin Overlap 이벤트를 생성합니다.
저장 로직 구현 (플레이어가 겹쳤을 때)
  • On Component Begin Overlap 이벤트에서 Other Actor를 Cast To BP_PlayerCharacter로 캐스팅합니다.
  • 캐스팅 성공 시, Get Game Mode 노드를 호출하고 여러분의 게임 모드(GM_MyGame)로 캐스팅합니다.
  • GM_MyGame에서 Save Game 커스텀 이벤트를 호출하도록 연결합니다. (또는 GM_MyGame에서 해당 로직을 직접 구현)
  • 중복 저장 방지: bIsActivated를 검사하고 저장 성공을 확인한 뒤에만 True로 바꿉니다. 비동기 저장이면 bIsSaving으로 진행 중 요청을 막고, 완료 시 이를 해제합니다. 실패했다면 다시 시도할 수 있어야 합니다.

게임 저장 및 불러오기 로직

게임 저장/불러오기 로직은 보통 GameMode (싱글 플레이어 게임) 또는 PlayerController (멀티플레이어 게임의 경우 클라이언트 측 저장)에서 구현됩니다.

여기서는 로컬 플레이어 한 명인 싱글플레이 GameMode를 기준으로 설명합니다. 원격 클라이언트에는 GameMode가 없으므로 멀티플레이 클라이언트의 로컬 저장에 그대로 적용하지 않습니다.

GM_MyGame 블루프린트 열기

세이브 슬롯 이름 정의: Save Slot Name 변수(String)를 생성하고 MyGameSaveSlot 등으로 이름을 지정합니다. (여러 개의 세이브 파일을 관리할 경우 슬롯 번호 등을 추가)

저장 함수 (SaveGame)
  • SaveGame이라는 커스텀 이벤트를 생성합니다.
  • Does Save Game Exist 노드를 사용하여 해당 슬롯에 기존 세이브 파일이 있는지 확인합니다.
    • Slot Name 핀에 미리 정의한 Save Slot Name 변수를 연결합니다.
  • True (기존 세이브 파일 있음)
    • Load Game From Slot 노드를 호출하여 기존 SaveGame 오브젝트를 로드합니다.
    • Return Value를 Cast To BP_MySaveGame으로 캐스팅합니다.
    • 유효한 오브젝트와 캐스트 성공을 확인한 뒤 현재 상태를 업데이트합니다. 파일이 있어도 로드가 실패할 수 있으므로 실패 시 저장을 중단하고 오류를 알립니다. 기존 파일을 조용히 빈 데이터로 덮어쓰지 않습니다.
  • False (기존 세이브 파일 없음)
    • Create SaveGame Object 노드를 호출하여 새로운 BP_MySaveGame 오브젝트를 생성합니다.
    • SaveGame Class 핀에 BP_MySaveGame을 선택합니다.
    • Return Value를 Cast To BP_MySaveGame으로 캐스팅합니다.
  • 공통 로직 (새로 생성했든 로드했든)
    • 유효한 SaveGame과 Get Player Pawn(0)의 플레이어 참조를 확인합니다. 현재 위치를 Get Actor Location으로 읽어 PlayerLocation에 저장합니다.
    • 플레이어의 현재 체력: Get Player Pawn > (Cast to PlayerCharacter) > Get Health → BP_MySaveGame의 PlayerHealth 변수에 저장.
    • 다른 모든 저장할 데이터들도 유사하게 BP_MySaveGame 오브젝트의 해당 변수에 업데이트합니다.
    • 최종적으로 Save Game To Slot 노드를 호출합니다.
      • SaveGame Object 핀에 업데이트된 BP_MySaveGame 오브젝트를 연결합니다.
      • Slot Name 핀에 Save Slot Name 변수를 연결합니다.
      • User Index는 보통 0으로 설정합니다. (단일 유저 세이브)
    • 저장 노드의 불리언 결과가 참일 때만 게임 저장 완료!를 출력합니다. 거짓이면 실패를 표시하며 체크포인트도 활성화하지 않습니다. 모든 저장·조회·로드 노드에서 슬롯명과 User Index를 같게 사용합니다.
불러오기 함수 (LoadGame)
  • LoadGame이라는 커스텀 이벤트를 생성합니다. (UI 버튼 클릭 시 호출되도록)
  • Does Save Game Exist 노드를 사용하여 해당 슬롯에 세이브 파일이 있는지 확인합니다.
    • False일 경우, Print String으로 저장된 게임이 없습니다! 메시지를 출력하고 함수 종료.
  • True일 경우
    • Load Game From Slot 노드를 호출하여 BP_MySaveGame 오브젝트를 로드합니다.
    • Return Value를 Cast To BP_MySaveGame으로 캐스팅합니다.
    • 로드 결과와 캐스트가 실패하면 오류를 표시하고 종료합니다. 파일 존재는 데이터의 유효성을 보장하지 않습니다.
    • 저장된 CurrentLevelName이 다르면 로드한 SaveGame 참조를 프로젝트의 GameInstance에 PendingSave로 보관하고 Open Level (by Name)을 호출합니다. 이전 GameMode는 레벨과 함께 교체되므로 그 뒤 실행선에서 새 플레이어를 복원하려 하지 않습니다.
    • 새 레벨의 GameMode가 Pawn 스폰을 마친 뒤 명시적인 ApplyPendingSave 함수를 호출하도록 연결합니다. 같은 레벨이면 현재 Pawn이 준비됐는지 확인하고 이 복원 함수를 바로 호출합니다.
    • 복원 함수는 유효한 플레이어의 Set Actor Location(PlayerLocation), 캐스트 후 Set Health(PlayerHealth)를 적용하고, 저장용 ID로 월드의 수집 아이템·적 상태를 복원합니다.
    • 적용 완료 후 GameInstance의 PendingSave를 비우고 게임 불러오기 완료!를 표시합니다. Open Level 전체에 자동 연결되는 범용 On Level Loaded 이벤트가 있다고 가정하지 않습니다.
레벨 이동 동안 유지할 저장 데이터

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

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

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

UI 버튼에 연결 (선택 사항)

메인 메뉴나 인게임 메뉴의 세이브/로드 버튼에 위에서 구현한 SaveGame과 LoadGame 함수를 연결합니다.

  • 버튼 On Clicked 이벤트 → Get Game Mode → 성공 분기를 확인한 Cast To GM_MyGame → SaveGame 또는 LoadGame 함수 호출.

세이브 시스템 설계 시 주요 고려사항

  • 저장할 데이터의 범위: 모든 것을 저장할 필요는 없습니다. 다시 생성하거나 쉽게 유추할 수 있는 데이터는 저장하지 않아도 됩니다. (예: 일회성 이펙트, 단기 계산 변수)
  • 복잡한 액터 저장: 저장용 ID, 클래스, Transform, 필요한 상태 값을 보관하고 복원합니다. 배치 액터에도 재실행·맵 수정 정책에 맞는 ID를 부여합니다. 표시 이름이나 런타임 고유 번호만으로 영구 식별을 가정하지 않습니다.
  • 로드 시점: 월드와 Pawn이 준비된 뒤 적용해야 합니다. 여러 액터의 BeginPlay 순서가 우연히 맞기를 기대하지 말고, 스폰 완료 지점에서 복원 함수를 호출합니다.
  • 성능: 너무 많은 데이터를 저장하거나, 너무 자주 저장하면 성능에 영향을 미칠 수 있습니다. 필요한 최소한의 데이터만 저장하고, 저장 빈도를 조절합니다.
  • 데이터 버전 관리: 게임 업데이트 시 세이브 파일의 구조가 변경될 수 있습니다. SaveGame 클래스에 SaveGameVersion (Int) 변수를 추가하여, 로드 시 버전 불일치를 감지하고 이전 버전 데이터를 변환하는 로직을 구현할 수 있습니다.
  • 보안: 일반 해시만으로 악의적 변조를 막을 수는 없습니다. 인증된 무결성 검증과 신뢰할 수 있는 키·서버 권한을 별도로 설계하며, 클라이언트의 로컬 파일을 완전히 신뢰하지 않습니다.
  • UI 피드백: 저장/로드 진행 중임을 플레이어에게 알려주는 UI(예: 저장 중..., 로딩 바)를 제공하여 사용자 경험을 개선합니다.

체크포인트와 세이브 시스템은 플레이어가 게임을 지속적으로 즐길 수 있도록 하는 필수적인 백본 시스템입니다.

잘 설계된 세이브 시스템은 플레이어의 몰입도를 높이고 게임의 만족도를 향상시킵니다.

세이브/로드 실패 증상과 복구

  • 증상: 저장 버튼을 눌러도 다음 실행에서 데이터가 사라짐
    • 원인: Save Slot Name 불일치 또는 Save Game To Slot 미호출
    • 복구: 저장/로드가 동일한 슬롯명을 쓰는지 확인하고, 저장 결과 불리언과 오류 경로를 기록해 성공 여부를 구분합니다.
  • 증상: 로드 직후 플레이어 위치가 원점으로 이동
    • 원인: Open Level 이후 복원 로직이 재실행되지 않음
    • 복구: 레벨 전환이 필요한 경우, 새 레벨의 Pawn 생성 완료 지점에서 PendingSave의 위치를 적용합니다.
  • 증상: 수집 아이템 상태가 복원되지 않음
    • 원인: 액터 참조 자체를 저장하려고 함
    • 복구: 액터 레퍼런스 대신 고유 ID 배열을 저장하고, 로드 시 ID 매칭으로 월드 상태를 재적용합니다.

이번 절에서는 언리얼 엔진에서 체크포인트와 세이브/로드 시스템을 설계하고 구현하는 기본 개념과 SaveGame 클래스 활용법에 대해 알아보았습니다.

게임의 진행 상황을 안정적으로 저장하고 불러오는 것은 플레이어에게 매우 중요한 기능입니다.