Rozwiązanie problemu z brakującą opcją Clear Player Data w Fortnite Creative: resety w Verse i odzyskiwanie stanu w niestandardowym backendzie
W skrócie
Artykuł omawia problem zniknięcia opcji „Clear Player Data” z konsoli publikacyjnej Epic w Fortnite Creative oraz przedstawia techniczne metody radzenia sobie z uszkodzeniami stanów graczy po zmianach schematów danych. Autorzy prezentują implementację systemu wersjonowania danych bezpośrednio w języku Verse jako lokalną metodę awaryjną oraz konfigurację edytora w celach testowych. Jako długofalowe i bardziej skalowalne rozwiązanie sugerują przeniesienie persystencji na zewnętrzną bazę danych zintegrowaną za pomocą REST API i usługi horizOn.
Twoja najnowsza mapa w Fortnite Creative lub UEFN jest już gotowa. Zrefaktoryzowałeś ekonomię, przeprojektowałeś system klas i zoptymalizowałeś gameplay loop. Jednak gdy przechodzisz do publikacji, napotykasz koszmar blokujący silnik: opcja „Press Triangle to Clear Player Data” zniknęła z ekranu potwierdzenia publikacji. Jesteś w pułapce. Gracze wchodzą na Twoją mapę ze starymi danymi persystencji, co powoduje natychmiastowe stack overflowy, zepsute linie zadań i błędy spójności stanu (state mismatches), które crashują całą grę.
Ten konkretny problem nęka twórców na forach deweloperskich Epic, gdzie nagły brak przycisku resetu uniemożliwia wyczyszczenie nieaktualnych stanów produkcyjnych z poziomu interfejsu użytkownika. Kiedy zmienia się schemat (schema) danych persystencji gracza, backend Epic próbuje zmapować stare zserializowane dane na Twoje nowe struktury. Jeśli Twój kod nie jest zaprojektowany tak, aby bezproblemowo obsługiwać te niezgodne właściwości, całkowicie zablokujesz (bricked) stan gry dla 100% powracających graczy.
W tym poradniku szczegółowo omówimy techniczne obejścia tego błędu. Pokażemy Ci, jak zbudować solidny system wersjonowania schematu (schema versioning) w Verse, skonfigurować lokalne ustawienia UEFN w celu czyszczenia stanów testowych oraz sprawdzimy, jak niestandardowy, zewnętrzny backend może uwolnić Cię od ograniczeń persystencji zamkniętej platformy.
The Root Cause: Dlaczego schematy (schemas) ulegają awarii przy aktualizacjach wersji
W Fortnite Creative i Unreal Editor for Fortnite (UEFN) trwałe dane są przechowywane w infrastrukturze bazodanowej Epic przy użyciu struktur weak_map. Konstrukcja weak_map wiąże instancję gracza z niestandardową klasą oznaczoną specyfikatorem <persistable>. W przeciwieństwie do tradycyjnych baz SQL lub dokumentowych, warstwa przechowywania danych Epic nie uruchamia formalnego procesu migracji (migration pipeline) po wydaniu aktualizacji. Zamiast tego próbuje zdeserializować dowolny binarny payload istniejący w zapisie chmurowym gracza do aktualnej definji klasy Verse.
Gdy usuniesz zmienną, zmienisz nazwę właściwości lub zmienisz typ (np. zmieniając pole z int na float), deserializator napotka niezgodność. W zależności od powagi zmiany, ta rozbieżność w schemacie prowadzi do jednego z trzech trybów awarii:
- Ciche awarie (Silent Failures): Środowisko uruchomieniowe przypisuje domyślne wartości do zmodyfikowanych właściwości, co i tak powoduje utratę postępu, ale bez wywoływania crashu.
- Uszkodzenie stanu (State Corruption): Aplikacja odczytuje nieprawidłowe stany pamięci lub napotyka nieoczekiwane struktury danych, co wyzwala błędy logiczne w innych miejscach.
- Poważne crashe środowiska uruchomieniowego (Severe Runtime Crashes): Mapa nie ładuje się lub maszyna wirtualna Verse (Verse VM) zatrzymuje wykonywanie, ponieważ nie może rozpakować trwałej mapy (persistent map).
Gdy przycisk „Clear Player Data” w konsoli wydawcy znika, tracisz możliwość globalnego wyczyszczenia danych i rozpoczęcia od nowa. Jeśli deweloper wprowadzi fundamentalne zmiany w układzie danych zapisu (save data layout), gra ulega uszkodzeniu.
The Manual Fix: Wersjonowanie schematu (Schema Versioning) wewnątrz Verse
Ponieważ nie możesz liczyć na to, że portal publikacyjny Epic wyczyści dane, musisz wdrożyć rozwiązanie programistyczne. Strategia jest prosta: wprowadź wskaźnik wersji (version tracker) bezpośrednio do swoich klas typu persistable. Porównując zapisaną wersję gracza z aktywną wersją aplikacji na początku sesji, możesz zidentyfikować nieaktualne zapisy i nadpisać je wartościami domyślnymi.
Oto gotowa do użycia produkcyjnego implementacja w Verse, która konfiguruje wersjonowaną warstwę persystencji.
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.")
Szczegółowe omówienie skryptu wersjonowania
Ten wzorzec opiera się na wczesnej walidacji wewnątrz metody InitializePlayer. Logika krok po kroku wygląda następująco:
- Wstępna weryfikacja (Entry Check): Gdy gracz dołącza, system odpytuje
PlayerDataMapo istniejącą instancjęplayer_save_data. - Ocena wersji (Version Assessment): Jeśli stan zapisu istnieje, silnik sprawdza jego właściwość
SchemaVersion. - Wyzwalacz migracji (Migration Trigger): Jeśli
SchemaVersionjest niższa niż właściwość@editableurządzeniaTargetSchemaVersion, skrypt wywołujeResetPlayerData(Player). - Nadpisanie stanu (State Overwrite):
ResetPlayerDatatworzy nową instancjęplayer_save_dataustawioną na nową wartośćTargetSchemaVersioni aktualizuje mapę.
Ręcznie zwiększając TargetSchemaVersion w konfiguracji urządzenia w UEFN, wymuszasz czyszczenie danych u wszystkich graczy przy ich kolejnej inicjalizacji sesji, co sprawia, że brakujący przycisk w UI od Epic przestaje mieć znaczenie.
Procedura testowa krok po kroku
Aby bezpiecznie zweryfikować ten skrypt migracyjny w cyklu testów rozgrywki (playtests), wykonaj następujące kroki:
- Przeciągnij swoje niestandardowe urządzenie
persistence_manager_devicedo rzutni poziomu (viewportu) w UEFN. - W panelu UEFN Details ustaw początkową wartość
TargetSchemaVersionna 1. - Uruchom sesję gry, zdobądź trochę punktów lub złota, a następnie zakończ grę.
- Po powrocie do edytora zaktualizuj pole
TargetSchemaVersionna urządzeniu z 1 na 2. - Uruchom sesję ponownie. Obserwuj dane wyjściowe w logach konsoli: 'Data version mismatch. Local: 1, Target: 2. Resetting player data.'
- Upewnij się, że złoto i postęp gracza zostały zresetowane bez wywołania crashu maszyny wirtualnej Verse (Verse VM).
Ustawienia lokalnego środowiska deweloperskiego: Omijanie plików zapisu podczas iteracji
Chociaż wersjonowanie kodu w czasie wykonywania (runtime) rozwiązuje problemy na serwerach produkcyjnych (live), dodaje niepożądane utrudnienia podczas lokalnych testów. Ręczne podbijanie numeru wersji w UEFN za każdym razem, gdy modyfikujesz rozwijaną funkcję, jest uciążliwe. Aby usprawnić proces debugowania, możesz całkowicie pominąć lokalną persystencję za pomocą ustawień edytora.
Wykonaj poniższe kroki, aby wyłączyć lokalną persystencję:
- W UEFN przejdź do Island Settings w panelu Outliner.
- Wyszukaj podsekcję User Options - Game Rules.
- Znajdź przełącznik Enable Persistence i go wyłącz.
- Jeśli testujesz lokalnie pętle multiplayer, otwórz Editor Preferences, przejdź do menu In-Game Play i znajdź pole wyboru Clear Local Saved Data On Launch. Zaznaczenie tej opcji gwarantuje, że każda symulacja rozpocznie się z czystym cachem.
Niemniej jednak lokalne obejścia nie symulują scenariuszy produkcyjnych. Choć przy wyłączonym cache'owaniu persystencji czas iteracji podczas testów może spaść z 45 sekund do poniżej 5 sekund, przed wdrożeniem na serwer produkcyjny (live) musisz uruchomić przynajmniej jeden pełny cykl testowy (staging cycle) z włączoną persystencją. To właśnie tutaj mogą nawarstwić się problemy z połączeniem. Lokalne środowiska testowe są znane z konfliktów sterowników, co sprawia, że deweloperzy tracą cenny czas na diagnozowanie sterowników sieciowych Unreal Engine w trakcie timeoutów uruchamiania sesji UEFN.
Ograniczenia wbudowanej persystencji w UEFN
Korzystanie z natywnej warstwy persystencji Verse wiąże się ze znacznymi ograniczeniami architektonicznymi. Nawet gdy opcje czyszczenia chmury działają bezbłędnie, deweloperzy są ograniczeni restrykcyjnymi limitami platformy:
- Storage Budgets: Bloki danych persistable są ograniczone do 12 KB na gracza na sesję. Przekroczenie tego limitu uniemożliwia zapisanie stanu.
- Tylko typy prymitywne (Primitive Types Only): Nie można serializować klas niestandardowych, które nie są oznaczone jako
<persistable>, ani przechowywać referencji do obiektów gry (takich jakcreative_deviceczy dynamiczne aktory). - Brak zewnętrznych zapytań (No External Querying): System Epic to kompletna czarna skrzynka (black box). Nie możesz odpytywać o dane graczy z poziomu zewnętrznego dashboardu, kontrolować stanów użytkowników pod kątem exploitów ani dynamicznie migrować baz danych.
- Telemetry Bottlenecks: Projektowanie złożonych dashboardów analitycznych w UEFN jest mocno ograniczone limitami długości ciągów znaków. Deweloperzy stale zmagają się z ograniczeniami, na przykład przy przełamywaniu 32-znakowego limitu nazwy zdarzenia urządzenia analitycznego w UEFN.
Dla prostych minigier te granice są akceptowalne. Jednak w przypadku złożonych, trwałych RPG, strzelanek typu live-ops czy tytułów międzyplatformowych, poleganie wyłącznie na własnościowej persystencji w zamkniętej pętli blokuje Twoją ścieżkę skalowania.
Alternatywa architektoniczna: Stany zewnętrznej bazy danych z horizOn
Aby zbudować skalowalną grę, która nie jest ograniczona błędami UI specyficznymi dla platformy ani limitami rozmiaru danych, deweloperzy zwracają się ku zewnętrznym architekturom backendowym. Zamiast przechowywać stany gry w lokalnych mapach pamięci Verse, Twój projekt UEFN może komunikować się z zewnętrzną bazą danych za pomocą modułu http_client w Verse.
Takie podejście daje Ci pełną kontrolę nad cyklem zapisu gracza. Kiedy musisz wyczyścić dane persystencji gracza, nie musisz szukać brakującego przycisku w konsoli Epic ani wdrażać aktualizacji kodu. Możesz po prostu uruchomić administracyjny skrypt bazodanowy lub kliknąć jeden przycisk w panelu sterowania backendem, aby wyczyścić, zmigrować lub załatać (patch) tabele graczy.
Samodzielne zbudowanie takiego rozwiązania wymaga skonfigurowania load balancerów, shardingu bazy danych i zarządzania certyfikatami SSL — to łatwo 4-6 tygodni pracy. Dzięki horizOn te usługi backendowe są wstępnie skonfigurowane, co pozwala Ci skupić się na wydaniu gry zamiast na infrastrukturze.
Przekierowując stany graczy przez horizOn, Twój kod Verse staje się prostym klientem, który odczytuje i zapisuje stan za pomocą przejrzystych endpointów REST:
# Pseudocode showing HTTP-based state synchronization with [horizOn](https://horizon.pm)
sync_manager_device := class(creative_device):
# Send player progress to [horizOn](https://horizon.pm) endpoint
SavePlayerState(Player : player, SaveData : player_save_data) : void =
# In a real environment, you construct a JSON payload and dispatch it
# via the Verse http_client. This bypasses the 12KB local persistence limit.
RequestURL := "https://api.horizon.pm/v1/players/{GetPlayerID(Player)}/state"
Print("Dispatching persistent payload to [horizOn](https://horizon.pm) at: {RequestURL}")
# This keeps the server-side payload lightweight (under 240 bytes)
# while securing long-term storage off the Fortnite engine.
Taka konfiguracja całkowicie oddziela Twój gameplay loop od fizycznych błędów backendu platformy. Jeśli patch naruszy kompatybilność lokalnych zapisów, możesz uruchomić precyzyjną migrację schematu w bazie danych horizOn w czasie rzeczywistym, bez jakichkolwiek zakłóceń działania aktywnego serwera gry. Unikasz w ten sposób ryzyka zepsucia działających serwerów (live servers) z powodu drobnych zmian strukturalnych.
Najlepsze praktyki (Best Practices) zarządzania persystencją stanu gry
Niezależnie od tego, czy pozostajesz przy natywnych mapach Verse, czy korzystasz z zewnętrznej architektury serwerowej, utrzymanie stabilnego cyklu życia persystencji jest kluczowe dla gier typu multiplayer. Przestrzegaj poniższych zasad, aby chronić stany graczy:
- Wdróż niejawne wersjonowanie na wczesnym etapie (Implement Implicit Versioning Early): Zawsze uwzględniaj liczbę całkowitą
SchemaVersionw początkowym układzie danych zapisu. Nawet jeśli nie planujesz modyfikować schematu, posiadanie klucza wersji od pierwszego dnia zapobiega katastrofalnemu uszkodzeniu danych w przyszłości. - Wymuszaj zasady walidacji stanu (Enforce State Validation Rules): Podczas odczytu załadowanego stanu sprawdzaj zakresy każdego atrybutu. Jeśli zapisana liczba złota gracza jest ujemna lub przekracza limit strukturalny Twojej mapy, zresetuj tę konkretną właściwość, aby zapobiec exploitom ekonomii.
- Minimalizuj częstotliwość zapisu (Minimize Save Frequency): Zapisywanie w chmurze jest kosztowne. Zamiast aktualizować bazę danych przy każdym podniesieniu monety, grupuj (batch) operacje zapisu. Wyzwalaj zapis w kluczowych momentach (milestones), takich jak ukończenie meczu, awans gracza na wyższy poziom lub gdy gracz opuszcza sesję.
- Twórz czyste struktury awaryjne (Construct Clean Fallback Structs): Nigdy nie pozwól, aby nieudany odczyt uniemożliwił graczowi wejście do gry. Jeśli deserializacja payloadu persystencji nie powiedzie się, natychmiast przywróć domyślną strukturę stanu (fallback).
- Oddziel kod rozgrywki od logiki przechowywania (Decouple Gameplay Code from Storage Logic): Przechowuj zapytania dotyczące zapisu w odizolowanym, dedykowanym urządzeniu zarządzającym persystencją. Twoje urządzenia obsługujące broń, progresję i interfejs użytkownika powinny odpytywać ten manager zamiast wykonywać bezpośrednie połączenia do mapy danych.
Kolejne kroki
Poleganie na interfejsie konsoli Epic w celu zarządzania krytycznymi migracjami stanu to ryzykowny wzorzec projektowy. Kiedy zawodzi opcja „Clear Player Data”, wersjonowanie w czasie wykonywania wewnątrz Verse służy jako pierwsza linia obrony. Jeśli jednak Twoja gra wymaga częstych zapisów, złożonej telemetryki lub pełnej kontroli administracyjnej, ostatecznym rozwiązaniem jest migracja bazy danych do dedykowanego backendu.
Gotowy na skalowanie swojego backendu multiplayer? Wypróbuj horizOn za darmo lub zapoznaj się z dokumentacją API.