authoritative-server について
ヘッドレスサーバーが共有ゲーム状態を管理し、クライアントの変更を検証する Decentraland シーン開発向けスキルです。サーバーとクライアントの処理、同期エンティティ、メッセージ、ストレージ、不正対策を扱います。
主な機能
- 01権威型ヘッドレスサーバーの設定
- 02サーバー側の状態検証
- 03クライアント・サーバー間のメッセージ設計
- 04シーンとプレイヤーデータの永続化
- 05プレイヤー位置の検証
活用シーン
- 01Decentraland のマルチプレイヤーゲーム開発
- 02共有コンポーネントへの書き込み検証
- 03クライアントとサーバー間のメッセージ送信
- 04再起動後のゲームデータ保持
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill authoritative-serverインストール後、Claude Code で/authoritative-serverを入力して読み込まれたか確認します。
同梱ファイルreferences/auth-server-examples.mdreferences/server-patterns.md
詳細
マルチプレイヤーサーバー
📔 注:マルチプレイヤーサーバーは、以前は オーソリタティブサーバー と呼ばれていました。名前だけが変わり、機能は同じです。インストールする SDK ブランチの名前は、引き続き auth-server です。
重要:シーンに追加する前に、必ずユーザーに知らせ、追加を進めてよいか確認してください。標準の SDK ではなく、@dcl/sdk@auth-server ブランチのインストールが必要であることも伝えてください。
ヘッドレスサーバーがゲームの状態を制御し、変更を検証して、不正行為を防ぐマルチプレイヤー Decentraland シーンを構築します。同じコードベースがサーバーとクライアントの両方で実行され、サーバーが完全な権限を持ちます。Decentraland がサーバーを自動的にホストしてデプロイします。基本的な CRDT マルチプレイヤー(サーバーなし)については、代わりに multiplayer-sync スキルを参照してください。
セットアップ
npm install @dcl/sdk@auth-server と npm install @dcl/js-runtime@auth-server を必ず使用してください。標準の @dcl/sdk には、オーソリタティブサーバー API は含まれていません。scene.json のルートにある "authoritativeMultiplayer": true がヘッドレスサーバーを有効にします(これがないと、シーンは通常のサーバーレス CRDT として実行され、isServer() は決して true を返しません)。ただし、手動で追加する必要はありません。@dcl/sdk@auth-server の SDK コマンドが、ビルドとプレビューのたびに自動で追加します(bundle.ts が ensureJsonKey を介して scene.json に authoritativeMultiplayer: true を書き込みます。この設定が存在しない場合のみ追加されます。また、package.json に server-logs スクリプトも自動で追加されます)。ルールは単純です。削除しないでください。 本番環境のサーバーログを読み取る権限を付与するには、必要に応じて logsPermissions(ルートレベルのウォレットアドレスの配列)を追加してください。詳しくは {baseDir}/references/server-patterns.md → 本番環境のログを参照してください。プレビューでは、ローカルサーバーがバックグラウンドで自動的に起動します。
サーバー/クライアントの分岐
単一のコードベースでロジックを分岐するには、@dcl/sdk/network の isServer() を使用します。サーバーはヘッドレス(レンダリングなし)で実行され、PlayerIdentityData を介してすべてのプレイヤーの位置にアクセスできます。
~system/EngineApi の低レベルな isServer()(@dcl/sdk/network の同期ヘルパーとは異なり、非同期です)を使って役割を判定する共有/ライブラリコードでは、防御的なイディオムを使用してください。起動時に役割を一度だけ判定し、役割が不明な間はシステムが早期リターンするようにし、クライアント専用機能はサーバー上で常に無効にします。また、クエリに失敗した場合はクライアントとして扱い、クエリでエラーが発生しても実際のクライアントの機能が失われないようにしてください。
検証付き同期コンポーネント
サーバーからすべてのクライアントに同期されるカスタムコンポーネントを定義します。クライアントがサーバー権限の状態を変更できないよう、必ず validateBeforeChange() を使用してください。validateBeforeChange()(および protectServerEntity() など、それをラップするヘルパー)は、必ず isServer() ブロック内で呼び出してください。エンティティごとのオーバーロードも、エンティティを指定しないグローバルなオーバーロードも、サーバー上でのみ意味があり、クライアントで呼び出すとエラーが発生します。このルールは、共有ファイル内にあるグローバルなカスタムコンポーネントバリデーターにも適用されます。コンポーネントはモジュールスコープで定義しますが、validateBeforeChange() の呼び出しは isServer() ガード内に配置してください(例:main() 内、または shared/schemas.ts 内の if (isServer()) { ... } ブロック内)。
バリデーターのコールバックには { entity, currentValue, newValue, senderAddress, createdBy } が渡されます。コンポーネントのフィールドは value.newValue.<field> から読み取ってください(value.<field> ではありません。このフィールドは存在しません)。currentValue は変更前の値です(コンポーネントが存在しない場合は undefined)。コンポーネントが削除される場合、newValue は undefined になります。senderAddress は送信者のウォレットアドレスです。サーバーが送信した場合は AUTH_SERVER_PEER_ID と等しくなります。アドレスは必ず .toLowerCase() を使って比較してください。
検証パターン
- パターン 1 — サーバーのみが書き込み可能(最も厳格):
Score.validateBeforeChange((v) => v.senderAddress === AUTH_SERVER_PEER_ID) - パターン 2 — 値自体を検証:あり得ない値を拒否します(例:
value.newValue.position.y > 0) - パターン 3 — 近接性の検証(チート対策):
PlayerIdentityDataとTransformを使って、プレイヤーがオブジェクトの近くにいるか確認します。サーバーが読み取るプレイヤーのTransform.positionはシーンローカルのメートル単位で、クライアントが見るものやシーンエンティティが使うものと同じ座標系です。そのため、ベース区画のオフセット計算はせず、Vector3.distance()でエンティティの位置と直接比較できます。代表例として、公式の90,-9-authoritative-server-leaderboardテストシーン(原点以外の区画にデプロイ)では、サーバーが読み取ったプレイヤーの位置をシーンローカルのターゲットと直接比較しています。(一部の古い SDK サーバービルドでは、ワールド座標/区画絶対座標が返されていましたが、これはバグであり、すでに修正されています。scene.baseオフセットによる補正は追加しないでください。) - パターン 4 — 管理者のみが書き込み可能:
@dcl/asset-packs/dist/admin-toolkit-ui/ModerationControl/apiのgetSceneAdmins()を使って、管理者に制限します。固定の許可リストを軽量に実装する場合は、代わりにサーバーで検証済みの送信者を使ってメッセージハンドラーを制限します。共有設定に小文字のADMINS: string[]を保持し、room.onMessage(...)内でADMINS.includes(context.from.toLowerCase())を確認してください(クライアントから報告されたロールを信頼してはいけません)。クライアントも同じリストを読み取って、特権ボタンを表示するかどうかを判断できますが、権限を確定するのはサーバー側のチェックのみです。
ローカル開発中に検証を緩和するには、@dcl/asset-packs/dist/admin-toolkit-ui/fetch-utils の isPreview()(同期処理、引数なし、boolean を返します)を使います。動作するのは dist/... の深いインポートパスのみです。このパッケージにはトップレベルの再エクスポートがありません。
カスタムコンポーネントではグローバル検証を使います:GameState.validateBeforeChange((value) => ...)。組み込みコンポーネント(Transform、GltfContainer)ではエンティティごとの検証を使います:Transform.validateBeforeChange(entity, (value) => ...)。
エンティティを作成して保護した後、syncEntity(entity, [Transform.componentId, GameState.componentId]) で同期します。権威サーバー型のシーンでは、syncEntity() を呼び出すのはサーバーのみです。呼び出しを if (isServer()) で囲んでください。サーバーがエンティティのインスタンスを作成して共有し、すべてのクライアントが同期を受け取ります。これは、各クライアントがそれぞれ syncEntity を呼び出す multiplayer-sync パターン(サーバーレス)とは異なります。権威サーバー型のシーンでクライアントから syncEntity を呼び出すとエラーが発生します。また、クライアント側からの呼び出しを避けることで、ピア間のエンティティ ID の一貫性を気にする必要もなくなります。
プレイヤーごとの同期エンティティ
プレイヤーごとに1つ存在するサーバー作成エンティティ(スコア、保持時間、ウォレットなど)では、プレイヤーのアドレスから明示的な同期 ID を決して導出しないでください(例:hash(address) % N)。明示的な同期 ID は衝突を厳密にチェックするグローバルなネットワーク ID です。ハッシュ化した ID は、2つのアドレスが同じスロットにハッシュされた場合(範囲が10万なら、約370人で約50%の確率)と、古いエンティティがクリーンアップされる前に同じプレイヤーが再接続した場合のどちらでも、syncEntity failed because the id provided is already in use をスローします。代わりに ID を省略します(syncEntity(entity, [Comp.componentId]))。自動割り当ては仕組み上、一意になります。また、プレイヤーのアドレスをコンポーネントのフィールド(playerId)に保存し、すべての読み取り処理(クライアントシステムとサーバー再起動時の照合)ではネットワーク ID ではなく、そのフィールドで照合してください。明示的な列挙型 ID は固定の単一エンティティ(ゲーム状態、フラグ、リーダーボード)用に確保してください。
長時間稼働するサーバーではエンティティスロットが再利用されるため、キャッシュした Map<address, Entity> のハンドルが古くなる場合があります(マップが破棄済みエンティティを指したまま、コンポーネントがなくなることがあります)。再利用する前に Comp.getOrNull(entity) でキャッシュを検証し、古くなっていたら作り直してください。フレームごとのシステムでは getMutableOrNull とガードを使い、一時的な取得失敗が発生した場合は例外 [mutable] Component <name> for <id> not found を毎フレーム発生させるのではなく、そのティックをスキップしてください。engine.getEntitiesWith(Comp) のスキャンでエンティティを取り込む場合(例:サーバー再起動後のマップ再構築)は、番号(entity & 0xffff)が512未満のエンティティをスキップしてください。これらはランタイムが所有する予約済み/アバター範囲のスロットです。キャッシュしたり、removeEntity() で削除したりしないでください。コードを含む完全なパターンは {baseDir}/references/server-patterns.md → プレイヤーごとの同期エンティティを参照してください。
メッセージ
クライアントからサーバー、およびサーバーからクライアントへの通信には registerMessages() を使用してください。メッセージスキーマは Schemas.Map(...) で定義します。通常の JavaScript オブジェクトはバイナリシリアライズに失敗します。
モジュール読み込みのタイミング(重要): registerMessages() は内部でコンポーネントを定義し、shared/schemas.ts 内の engine.defineComponent() もコンポーネントを定義します。どちらもエンジンがシールされる前の初期モジュール読み込み時に実行する必要があります。静的な import(例:index.ts の先頭にある import './shared/messages')で読み込んでください。main() 内の動的な import() は使用しないでください。動的インポートはシール後に実行され、Engine is already sealed がスローされます。@dcl/sdk/server をインポートするサーバー専用モジュールのみ、isServer() 分岐内で動的にインポートしてください。ただし、モジュールスコープでコンポーネントを定義しない場合に限ります。これにより、クライアントのバンドルパスに @dcl/sdk/server が含まれないようにできます。
- クライアントから送信:
room.send('playerJoin', { displayName: 'Alice' }) - サーバーから全員に送信:
room.send('gameEvent', { ... }) - サーバーから1人に送信:
room.send('gameEvent', { ... }, { to: [playerAddress] }) - 受信:
room.onMessage('playerJoin', (data, context) => { ... })—context.fromは送信者のウォレットです
クライアントは、メッセージを送信する前に isStateSyncronized()(SDK のタイポ)を呼び出し、true が返されるのを待つ必要があります。
重要 — メッセージサイズの上限:13 KB を超えるメッセージは決して送信しないでください。トランスポートは上限を超えたメッセージを警告なしで破棄します。大きなペイロードは、必要に応じて複数の小さなチャンクに分割してください。
MessageBus はクライアント専用です。 MessageBus(@dcl/sdk/message-bus から)は、レガシーな EngineApi.subscribe('comms') イベントを購読しますが、ヘッドレスサーバーのランタイムではこのイベントが実装されていません。そのため、サーバー上では RemoteError: not implemented で失敗します。モジュールスコープのコードは両方の側で実行されるため、権威サーバーのシーンではモジュールスコープでインスタンスを生成しないでください(const bus = new MessageBus())。クライアント分岐の内部(if (!isServer()))でのみ生成してください。クライアント側で一時的なクライアント間エフェクトに使うのは問題ありませんが、サーバーは MessageBus で送受信できません。クライアントとサーバー間の通信には、すべて registerMessages() と room を使用してください。
スキーマ型リファレンス
Schemas.String, .Int, .Float, .Boolean, .Int64(Date.now()/13 桁以上の数値用)、.Vector3、.Quaternion、.Entity、.Array(Schemas.String)、.Optional(Schemas.String)、.Map({ name: Schemas.String, hp: Schemas.Int })。
ブール値のスキーマは
Schemas.Booleanであり、Schemas.Boolではありません(確認済み —@dcl/ecsのschemas/index.d.tsではSchemas.Booleanが公開されています。内部クラスの名前はBoolですが、Schemas名前空間ではその名前で公開されていません)。
タイムスタンプには Schemas.Int64 を使用してください — Schemas.Number は大きな数値(13 桁以上)を破損させます。
サーバーでのプレイヤー位置の読み取り
engine.getEntitiesWith(PlayerIdentityData) + Transform.getOrNull(entity) を使用して、サーバーで検証された実際の位置を読み取ります。クライアントが報告した位置は決して信用しないでください。
ストレージ
サーバーの再起動後もデータを保持します。サーバー専用 — isServer() でガードしてください。@dcl/sdk/server からインポートします。
- シーンストレージ(グローバル、全プレイヤー間で共有):
Storage.set/get/delete(key)—Storageのトップレベルメソッド - プレイヤーストレージ(プレイヤーごと、ウォレットアドレスでスコープ設定):
Storage.player.set/get/delete(address, key)
ストレージが受け付けるのは文字列のみです — オブジェクトには JSON.stringify()/JSON.parse() を使用します。ローカル開発用ストレージは node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json にあります。本番環境のストレージは decentraland.org/storage にあります。CLI:npx sdk-commands storage scene/player set/get/delete ...。ストレージはデプロイ後も保持されます(ハッシュではなくワールド単位でスコープ設定されます)。
追記も compare-and-set もありません — ログへの追加は読み取り・変更・書き込みとなるため、2 つのサーバーインスタンス(古いデプロイと新しいデプロイが重複する時間帯)間で、互いの行が失われる可能性があります。書き込みの直前に再読み込みして ID でマージすれば、その時間帯を短くできますが、なくすことはできません。増え続けるログ(プレイテストの回答、ラウンド履歴、テレメトリー)には、現在のパートを示すヘッドキーを使い、番号付きキーに分割してください。{baseDir}/references/server-patterns.md → 追記専用ログ:Storage での CSV パートのローテーション を参照してください。これは Scene Feedback モジュールが使用するパターンです。
重要 — ストレージへの書き込みには上限があるため、変更やティックごとに書き込まないでください:スコア変更/イベント/ティックごとに Storage.set を実行するシーンは、分離環境で共有されているホスト呼び出しの処理中上限に達します。上限を超えた書き込みは通知なく失敗し、SDK は例外を投げずに false を返します。Storage.set/Storage.player.set は Promise<boolean> を返すため、必ず確認してください(false =書き込みが保存されませんでした。再試行するか、ユーザーに通知してください)。ライブ状態/作業状態はメモリ内に保持し(サーバーではより高速で正確です)、ストレージへの永続化はゲーム終了、プレイヤーの退出、定期的なデバウンス保存など、重要なチェックポイントでのみ行います。サーバーの再起動/デプロイ後も保持する必要があるデータだけを永続化してください。上限の仕組みとチェックポイントのパターンについては、{baseDir}/references/server-patterns.md → Storage Patterns and Server Resource Limits を参照してください。
ストレージのライブ Web UI(decentraland.org/storage、または Creator Hub の Manage → 公開済みの場所の横にある三点リーダー → View Storage からアクセスできます)。タブは Scene、Player、Environment の 3 つです。編集内容は再公開せずに実行中のシーンへ即時反映されます:
- Scene タブ:共有変数(リーダーボード、ドアの状態)を表示/編集/削除します。リーダーボードをリセットするなど、ライブの値を調整するのに便利です。
- Player タブ:ウォレットアドレスまたは名前でプレイヤーを検索し、保存されたデータを確認/編集/消去します。主な用途はサポートです — 古いシーンバージョンとのデータの矛盾などにより、不正な状態で動けなくなったプレイヤーを、再デプロイせずに復旧できます。
環境変数
値をハードコードせずに設定できます。サーバー専用です。@dcl/sdk/server の EnvVar.get(key: string): Promise<string> は常に文字列を返し、変数が設定されていない場合や取得に失敗した場合は ''(空文字列)を返します(undefined にはなりません)。デフォルト値には || 'fallback' パターンを引き続き使用できます。'' || 'x' は 'x' と評価されるためです。ローカルでは .env ファイルを使用します(.gitignore に追加してください)。デプロイ時は npx sdk-commands storage env set KEY --value VALUE を実行します。本番環境の UI は decentraland.org/storage → Environment タブ(または Creator Hub → Manage → 三点メニュー → View Storage)にあります。
- シークレットの保存に最適(秘密鍵、報酬/請求コード、API キー)。値が存在するのはサーバー上のみで、クライアントや公開済みシーンのコードに届くことはありません。
- UI からは書き込みのみ:変数の追加、上書き、削除はできますが、現在の値を読み戻すことはできません(シークレット保護のための仕様です)。
- 公開し直すことなく、実行中のシーンで調整したいリアルタイムに変更可能なゲームパラメーター/機能フラグ(マッチ時間、最大プレイヤー数)にも最適です。
推奨プロジェクト構成
src/
├── index.ts # Entry point — isServer() branching
├── client/
│ ├── setup.ts # Client init, input handlers, message senders
│ └── ui.tsx # React ECS UI (reads synced state, sends messages)
├── server/
│ ├── server.ts # Server init, systems, message handlers
│ └── gameState.ts # Server state management class
└── shared/
├── schemas.ts # Synced component definitions + validateBeforeChange
└── messages.ts # Message definitions via registerMessages()
パフォーマンスのベストプラクティス
コンポーネントが変更されるたびに、コンポーネントデータ全体が送信されます。モノリシックなコンポーネントよりもアトミックなコンポーネントを優先してください。一緒に変更されるフィールドをまとめ、頻繁に変更されるデータと変更頻度の低いデータを分けます。頻繁なメッセージは間引いてください(毎フレーム送信してはいけません)。算出可能な状態は約 30 秒ごとにブロードキャストし、その間はローカルで計算してください。
サーバーのライフサイクル
サーバーがアクティブになるのは、シーンに少なくとも 1 人のプレイヤーがいる間だけです。最後のプレイヤーが退出すると、約 2 分間稼働した後にシャットダウンします。次の訪問時には新しいインスタンスがコールドスタートし、本番環境では約 15 秒かかります。ローカルプレビューではサーバーがすぐに起動します。そのため、サーバーの準備状態に関するバグは、ほとんど気づかれないまま本番環境に持ち込まれます。「しばらく誰も訪れていない」状況の動作は、必ず実際のデプロイ環境でテストしてください。
isStateSyncronized() はサーバーの準備状態を確認するものではありません。 CRDT ルームのトランスポートが接続されていることを確認するだけです。ルームの CRDT スナップショットには、前回のサーバー実行時に永続化された状態が保持されている場合があります。そのため、新しいクライアントには、サーバーの起動中、あるいはこのクライアントしかおらずプラットフォームがサーバーをまだ起動していないためにサーバーがまったく起動しない場合でも、「有効な」状態が見えることがあります。この間に送信されたメッセージは通知なく失われ、サーバーからの応答を待つシーンが永久に停止します。
信頼できる方法は、サーバーハートビートです。サーバーは同期されるコンポーネントフィールドに Date.now() を約 2 秒ごとに書き込みます。クライアントはサーバーのタイムスタンプではなく、値の最後の変化を確認したクライアント側の時刻を記録し、約 3 倍の間隔以内に更新を確認できた場合にのみサーバーが稼働中と判断します。ハートビートの値ではなくクライアントが確認した時刻を記録することで、長時間前に停止したサーバー実行時の古いスナップショットを稼働中と誤認せず、サーバーとクライアントの時計のずれも問題になりません。最初のクライアントが接続したときに丸々 1 間隔待たずに済むよう、最初のハートビートはサーバーの状態初期化関数内で送信してください。
UI 層では、見た目は似ていても挙動が大きく異なる 2 つの障害モードを区別します。ルーム未同期は一時的な状態(シーンの読み込み中に約 1 秒間)です。アクションをバッファーし、再試行システムから自動的に実行してください。サーバー未稼働はコールドスタート時に 15 秒以上続く場合があり、解消されないこともあります。アクションを通知なくバッファーするのではなく、「サーバーを起動しています」というポップアップを表示し、ハートビートを受信した時点で自動的に閉じて、待っていたプレイヤーに古いダイアログが表示されたままにならないようにしてください。実装の詳細は {baseDir}/references/auth-server-examples.md → Server Liveness Heartbeat を参照してください。
デプロイのバージョン管理
クライアントとサーバーは常に一緒に更新されます(ハッシュで紐付けられています)。既存のプレイヤーは再参加するまで古いバージョンを使い続けます。Storage データはバージョンをまたいで保持されます。
テストとデバッグ
- ログのプレフィックス:
console.log()では[SERVER]と[CLIENT]を使用します - ローカルでのマルチプレイヤー:デスクトップ Explorer は一度に 1 インスタンスしか起動しません。もう一度 Play を押しても、すでに開いているウィンドウにフォーカスが移るだけで、プレイヤーは追加されません。(a)Creator Hub の Play Options ポップオーバーで Multi-Instance Preview にチェックを入れます(Desktop Client の行にカーソルを合わせるとフライアウトが開きます。=
sdk-commands start --multi-instance。@dcl/sdk>= 7.20.4 が必要)。その後、もう一度 Play を押します。マルチインスタンスではウィンドウごとに個別のログインが必要になるため、Skip Auth Screen はオフになり、各インスタンスで別々のアカウントを使ってサインインする必要があります。または、(b)デスクトップクライアントを開いたまま、ブラウザクライアントをプレイヤー 2 として追加します。Play Options → Web (Bevy) クライアントの行を選択します(=sdk-commands start --web、エイリアスは--bevy-web)。-nを単独で指定しても、新しいデスクトップウィンドウが強制的に開きます。 - 本番環境のログ:
npx sdk-commands sdk-server-logsを実行します(Worlds の場合は--world WORLD_NAME.dcl.ethを追加します)。ウォレット署名チャレンジが表示されます。署名するウォレットはscene.jsonのlogsPermissionsに登録されている必要があります。{baseDir}/references/server-patterns.md→ 本番環境のログを参照してください。 - サーバーログのノイズの特徴:サーバーログで繰り返し発生する以下 2 つのエラーは、どちらもクライアント専用コードがサーバーブランチで実行されていることを示します。
EngineApi.subscribe('comms')でのRemoteError: not implemented→ サーバー上でMessageBusが構築されています(メッセージを参照)。comms-gatekeeper.decentraland.orgからの400 Invalid metadata content→ サーバーコードがsignedFetchを介してクライアントコンテキストのプラットフォーム API を呼び出しました。どちらもサーバーをクラッシュさせることはありませんが、繰り返し出るノイズが実際のサーバーログを埋もれさせるため、問題のコードを!isServer()で囲んでください。 - プレイヤーごとのエンティティエラーの特徴:
syncEntity failed because the id provided is already in use→ 明示的に指定した同期 ID が衝突しました(通常はハッシュから導出されたプレイヤーごとの ID、または同じフレーム内で同じ ID の削除と再作成を行った場合)。[mutable] Component <name> for <id> not foundが毎フレーム繰り返し表示される → 長時間稼働しているサーバーでキャッシュされたエンティティハンドルが古くなっています。どちらの修正方法も、上記のプレイヤーごとの同期エンティティを参照してください。 - 古い CRDT ファイル:
main.crdtとmain1.crdtを削除して再起動します - ストレージの確認:ローカルの JSON ファイル、または decentraland.org/storage を確認します
- タイマーとサンドボックス:QuickJS サンドボックスでは Node.js API(
fs、httpなど)は使用できません。遅延には@dcl/sdk/ecsのtimers.setTimeout/timers.setIntervalを使用します。ネイティブの JS グローバルは使用しないでください。ゲームロジックには dt アキュムレーターを使ったengine.addSystem()を推奨します - エンティティの同期:
syncEntity(entity, [componentIds])で正しい.componentId値を指定していることを確認します
シーンの例
-
https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/90,-9-authoritative-server-leaderboard — エンドツーエンドの権威型リーダーボードの完全な例です。クライアントは
claimPointアクションを送信します(スコアは送信しません)。ヘッドレスサーバーが「スコアオーブ」への近接を検証し(シーン内座標を直接比較)、自らスコアを加算して、プレイヤーごとの合計をStorageに保存し、同期された上位N件のLeaderboardコンポーネントをブロードキャストして、すべてのクライアントに描画させます。次の内容を示しています。scene.jsonのauthoritativeMultiplayer: true、isServer()による分岐、モジュール読み込みタイミングを考慮したregisterMessages()/defineComponent()の静的インポート、isServer()を条件とするカスタムコンポーネントのvalidateBeforeChange(AUTH_SERVER_PEER_IDによるサーバーのみの書き込み)、サーバーのみのsyncEntity、アトミックコンポーネント(ハートビートをボードと分離)、クライアントが観測した時刻を追跡するサーバー稼働確認用ハートビート。 -
https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/92,-9-authoritative-server-gem-rush — 不正防止機能とチェックポイントの永続化を備えた、競争型ゲームの完全な例です。主なパターン:
PlayerIdentityDataとTransformによるサーバー側の近接検証(クライアントの位置を決して信用せず、サーバーがプレイヤーの Transform を直接読み取り、Vector3.distanceで比較)、クライアントからの書き戻しを防ぐため、位置を同期対象の Transform ではなく保護されたカスタムコンポーネントに保存、変更頻度ごとにアトミックコンポーネントを分割(カウントダウン、スコアボード、殿堂をそれぞれ分離)、Storageへの書き込みはラウンド終了時のみ(ラウンドごとにシーンの SET が1回、プレイヤーの SET がN回、ゲームプレイ中は呼び出しなし)、拒否メッセージ内のantiCheat: trueフラグ、起動時および約2秒ごとにDate.now()を送信するServerHeartbeat、セッションごとにStorage.player.getでプレイヤー別統計情報を遅延読み込みし、メモリにキャッシュ。 -
https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/93,-9-authoritative-server-limits-lab — すべてのランタイム制限を負荷テストします。
maxInflightHostCalls40、maxConcurrentFetches32、maxSendMessages512、maxCommsMessageBytes30000、maxMessagesPerWindow300/s、maxLiveEntities100000、isolateMemoryLimitBytes256 MB、同期10秒/非同期60秒、フェッチタイムアウト15秒、最大WSメッセージ1 MB、最大オープンソケット数32、最大フェッチリダイレクト数5、最大ボディサイズ10 MB。制限値の参照先は{baseDir}/references/server-patterns.md→ Server Resource Limits です。 -
https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,-1-scene-feedback — プレイテスト用アンケートモジュールの Scene Feedback と、Storage の追記専用ログ のリファレンス実装です。インメモリバッファ、バッファへの格納時にACK、400 KB単位で分割するロール式CSV(
scene-feedback-N.csv)とヘッドキー、クライアント生成のrequestIdによるサーバー側の重複排除、アドレス単位のレート上限、CSVエスケープとスプレッドシート数式対策を備えています。src/scene-feedback/とassets/scene-feedback/をシーンにコピーし、サーバーを有効にして(@dcl/sdk@auth-server、authoritativeMultiplayer: true)、requiredPermissionsにUSE_FETCHを追加してください。設定はquestions.tsで行います。README にはエージェント向けのインストールプロンプトが含まれています。何をいつ尋ねるかについての設計レベルのガイダンスは、game-design → Test Early を参照してください。
コード例(検証パターン、メッセージ、Storage、EnvVar、パフォーマンス)の全容については、{baseDir}/references/auth-server-examples.md を参照してください。サーバー設定のパターンについては、{baseDir}/references/server-patterns.md を参照してください。