背景
Roblox は、DataStoreService を通じてデータストアとインタラクトするセットの API を提供します。これらの API の最も一般的な使用例は、「player のデータを保存、読み込み、レプリケートする」ことです。つまり、プレイセッション間の進行状況、購入、その他のセッション特性に関連す
Roblox のほとんどのエクスペリエンスは、これらの API を使用してプレイヤーのデータシステムのいくつかの形式を実装します。これらの実装はアプローチが異なりますが、一般的に同じセットの問題を解決しようとします。
一般的な問題
以下は、プレイヤーデータシステムが解決しようとしている問題の一部です:
In Memory Access: DataStoreService リクエストは、非同期であり、レート制限を超えるウェブ要求を実行します。これは、セッション開始時の初期読み取りに適していますが、ゲームプレイの通常の読み書きオペレーションには適してい
- セッションの開始時の初期読み取り
- セッションの終了に対する最終書き
- 期間的に書き込みを間隔して、最終書き込みが失敗するシナリオを軽減する
- 購入を処理するときにデータを保存するために書き込まれます
効率的なストレージ: すべてのプレイヤーのセッションデータを 1つの テーブルに保存すると、複数の値をアトミックに更新し、多くのリクエストで同じデータを処理できます。また、インターバルで間違いのないロールバックを理解しやすくします。
一部の開発者は、大規模なデータ構造を圧縮するためにカスタムなシリアル化を実装します (通常、ゲーム内のユーザー生成コンテンツを保存するため)。
レプリケーション: クライアントは、プレイヤーのデータに正常にアクセスする必要があります (たとえば、UI を更新するため)。クライアントにプレイヤーデータをレプリケートするためのユーザー指定のレプリケーションシステムを作成する必要はありません。開発者は通常、レプリケーションす
エラー処理: データストアにアクセスできない場合は、ほとんどのソリューションは再試行メカニズムとデフォルトのデータに対するフォールバックを実装します。特に、フォールバックのデータが「リアル」データを上書きしないようにするためには、フォールバックのデータが「プレイヤーに正しく通信される」ことを確認する必要があ
再試行: データストアがアクセス不可能になった場合、多くのソリューションは再試行メカニズムとデフォルトのデータに落とし子を実装します。特に、落とし子データが「実際の」データを上書きしないように注意し、プレイヤーに状況を適切に通知することをお勧めします。
セッションロック: 1人のプレイヤーのデータが複数のサーバーでインメモリにロードされている場合、1つのサーバーでデータが古くなっているため、問題が発生する可能性があります。これにより、データが失われ、一般的なアイテムの複製がある可能性があります。
アトミック購入処理: 購入をアトミックに検証、報酬を記録して、アイテムを失われたり、複数回アワードされたりすることを防止します。
サンプルコード
Roblox には、プレイヤーデータシステムの設計と構築に役立つ参照コードがあります。このページの残りは、背景、実装詳細、および一般的な caveats について説明します。
モデルをStudio にインポートした後、次のフォルダ構造を見る必要があります:
アーキテクチャ
このレベルの高い図は、サンプル内のキーシステムと、エクスペリエンスの残りの部分でコードとどのようにインターフェースするかを示しています。
再試行
クラス: DataStoreWrapper >
背景
Class.DataStoreService は、DataStore メソッドを呼び出して、ウェブ要求をフードで実行するため、その要求は成功することは保証されていません。このような状況では、Class.GlobalDataStore|DataStore メソッドがエラーをスローし、1>Class.GlobalDataStore|DataStore1> メソッドを使用して処理できます。
データストア失敗のようなことを処理しようとした場合、「gotcha」が発生する可能性があります:
local function retrySetAsync(dataStore, key, value)
for _ = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
break
end
task.wait(TIME_BETWEEN_ATTEMPTS)
end
end
これは、ジェネリック関数の完全に有効な再試行メカニズムですが、DataStoreService のリクエストには適用されません。リクエストの順序を保証しないため、DataStoreService のリクエストは、Class.DataStoreService リクエスト と同じ順序で実行されません。次のス
- リクエスト A は、キーの値を 1 に設定するために行われます。
- リクエストに失敗したため、2秒後に再試行がスケジュールされています。
- 再試行が発生する前に、リクエストBはK を2に設定しますが、リクエストAの再試行は、すぐにこの値を上書きし、K を1に設定します。
Class.GlobalDataStore:UpdateAsync()|UpdateAsync は、キーの値の最新バージョンで動作しますが、UpdateAsync はまだキーの値の処理を完了する必要があります。これにより、無効なトランジェント状態 (例: コインがコイン追加される前にコインを減少させるなど) が
私たちのプレイヤーデータシステムは、新しいクラス、 DataStoreWrapper を使用しています。これは、キーごとに処理される保証された再試行を提供します。
アプローチ
DataStoreWrapper は、 Class.GlobalDataStore|DataStore メソッドに対応するメソッドを提供します: Class.GlobalDataStore:GetAsync()|DataStore:GetAsync()、0> Class.GlobalDataStore:SetAsync()
これらのメソッド、呼び出されるとき:
リクエストをキューに追加します。各キーには独自のキューがあり、リクエストが順番で処理されます。リクエストを処理するスレッドは、リクエストが完了するまで生成されます。
この機能は、ThreadQueue クラス、ThreadQueue、を基に構築されています。プロミスを返す代わりに、ThreadQueue は、オペレーションが完了し、エラーが発生した場合、現在のスレッドを返すまであります。これは idiomatic な非同期 L
リクエストが失敗すると、設定可能な凸状オフバックを持つ再試行が行われます。これらの再試行は、ThreadQueueに送信されたコールバックの一部であり、キーの次のリクエストが開始される前に完了することが保証されています。
リクエストが完了すると、リクエストメソッドは success, result パターンで返されます
DataStoreWrapper また、指定されたキーのクエールの長さを取得するメソッドも公開します。これは、サーバーがシャットダウンされ、最新のリクエスト以外のリクエストを処理する時に特に便利です。このオプションは、サーバーがシャットダウンされている場合、最新のリクエスト以外のリクエストを処理する時に特に便利です。
洞窟
DataStoreWrapper は、極端なシナリオ以外のすべてのデータストアリクエストが完了することを許可する原則に従っています(成功またはそうでない)、即ち、最新のリクエストが完了するために(またはそれ以上のリクエストを完了するた
リクエストをキューから削除するときに直感的なルールのセットを決定するのは困難です。次のキューを考慮してください:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
期待される動作は、GetAsync() が 1 を返すことですが、最新の Class.GlobalDataStore:SetAsync()|SetAsync() リクエストをキューから削除するために、Class.GlobalDataStore:SetAsync()|SetAsync() リクエストをキューから削除することで、1>01>
論理進行は、新しい書き込みリクエストが追加されると、最新の読み込みリクエストから最も古いリクエストのみを更新します。 UpdateAsync() 、このシステムで使用される最も一般的なオペレーション (およびこのシステムで使用される唯一のオペレーション) は、読み取り
DataStoreWrapper は、UpdateAsync() リクエストが読み取ったり/書き込まれたりすることを許可するかどうかを指定する必要がありますが、これは我々のプレイヤーデータシステムでは、セッションロックメカニズム (後で詳細に説明) により、時間内に決定でき
クエールから削除されると、どのようにこれが処理されるかについて直感的なルールを決定するのが困難になります。DataStoreWrapper リクエストが作成されると、現在のスレッドが完了するまでありま
最終的に、私たちの見解は、単純なアプローチ (すべてのリクエストを処理する) がここで優れているということであり、より複雑な問題に近づくときにクリアな環境を作成します。唯一の
セッションロック
クラス: SessionLockedDataStoreWrapper
背景
プレイヤーデータは、サーバーのメモリに格納され、サーバーが必要なときにのみ読み取り、書き込みを行うことができます。メモリ内のプレイヤーデータを読み取ると、ウェブ要求を必要とせずにすぐにメモリ内のプレイヤーデータを読み取り、更新できます。 DataStoreService
このモデルが期待通りに機能するには、1つのサーバーだけが DataStore からプレイヤーのデータをメモリに読み込むことができる必要があります。
たとえば、サーバー A がプレイヤーのデータをロードする場合、サーバー B は、サーバー A がロックを解除するまで、サーバー A がそのデータを読み込むことができません。ロックメカニズムがない場合、サーバー B は、サーバー A が最新バージョンをメモリに保
Roblox は 1 回に 1 台のクライアントにのみ接続できることを許可していますが、1 セッションのデータが次のセッション開始時に常に保存されるとはありません。次のシナリオを考慮してください:
- サーバー A は、DataStore のデータを保存するためのリクエストを行いますが、リクエストに失敗し、複数の再試行が必要です。リトライ期間中、プレイヤーはサーバー B に参加します。
- サーバー A は、同じキーに UpdateAsync() を複数回実行し、サーバー A は、最大速度で実行されます。最終の保存リクエストは、キューに配置されます。リクエストがキューに配置されている間、プレイヤーはサーバー B に参加します。
- サーバー A で、PlayerRemoving イベントに接続されたコードは、プレイヤーのデータが保存される前に出力されます。この操作が完了する前に、プレイヤーはサーバー B に参加します。
- サーバー A のパフォーマンスは悪化したため、最終保存はプレイヤーがサーバー B に参加するまで遅れます。
これらのシナリオはレアですが、発生する可能性はあります。たとえば、プレイヤーが 1 つのサーバーから切断されて別のサーバーに速速に接続する状況では、一部の悪意のあるユーザーはこの動作を悪用してアクションを完了します。これは特にゲームで、プレイヤーが取引を完了するために使用する可
セッションロックは、プレイヤーの DataStore キーがサーバーに最初に読み取られるときに、サーバーのアトムレベルでキーのメタデータにロックを書き込むことでこの脆弱性を悪用しています。如果このロック値が存在すると、他のサー
アプローチ
SessionLockedDataStoreWrapper は、DataStoreWrapper クラスのメタワーラーです。DataStoreWrapper は、クエールと再試行機能を提供し、0> DataStore0> はセッションロックをサプリメントします。
SessionLockedDataStoreWrapper パスは、<
変換機能は、各リクエストで次のオペレーションを実行します: UpdateAsync
キーがアクセアクセス, 書き込み権限 (write access)できるかどうかを確認し、そうでない場合はオペレーションを放棄します。「安全にアクセアクセス, 書き込み権限 (write access)」は次の意味を意味します:
キーのメタデータオブジェクトには、ロックの有効期限以上の更新時間が経過した LockId の値が含まれていません。これにより、別のサーバーによってロックを配置したことに対応し、ロックの有効期限が期限切れになった場合を含め、そのロックを無視することがあります。
このサーバーが以前に LockId の価値をキーのメタデータに置いた場合、この価値はまだキーのメタデータにあります。これは、別のサーバーがこのサーバーのロックを (期限切れにより) 取り上げ、その後リリース
UpdateAsync は、 DataStore のオペレーションを実行します。たとえば、 SessionLockedDataStoreWrapper は、 0> function(value) return value en終了0> に翻訳されます。
リクエストにパラメーターが与えられた場合、UpdateAsync は、キーをロックまたはアンロックします:
キーがロックされている場合、UpdateAsync は、キーのメタデータに LockId を GUID で設定します。この GUID は、サーバー内のメモリに保存されて、次回、キーにアクセスする
キーがアンロックされる場合、UpdateAsync は、キーのメタデータに LockId を削除します。
カスタムな再試行ハンドラーは、DataStoreWrapper の下に渡され、セッションがロックされたため、ステップ 1 で中止された場合、操作が再試行されます。
カスタムエラーメッセージは、プレイヤーデータシステムがクライアントのセッションロックに対応する場合、プレイヤーデータシステムに報告される代替のエラーを報告することができます。
洞窟
セッションロック済みの実装は、PlayerRemoving または BindToClose() のキーを含むすべてのロックを常に解除するサーバーに基づいています。これは常に Class.Players.PlayerRemoving|PlayerRemoving または 2> Class.DataModel:BindToClose()|BindTo
しかし、アンロックは特定の状況で失敗することがあります。たとえば:
- サーバーがクラッシュしたか、または DataStoreService は、キーにアクセスするすべての試行において実行できませんでした。
- ロジックの間違いや類似のバグにより、キーのロックを解除する指示が行われませんでした。
キーのロックを維持するには、メモリに読み込まれるまでにそれに正常にアクセスする必要があります。これは通常、プレイヤーのデータシステムのバックグラウンドで実行されている自動保存ループの一部で行われますが、このシステムは、手動で行う必要がある場合には refreshLockAsync メソッドを公開します。
ロックの有効期限が更新されていない場合、ロックが更新されていないため、サーバーはロックを取得することができます。如果、別のサーバーがロックを取得する、現在のサーバーの読み取りまたは書き込みの試みは、新しいロックを設定するまで失敗します。
開発者製品プロセッシング
Singleton: ReceiptHandler >
背景
ProcessReceipt コールバックは、購入を終了するタイミングを決定するクリティカルなジョブを実行します。ProcessReceipt は、非常に特定のシナリオで呼び出されます。MarketplaceService.ProcessReceipt の保証については、1>Class.MarketplaceService.ProcessReceipt1> を参照してください。
購入を処理する方法はエクスペリエンス間で異なる場合がありますが、次のクリテリオを使用します
以前に購入されていない。
購入は現在のセッションに反映されます。
これには、次の操作を実行する必要があります PurchaseGranted を返す前に:
- PurchaseId が既に処理されていないことを確認します。
- プレイヤーのインメモリプレイヤーデータで購入を授与する。
- PurchaseId をプレイヤーのインメモリプレイヤーデータで処理した記録。
- プレイヤーのインメモリプレイヤーデータを DataStore に書き込みます。
セッションロックは、次のシナリオの必要性を減らし、フローを簡素化します:
- 現在のサーバーのメモリ内のプレイヤーデータは、損傷している可能性があり、DataStoreから最新の値を取得する必要があります。PurchaseId の履歴を確認する前に
- 同じサーバーで実行されている同じ購入のコールバックで、PurchaseId の履歴を読み込み、更新されたプレイヤーのデータをコールバックする必要があります。これにより、レースコンディションを防止できます。
セッションロックは、プレイヤーの DataStore に書き込みを試行した場合、他のサーバーにより、このサーバーのデータを読み込んで保存することができません。これは、このサーバーのメモリ内のプレイヤーデータが最新の
アプローチ
ReceiptProcessor のコメントはアプローチを説明しています:
このサーバーでプレイヤーのデータが正常に読み込まれているか、エラーなく読み込まれているかを確認します。
このシステムはセッションロックを使用しているため、このチェックは、メモリ内のデータが最新バージョンであることを確認します。
プレイヤーのデータがまだ読み込まれていません (プレイヤーがゲームに参加するときに期待される) を待ちます。プレイヤーがデータを読み込まないと、システムはまだ無効になり、このコールバックを呼び出すことができなくなります。プレイヤーがゲームから退出すると、データが読み込まれるのを待つ必要があります。このコール
PurchaseId がプレイヤーのデータにすでに記録されていないことを確認します。
セッションロックにより、メモリにある PurchaseIds 系のアレイは、最新のバージョンです。PurchaseId が Class.Global
このサーバーで「報酬」として購入を「更新」します。
ReceiptProcessor は、一般的なコールバックアプローチを取り、各 DeveloperProductId に別のコールバックを割り当てます。
このサーバーでプレイヤーのデータをローカルに更新して、PurchaseId を保存します。
リクエストを保存するために、DataStore にインメモリデータを保存するためのリクエストを送信し、PurchaseGranted を返す。如果リクエストが成功した場合は、NotProcessedYet を返す。
この保存リクエストが成功しない場合は、後でプレイヤーのインメモリセッションデータを保存するためのリクエストがまだ成功する可能性があります。次の ProcessReceipt コール中、ステップ 2 はこの状況を処理し、PurchaseGranted を返します。
プレイヤーデータ
Singletons: PlayerData.Server PlayerData.Client> , 1> PlayerData.Client1>>
背景
ゲームコードのインターフェイスを提供して、プレイヤーセッションデータを同期して読み取り書き込みするモジュールは、Roblox エクスペリエンスで一般的です。このセクションでは、PlayerData.Server とPlayerData.Client の両方をカバーしています。
アプローチ
PlayerData.Server と PlayerData.Client は、フォロー中処理を行います:
- プレイヤーのデータをメモリにロードし、読み込みできない場合の読み込む理を含む
- プレイヤーデータを変更およびクエリーするためのインターフェイスを提供する
- クライアントがアクセスできるようにプレイヤーのデータの変更をクライアントにレプリケートする
- クライアントにエラーダイアログを表示できるように、読み込み/保存ミスをクライアントに再現する
- プレイヤーが終了すると、プレイヤーのデータを期間限定で保存する
データのロード
SessionLockedDataStoreWrapper は、データストアに getAsync リクエストを行います。
このリクエストが失敗すると、デフォルトのデータが使用され、プロフィールは「エラー」とマークされて、データストアに書き込まれないようにします。
代わりにプレイヤーを追放することもできますが、プレイヤーがデフォルトのデータでプレイし、メッセージをクリアすることをお勧めします、これにより、エクスペリエンスから削除することなく何が起きたかを明確にすることができます。
インタリアルデータとエラーステータスを含む読み込まれたデータを含む初期の読み込みは、PlayerDataClient に送信されます。
プレイヤーのために waitForDataLoadAsync を使用しているスレッドは再開されます。
サーバーコードのインターフェイスを提供
- PlayerDataServer は、同じ環境で実行されているすべてのサーバーコードによって必要になり、アクセスできるシングルトンです。
- プレイヤーデータはキーと値の辞典に構成されています。これらの値は、setValue、getValue、updateValue、および2>RemoveValue2>メソッドを使用して、サーバー上で操作できます。これらのメソッドは、5>Yield5>を返さないで同期して動作します。
- hasLoaded および waitForDataLoadAsync メソッドは、データが読み込まれる前に確認する必要があります。これは、クライアントでデータの読み込みを処理する前に、ロードエラーをチェックする必要があるためです。
- hasErrored メソッドは、プレイヤーの最初のロードが失敗した場合、およびプレイヤーがデフォルトのデータを使用するようになったため、プレイヤーの初期ロードに失敗しているかどうかをクエリーできます。このメソッドを許可する前に、プレイヤーが購入を行うことはできません、因みに購入はデータに保存されることはできません。
- プレイヤーのデータが更新されると、playerDataUpdated 信号が player 、key 、および 2>value2> で発動します。個々のシステムはこれにサブスクリプトできます。
クライアントに変更をレプリケートする
- PlayerDataServer のプレイヤーデータに変更がある場合、そのキーが setValueAsPrivate を使用してプライベートでない限り、PlayerDataClient にレプリケートされます。
- setValueAsPrivate は、クライアントに送信しないべきキーを指します
- PlayerDataClient には、キーの値を取得するメソッドが含まれており、更新されると発信される信号が含まれます。hasLoaded メソッドとloaded 信号は、クライアントがシステムを開始する前にデータをロードしてコピーすることができます。
- PlayerDataClient は、同じ環境で実行されているすべてのクライアントコードによって必要になり、アクセスできるシングルトンです
クライアントにエラーをレプリケートする
- プレイヤーデータの保存または読み込みを PlayerDataClient に再プレースすると、エラーステータスが発生します。
- この情報には、getLoadError と getSaveError メソッド、および loaded と 1>saved1> 信号が含まれています。
- これらのイベントを使用して、クライアントの購入プロンプトを無効にし、警告ダイアログを実装します。この画像には、例のダイアログが示されています:
プレイヤーデータの保存
プレイヤーがゲームから退出すると、システムは次のステップを実行します:
- プレイヤーのデータをデータスト保管に書き込むことが安全であるかどうかをチェックします。場合により、プレイヤーのデータが読み込まれなかったり、まだ読み込まれています。
- 現在のメモリデータ値をデータストアに書き込み、セッションロックが完了した後、セッションロックを削除するためのリクエストを SessionLockedDataStoreWrapper を通じて行います。
- プレイヤーのデータ (およびメタデータやエラーステータスなどの他の変数) をサーバーメモリからクリアします。
定期的なループでは、サーバーは各プレイヤーのデータをデータストアに書き込みます (保存することが安全であることを含む) 。これにより、サーバークラッシュの場合の損失が軽減され、セッションロックを維持するためにも必要です。
サーバーのシャットダウンリクエストが受信されると、次の BindToClose コールバックが発生します:
- プレイヤーがサーバーから去ると、通常のプロセスを経て、各プレイヤーのデータをサーバーに保存するリクエストが行われます。これらのリクエストは並行して実行され、 BindToClose コールバックの完了時間は 30 秒しかありません。
- セーブをスピードアップするために、各キーのキューにある他のすべてのリクエストが、DataStoreWrapper (参照してください)からクリアされます。
- コールバックは、すべてのリクエストが完了するまで返さない。