Fortnite Creativeの「Clear Player Data」オプション消失の修正:VerseによるリセットとカスタムBackendでのステート復旧
要点まとめ
Fortnite CreativeおよびUEFNのパブリッシュ画面から「Clear Player Data」ボタンが消失する問題の原因と、その技術的対策を解説しています。本記事では、Verseを用いたセーブデータのスキーマバージョニングによる手動データリセットの実装方法や、デバッグを加速するローカルテスト時の設定について詳述します。さらに、独自データのクエリや12KB制限といった標準機能の限界を打破し、ゲームステートの管理を完全にコントロールするための[horizOn](https://horizon.pm)を利用した外部Backend接続という選択肢についても提示しています。
最新のFortnite CreativeまたはUEFNのマップが完成しました。エコノミーのリファクタリング、クラスシステムの再設計、ゲームプレイループの最適化も完了しています。しかし、いざパブリッシュしようとしたその時、エンジンを破壊しかねない悪夢に直面します。パブリッシュ確認画面から「Press Triangle to Clear Player Data(三角ボタンを押してプレイヤーデータをクリア)」のオプションが消失しているのです。あなたは完全に手詰まりになります。プレイヤーは古いpersistenceデータのままマップにロードされ、即座にスタックオーバーフローやクエストラインの破損、そしてゲームをクラッシュさせるステートのミスマッチが発生します。
この問題はEpicのデベロッパーフォーラムでも多くのクリエイターを悩ませており、リセットボタンが突如消えたことで、古くなった本番環境のステートをパージするためのUI上の手段が失われてしまいます。プレイヤーのpersistenceデータのスキーマが変更されると、EpicのBackendは古いシリアライズされたデータを新しい構造にマッピングしようと試みます。もしコードがこれらのミスマッチしたプロパティを適切に処理するように設計されていない場合、戻ってきたプレイヤーの100%に対してゲーム状態を完全に破損させることになります。
このチュートリアルでは、このバグに対する技術的な回避策を深く掘り下げます。Verseで頑丈なスキーマバージョニングシステムを構築する方法、テスト状態をクリアするためのローカルのUEFN設定、そしてクローズドなプラットフォームのpersistenceの制限から解放してくれるカスタムの外部Backendの活用方法について解説します。
根本原因:バージョンアップ時にスキーマが機能しなくなる理由
Fortnite CreativeおよびUnreal Editor for Fortnite(UEFN)では、永続データはweak_map構造を使用してEpicのデータベースインフラストラクチャに保存されます。weak_mapは、playerインスタンスを<persistable>指定子が付与されたカスタムクラスにリンクします。従来のSQLやドキュメントデータベースとは異なり、Epicのストレージレイヤーはアップデートのリリース時に正式なマイグレーションパイプラインを実行しません。代わりに、プレイヤーのクラウドセーブ内に存在するバイナリペイロードを現在のVerseクラス定義へと強引にデシリアライズしようとします。
変数の削除、プロパティのリネーム、あるいは型の変更(例: フィールドをintからfloatに変更するなど)を行うと、デシリアライザはミスマッチを検出します。変更の深刻度に応じて、このスキーマの乖離は次の3つのフェイルモードのいずれかを引き起こします。
- サイレントエラー(Silent Failures): ランタイムは変更されたプロパティにデフォルト値を割り当て、クラッシュさせることなく(ただし進行状況は消去した状態で)動作します。
- ステートの破損(State Corruption): アプリケーションが無効なメモリ状態を読み込むか、予期しないデータ構造に遭遇し、他の場所でロジックエラーを引き起こします。
- 深刻なランタイムクラッシュ(Severe Runtime Crashes): マップのロードに失敗するか、Verse VMが永続マップを展開できないために実行を停止します。
パブリッシャーコンソールの「Clear Player Data」ボタンが消失すると、グローバルにデータを完全に初期化する手段が失われます。デベロッパーがセーブデータのレイアウトに根本的な変更を加えた場合、ゲームは動作しなくなってしまいます。
手動での修正:Verse内部でのスキーマバージョニング
Epicのパブリッシングポータルでデータをクリアすることに頼れない以上、ソフトウェアレベルでのフォールバックを実装する必要があります。戦略はシンプルです。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メソッド内での早期バリデーションに依存しています。具体的なステップは以下の通りです。
- 進入時のチェック(The Entry Check): プレイヤーが参加した際、システムは
PlayerDataMapをクエリしてplayer_save_dataの既存のインスタンスがあるか確認します。 - バージョン評価(The Version Assessment): セーブステートが存在する場合、エンジンはその
SchemaVersionプロパティを検証します。 - マイグレーションのトリガー(The Migration Trigger): もし
SchemaVersionがデバイスの@editableプロパティであるTargetSchemaVersionよりも低い場合、スクリプトはResetPlayerData(Player)を呼び出します。 - ステートの上書き(State Overwrite):
ResetPlayerDataは、新しいTargetSchemaVersionが設定されたplayer_save_dataの新しいインスタンスを構築し、マップを更新します。
UEFN内のデバイス設定でTargetSchemaVersionを手動でインクリメントすることにより、プレイヤーの次回セッション初期化時に強制的にデータをワイプ(初期化)できます。これにより、消失したEpicのUIボタンの問題を回避できます。
ステップバイステップのテスト手順
プレイテストのサイクル内でこのマイグレーションスクリプトを安全に検証するには、以下の手順を実行してください。
- カスタムの
persistence_manager_deviceをUEFNのレベルビューポートにドラッグ&ドロップします。 - UEFNのDetails panelで、初期の
TargetSchemaVersionを1に設定します。 - プレイセッションを開始し、スコアやゴールドを適当に獲得したあと、ゲームを終了します。
- エディタに戻り、デバイス上の
TargetSchemaVersionフィールドを1から2に更新します。 - セッションを再度開始します。コンソールログの出力に「Data version mismatch. Local: 1, Target: 2. Resetting player data.」と表示されていることを確認します。
- Verse VMをクラッシュさせることなく、プレイヤーのゴールドと進行状況が正常にリセットされていることを確認します。
ローカル開発設定:イテレーション中のセーブファイルのバイパス
ランタイムでのコードによるバージョニングは本番サーバーでの問題を解決しますが、ローカルでのプレイテスト中には余計な摩擦を生みます。開発中の機能を修正するたびに、UEFN内で手動でバージョン番号を上げるのは面倒です。デバッグプロセスを効率化するために、エディタ設定からローカルのpersistenceを完全にバイパスできます。
ローカルのpersistenceを無効化するには、以下の手順に従います。
- UEFNで、Outliner panel内のIsland Settingsに移動します。
- User Options - Game Rulesサブセクションを検索します。
- Enable Persistenceのトグルを見つけ、無効(Disabled)にします。
- ローカルでMultiplayerループをテストしている場合は、Editor Preferencesを開き、In-Game Playメニューに移動し、Clear Local Saved Data On Launchチェックボックスをオンにします。これをチェックすることで、各シミュレーションが常に空のキャッシュで開始されるようになります。
ただし、ローカルでのバイパスは本番環境のシナリオをシミュレートしていません。persistenceのキャッシュを無効にすることで、テストのイテレーション速度を45秒から5秒未満に短縮できますが、リリース前には必ずpersistenceを有効にした状態で少なくとも1回はフルステージングサイクルを実行する必要があります。そして、ここで接続に関するハードルが重なることがあります。ローカルのプレイテスト環境はドライバーの競合が発生しやすいことで有名であり、開発者はUEFNセッション起動タイムアウト時のUnreal Engineネットワークドライバー診断に貴重な時間を費やすことになるケースが多々あります。
UEFNに組み込まれたPersistenceの制限事項
Verseのネイティブなpersistenceレイヤーの使用には、いくつかの大きなアーキテクチャ上の制約が伴います。クラウドクリアのオプションが完璧に機能している場合であっても、開発者はプラットフォーム固有の厳しい制限に拘束されます。
- ストレージ容量の制限(Storage Budgets): 永続化データブロックは、セッションごとのplayerあたり最大12KBに制限されています。この制限を超えると、状態を保存できなくなります。
- プリミティブ型のみサポート:
<persistable>マークのないカスタムクラスはシリアライズできず、ゲームオブジェクト(creative_deviceや動的アクターなど)への参照を保存することもできません。 - 外部からのクエリ不可: Epicのシステムは完全なブラックボックスです。外部のダッシュボードからplayerデータをクエリしたり、不正行為対策のためにユーザーステートを監査したり、データベースを動的にマイグレーションしたりすることはできません。
- テレメトリのボトルネック(Telemetry Bottlenecks): UEFN内で複雑な分析ダッシュボードを設計することは、文字列の長さ制限によって厳しく制限(スロットリング)されます。例えば、開発者はUEFNアナリティクスデバイスにおける32文字のイベント名制限の突破に常に苦労しています。
小規模なミニゲームであればこれらの制限の範囲内でも対応可能ですが、複雑な永続RPGやライブサービス系のシューター、あるいはマルチプラットフォーム対応のタイトルなどでは、独自のクローズドなpersistenceだけに依存していると将来的なスケールアップの妨げになります。
アーキテクトの代替案:horizOnによる外部データベースステートの運用
プラットフォーム特有のUIバグや独自のデータサイズ制限に縛られない、スケーラブルなゲームを開発するために、デベロッパーは外部のBackendアーキテクチャへと移行しつつあります。Verseのローカルメモリマップにゲームの状態を保存する代わりに、UEFNプロジェクトからVerseのhttp_clientモジュールを使用して外部データベースと通信させることができます。
このアプローチにより、プレイヤーのセーブサイクルを完全にコントロールできるようになります。プレイヤーのpersistenceデータをクリアしたい場合、Epicコンソール上で見つからないボタンを探し回ったり、コードアップデートを配信したりする必要はありません。管理用のデータベーススクリプトを実行するか、Backendのコントロールパネルでボタンを1回クリックするだけで、プレイヤーテーブルのクリア、マイグレーション、パッチ適用が可能です。
これを自前で構築するには、load balancersのセットアップ、database sharding、SSL証明書管理などが必要になり、優に4〜6週間はかかります。しかしhorizOnを利用すれば、これらのBackendサービスがあらかじめ事前設定されているため、インフラの構築ではなく、ゲーム自体の開発とリリースに集中できます。
プレイヤーのステートをhorizOn経由でルーティングすることで、VerseコードはクリーンなREST endpointsを介してステートを読み書きするシンプルなクライアントになります。
# 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.
この構成により、プラットフォームの物理的なBackendバグからゲームプレイループが完全に切り離されます。万が一パッチによってローカルセーブの互換性が損なわれたとしても、稼働中のゲームサーバーに一切影響を与えることなく、リアルタイムにhorizOnデータベース上で対象のschema migrationをトリガーできます。軽微な構造変更によって本番サーバーをクラッシュさせるリスクを回避可能です。
ゲームステートのPersistence管理におけるベストプラクティス
ネイティブのVerseマップを使用する場合でも、外部のサーバーアーキテクチャを活用する場合でも、Multiplayerゲームにおいて安定したpersistenceのライフサイクルを維持することは不可欠です。プレイヤーのステートを保護するために、以下の原則に従ってください。
- 初期段階での暗示的バージョニングの実装(Implement Implicit Versioning Early): 初期のセーブデータレイアウトには、必ず
SchemaVersion整数型を含めておいてください。将来的にスキーマを変更する予定がなくても、初日からバージョンキーを用意しておくことで、将来の致命的なデータ破損を防ぐことができます。 - ステート検証ルールの適用(Enforce State Validation Rules): ロードされたステートを読み込む際は、すべての属性の範囲を検証してください。もしプレイヤーの保存されたゴールド獲得数が負の値であったり、マップの構造的な上限を超えていたりする場合は、エコノミーの悪用(グリッチ)を防ぐためにそのプロパティをリセットします。
- セーブ頻度の最小化(Minimize Save Frequency): クラウドへの書き込み処理は高コストです。コインを拾うたびにデータベースを更新するのではなく、書き込みをバッチ処理してください。マッチ完了、プレイヤーのレベルアップ、プレイヤーがセッションから退出したときなどの重要なマイルストーンでセーブを実行します。
- クリーンなフォールバック構造の構築(Construct Clean Fallback Structs): 読み込みエラーによってプレイヤーのゲーム参加が妨げられないようにしてください。persistenceペイロードのデシリアライズに失敗した場合は、即座にデフォルトのステート構造へとフォールバックします。
- ストレージロジックからのゲームプレイコードの分離(Decouple Gameplay Code from Storage Logic): ストレージのクエリ処理は、専用の永続化管理用デバイス(persistence manager device)に隔離してください。武器、進行状況、およびUIのデバイスは、データマップに直接呼び出すのではなく、このマネージャーをクエリするようにします。
次のステップ
重要なステートのマイグレーション管理においてEpicのコンソールUIに依存することは、リスクの高い設計パターンです。「Clear Player Data」オプションが機能しない場合、Verse内部でのランタイムバージョニングが第一の防衛線となります。しかし、より高頻度なセーブ、複雑なtelemetry、あるいは完全な管理者権限による制御がゲームに求められる場合、専用のBackendへのデータベース移行が究極の解決策となります。
MultiplayerのBackendをスケーリングさせる準備はできましたか?無料でhorizOnを試すか、API docsをご覧ください。