누락된 Fortnite Creative Clear Player Data 옵션 해결하기: Verse 리셋 및 커스텀 Backend 상태 복구
핵심 요약
Fortnite Creative 및 UEFN에서 퍼블리싱 시 'Clear Player Data' 옵션이 사라져 발생하는 스키마 불일치와 게임 크래시 문제를 해결하기 위한 기술적 가이드입니다. Verse 내부에서 스키마 버저닝을 구현하여 플레이어 접속 시 오래된 세이브 데이터를 감지하고 리셋하는 소프트웨어 방식의 폴백 해결법을 단계별 코드로 제공합니다. 또한 로컬 테스트 시 persistence를 우회하는 에디터 설정법과 내장 기능의 한계를 설명하며, [horizOn](https://horizon.pm)과 같은 외부 Backend 데이터베이스를 활용한 안정적인 상태 관리 및 실시간 마이그레이션 방안을 제시합니다.
최신 Fortnite Creative 또는 UEFN 맵이 준비되었습니다. 이코노미를 리팩터링하고, 클래스 시스템을 재설계했으며, 게임플레이 루프를 최적화했습니다. 하지만 출시하려는 순간, 엔진을 마비시키는 악몽에 직면하게 됩니다. 퍼블리싱 확인 화면에서 "Press Triangle to Clear Player Data" 옵션이 사라진 것입니다. 갇혀버린 셈이죠. 플레이어들이 오래된 persistence data를 가진 채 맵에 진입하면서 즉각적인 stack overflow, 손상된 퀘스트 라인, 그리고 게임을 크래시시키는 상태 불일치(state mismatch)가 발생합니다.
이 정확한 문제는 Epic의 개발자 포럼에서 크리에이터들을 오랫동안 괴롭혀 왔습니다. 리셋 버튼이 갑자기 사라지면서 오래된 프로덕션 상태를 퍼지(purge)할 UI 기반 메커니즘이 남지 않게 된 것입니다. 플레이어의 persistence data 스키마(schema)가 변경되면, Epic의 Backend는 이전 직렬화된(serialized) 데이터를 새 구조에 매핑하려고 시도합니다. 만약 코드가 이러한 불일치 프로퍼티를 매끄럽게 처리하도록 설계되지 않았다면, 복귀하는 플레이어 100%의 게임 상태가 완전히 망가지는(brick) 현상이 발생합니다.
본 튜토리얼에서는 이 버그에 대한 기술적 우회 방법을 깊이 있게 다룹니다. Verse에서 강력한 스키마 버저닝(schema versioning) 시스템을 구축하는 방법, 로컬 UEFN 설정에서 테스트 상태를 지우도록 구성하는 방법, 그리고 커스텀 외부 Backend가 어떻게 폐쇄형 플랫폼의 persistence 한계에서 여러분을 해방시켜 줄 수 있는지 살펴보겠습니다.
근본 원인: 버전 업그레이드 시 스키마가 실패하는 이유
Fortnite Creative 및 UEFN(Unreal Editor for Fortnite)에서 persistent data는 weak_map 구조를 사용하여 Epic의 데이터베이스 인프라에 저장됩니다. weak_map은 플레이어 인스턴스를 <persistable> 지정자가 표시된 커스텀 클래스에 연결합니다. 전통적인 SQL이나 문서 데이터베이스와 달리, Epic의 스토리지 레이어는 업데이트를 릴리스할 때 정식 마이그레이션 파이프라인을 실행하지 않습니다. 대신 플레이어의 클라우드 세이브에 있는 바이너리 페이로드(binary payload)를 현재 Verse 클래스 정의로 역직렬화(deserialize)하려고 시도합니다.
변수를 삭제하거나, 프로퍼티 이름을 바꾸거나, 타입을 변경하는 경우(예: 필드를 int에서 float로 변경) 역직렬화기(deserializer)에서 불일치가 발생합니다. 변경의 심각도에 따라 이러한 스키마 분기(schema divergence)는 다음 세 가지 오류 모드 중 하나를 유발합니다.
- Silent Failures (소리 없는 실패): 런타임이 변경된 프로퍼티에 기본값을 할당하여 크래시는 발생하지 않지만 진행 상황이 초기화됩니다.
- State Corruption (상태 손상): 애플리케이션이 잘못된 메모리 상태를 읽거나 예상치 못한 데이터 구조를 만나 다른 곳에서 로직 에러가 발생합니다.
- Severe Runtime Crashes (심각한 런타임 크래시): 맵을 로드하지 못하거나, Verse VM이 persistent map의 패키지를 풀 수 없어 실행을 중단합니다.
퍼블리셔 콘솔의 "Clear Player Data" 버튼이 사라지면, 글로벌하게 데이터를 모두 지울 수 있는 능력을 잃게 됩니다. 개발자가 세이브 데이터 레이아웃을 근본적으로 변경하면 게임이 중단됩니다.
수동 해결법: Verse 내부의 스키마 버저닝(Schema Versioning)
데이터를 지우기 위해 Epic 퍼블리싱 포털에만 의존할 수 없으므로, 소프트웨어 수준의 폴백(fallback)을 구현해야 합니다. 전략은 간단합니다. persistable 클래스에 직접 버전 트래커를 도입하는 것입니다. 세션 시작 시 플레이어의 저장된 버전과 활성화된 애플리케이션 버전을 비교하여 오래된 세이브를 식별하고 기본값으로 덮어쓸 수 있습니다.
다음은 버전 제어가 적용된 persistence 레이어를 구축하는 프로덕션 지원 Verse 구현체입니다.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Simulation }
# A simple persistable class representing our player save schema.
# Note: Classes tagged with <persistable> can only contain certain types like int, float, string, and maps.
player_save_data := class<persistable>:
# The SchemaVersion tracks which generation of player data this struct belongs to.
SchemaVersion : int = 0
GoldCount : int = 0
XP : int = 0
HasCompletedTutorial : int = 0 # Using int as a boolean flag representation for older Verse constraints
# The persistence manager device handles loading, validating, and migrating player data.
persistence_manager_device := class(creative_device):
# By updating this value in the Editor, we force a schema migration next time the game runs.
@editable
TargetSchemaVersion : int = 2
# Map to store player data in memory
var PlayerDataMap : weak_map(player, player_save_data) = map{}
# Initialize the persistence logic for a joining player
InitializePlayer(Player : player) : void =
# Check if the player already has persistent data
if (ExistingData := PlayerDataMap[Player]):
# If the stored version is older than our target version, trigger a manual reset
if (ExistingData.SchemaVersion < TargetSchemaVersion):
Print("Data version mismatch. Local: {ExistingData.SchemaVersion}, Target: {TargetSchemaVersion}. Resetting player data.")
ResetPlayerData(Player)
else:
Print("Loaded existing player data. Version: {ExistingData.SchemaVersion}")
else:
# If no data exists, initialize a new record with the current version
Print("No player data found. Initializing new record.")
ResetPlayerData(Player)
# Re-initializes a player's record with default values at the current version
ResetPlayerData(Player : player) : void =
NewData := player_save_data:
SchemaVersion := TargetSchemaVersion
GoldCount := 0
XP := 0
HasCompletedTutorial := 0
# Save the freshly initialized data block to the persistence map
if (set PlayerDataMap[Player] = NewData):
Print("Successfully saved fresh persistent data block.")
버저닝 스크립트 상세 분석
이 패턴은 InitializePlayer 메서드 내부의 조기 유효성 검사(early validation)에 의존합니다. 단계별 로직은 다음과 같이 진행됩니다.
- 진입 확인 (Entry Check): 플레이어가 참가하면 시스템은
PlayerDataMap에서 기존player_save_data인스턴스를 쿼리합니다. - 버전 평가 (Version Assessment): 세이브 상태가 존재하면 엔진은 해당
SchemaVersion프로퍼티를 확인합니다. - 마이그레이션 트리거 (Migration Trigger):
SchemaVersion이 디바이스의@editable프로퍼티인TargetSchemaVersion보다 낮으면 스크립트는ResetPlayerData(Player)를 호출합니다. - 상태 덮어쓰기 (State Overwrite):
ResetPlayerData는 새TargetSchemaVersion으로 설정된player_save_data인스턴스를 새로 생성하고 맵을 업데이트합니다.
UEFN 내의 디바이스 구성에서 TargetSchemaVersion을 수동으로 증가시킴으로써 다음 세션 초기화 시 모든 플레이어의 대상 데이터를 강제로 지울 수 있으며, 사라진 Epic UI 버튼의 유무와 상관없이 문제를 해결할 수 있습니다.
단계별 테스트 절차
- 커스텀
persistence_manager_device를 UEFN 레벨 뷰포트로 드래그합니다. - UEFN Details 패널에서 초기
TargetSchemaVersion을 1로 설정합니다. - 플레이 세션을 시작하고 점수나 골드를 획득한 다음 게임을 종료합니다.
- 에디터로 돌아와 디바이스의
TargetSchemaVersion필드를 1에서 2로 업데이트합니다. - 세션을 다시 시작합니다. 콘솔 로그 출력을 확인합니다: 'Data version mismatch. Local: 1, Target: 2. Resetting player data.'
- 플레이어의 골드와 진행 상황이 Verse VM을 크래시시키지 않고 깔끔하게 리셋되었는지 확인합니다.
로컬 개발 설정: 반복 작업 중 세이브 파일 우회하기
런타임 코드 버저닝은 라이브 서버의 문제를 해결하지만, 로컬 플레이테스트 중에 원치 않는 마찰을 추가합니다. 진행 중인 기능을 수정할 때마다 UEFN 내부에서 수동으로 버전 번호를 올리는 것은 번거로운 일입니다. 디버깅 프로세스를 간소화하기 위해 에디터 설정을 통해 로컬 persistence를 완전히 우회할 수 있습니다.
로컬 persistence를 비활성화하려면 다음 단계를 따러세요.
- UEFN의 Outliner 패널에서 Island Settings로 이동합니다.
- User Options - Game Rules 하위 섹션을 검색합니다.
- Enable Persistence 토글을 찾아 비활성화합니다.
- 로컬에서 Multiplayer 루프를 테스트하는 경우, Editor Preferences를 열고 In-Game Play 메뉴로 이동하여 Clear Local Saved Data On Launch 체크박스를 찾습니다. 이를 체크하면 각 시뮬레이션이 빈 캐시로 시작됩니다.
UEFN 내장 Persistence의 한계
Verse의 내장 persistence 레이어를 사용하는 데는 상당한 아키텍처 제약이 따릅니다. 클라우드 초기화 옵션이 완벽하게 작동하더라도 개발자는 엄격한 플랫폼 제한을 준수해야 합니다.
일반 미니게임의 경우 이러한 한계는 감당할 수 있는 수준입니다. 그러나 복잡한 persistent RPG, 라이브 서비스 슈팅 게임 또는 크로스 플랫폼 타이틀의 경우 독점적이고 폐쇄적인 persistence 블록에만 의존하는 것은 확장성을 가로막는 장애물이 됩니다.
# This keeps the server-side payload lightweight (under 240 bytes)
# while securing long-term storage off the Fortnite engine.
## 게임 상태 Persistence 관리를 위한 모범 사례
네이티브 Verse 맵을 사용하든 외부 서버 아키텍처를 활용하든, 안정적인 persistence 라이프사이클을 유지하는 것은 multiplayer 게임에 필수적입니다. 플레이어 상태를 보호하기 위해 다음 원칙을 준수하세요.
1. **초기에 암시적 버저닝 구현 (Implement Implicit Versioning Early):** 초기 세이브 데이터 레이아웃에 항상 `SchemaVersion` 정수를 포함하세요. 스키마를 수정할 계획이 없더라도, 첫날부터 버전 키를 확보해 두면 향후 치명적인 데이터 손상을 방지할 수 있습니다.
2. **상태 유효성 검사 규칙 적용 (Enforce State Validation Rules):** 로드된 상태를 읽을 때 모든 속성의 범위를 유효성 검사하세요. 플레이어의 저장된 골드 수가 음수이거나 맵의 구조적 한도를 초과하는 경우, 해당 프로퍼티를 리셋하여 이코노미 어뷰징을 방지하세요.
3. **저장 빈도 최소화 (Minimize Save Frequency):** 클라우드에 쓰는 작업은 비용이 많이 듭니다. 코인을 주울 때마다 데이터베이스를 업데이트하는 대신, 쓰기 작업을 배치(batch) 처리하세요. 매치 완료, 플레이어 레벨업 또는 플레이어가 세션을 종료할 때와 같은 주요 마일스톤에 저장을 트리거하세요.
4. **깔끔한 폴백 구조체 구축 (Construct Clean Fallback Structs):** 읽기 실패로 인해 플레이어의 게임 진입이 막히는 일이 없도록 하세요. persistence 페이로드 역직렬화에 실패하면 즉시 기본 상태 구조체로 폴백하도록 설계해야 합니다.
5. **게임플레이 코드와 스토리지 로직 분리 (Decouple Gameplay Code from Storage Logic):** 스토리지 쿼리를 전용 persistence 매니저 디바이스에 격리하세요. 무기, 진척도(progression), UI 디바이스는 데이터 맵에 직접 호출하는 대신 이 매니저를 쿼리해야 합니다.
## 다음 단계
중요한 상태 마이그레이션을 처리하기 위해 Epic의 콘솔 UI에만 의존하는 것은 위험한 디자인 패턴입니다. "Clear Player Data" 옵션이 작동하지 않을 때, Verse 내부의 런타임 버저닝은 첫 번째 방어선 역할을 합니다. 하지만 게임에 고주파 저장, 복잡한 텔레메트리(telemetry) 또는 완전한 관리자 제어 권한이 필요하다면, 데이터베이스를 전용 Backend로 마이그레이션하는 것이 궁극적인 해결책입니다.
---
출처: [clear players data button in creative 1](https://forums.unrealengine.com/t/clear-players-data-button-in-creative-1/2735737)