multiplayer-sync について
Decentraland SDK7の開発者向けマルチプレイ同期ガイドです。プレイヤー間でシーン状態を共有したり、一時的なイベントを送ったりする方法を説明し、外部APIやWebSocket、自作コンポーネント、接続状態も扱います。
主な機能
- 01CRDTベースの状態同期
- 02syncEntityによるエンティティとコンポーネントの同期
- 03MessageBusによる一時イベントの送信
- 04fetch、signedFetch、WebSocketの利用
- 05同期コンポーネントとスキーマの定義
- 06プレイヤー識別情報と同期接続状態の確認
活用シーン
- 01ドア、スイッチ、スコアボードの状態共有
- 02一時的な音声やパーティクルイベントの配信
- 03外部サービスを使ったゲームデータの保存
- 04リアルタイムの双方向通信によるサーバー接続
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill multiplayer-syncインストール後、Claude Code で/multiplayer-syncを入力して読み込まれたか確認します。
同梱ファイルreferences/networking-patterns.md
詳細
Decentraland のマルチプレイヤー同期
Decentraland では、プレイヤーの Explorer インスタンス内でシーンがローカルに実行されます。デフォルトでは、プレイヤー同士が互いを認識して直接やり取りできますが、各プレイヤーは環境と個別にやり取りします。デフォルトでは、環境の変更はプレイヤー間で共有されません。
シーンの状態に対する変更を同期するために、SDK7 では CRDT ベースの同期を使用します。
ランタイムの制約: Decentraland は QuickJS サンドボックスで実行されます。Node.js API(
fs、http、path、process)は使用できません。ネットワーク通信にはfetch()とWebSocketを使用してください。非同期処理のパターンについては、scene-runtime スキルを参照してください。
同期方法の選択フローチャート
必要な用途に応じて、適切なネットワーク方式を選択してください。
| 方式 | 使用する場面 | 永続性 | 例 |
|---|---|---|---|
syncEntity | すべてのプレイヤーに共有され、新しく参加したプレイヤーにも維持される状態 | はい — プレイヤーの参加・退出後も状態は維持されますが、シーンにプレイヤーが1人以上いる間に限ります。シーンが空になると、状態はすぐにリセットされます | ドア、スイッチ、スコアボード、エレベーター |
MessageBus | その場限りのイベント | いいえ — 後から参加したプレイヤーは過去のメッセージを受け取れません | チャットメッセージ、効果音、パーティクルのトリガー |
fetch / REST API | 外部サーバー上のデータの読み取りや書き込み | サーバーに依存 | リーダーボード、インベントリ、外部のゲーム状態 |
signedFetch | プレイヤーの本人確認を証明する認証済みリクエスト | サーバーに依存 | 報酬の獲得、検証済みスコアの送信 |
WebSocket | サーバーとのリアルタイム双方向通信 | 接続に依存 | ライブゲームサーバー、リアルタイムチャット。 |
選択の流れ:
- 後から参加したプレイヤーも含め、すべてのプレイヤーが同じ状態を見る必要がありますか? -->
syncEntity - シーン内に現在いるプレイヤーだけを対象とする、一度きりのイベントですか? -->
MessageBus - すべてのプレイヤーが退出した後も情報を永続化する必要がありますか?または、その情報に対して安全な検証を実行する必要がありますか? -->
fetchまたはsignedFetch - サーバーとの継続的なリアルタイム通信が必要ですか? -->
WebSocket - 方式は自由に組み合わせられます。ワールドの状態には
syncEntity、効果にはMessageBus、永続化にはfetchを使用してください。
syncEntity の基本
インポートと基本的な使い方
import { engine, Transform, MeshRenderer, Material } from '@dcl/sdk/ecs'
import { syncEntity } from '@dcl/sdk/network'
import { Vector3, Color4 } from '@dcl/sdk/math'
シグネチャ:syncEntity(entity, componentIds[], syncId?)
entity— 同期するエンティティcomponentIds[]— 同期を維持するコンポーネント ID の配列(例:[Transform.componentId])syncId— 一意の数値識別子(事前定義されたエンティティでは必須、プレイヤーが生成するエンティティでは任意)
列挙型の同期 ID(事前定義されたエンティティ)
事前定義された同期エンティティには、それぞれ一意の数値 ID を必ず割り当ててください。ID の衝突を避けるには、列挙型を使用します。
enum SyncIds {
DOOR = 1,
ELEVATOR = 2,
SCOREBOARD = 3,
}
const door = engine.addEntity()
Transform.create(door, { position: Vector3.create(8, 1, 8) })
MeshRenderer.setBox(door)
syncEntity(
door,
[Transform.componentId, MeshRenderer.componentId],
SyncIds.DOOR
)
同期 ID を持つ事前定義エンティティは、作成したプレイヤーが退出した後も保持されます。同期 ID を持たない、プレイヤーが作成したエンティティは、プレイヤーの接続が切れると削除されます。
ベストプラクティス — シングルトンには必ず安定した同期 ID を割り当ててください。 自動 ID は、作成元ピアと、エンジンが再利用するローカルのエンジンエンティティ番号から ID を導出します。自動 ID を持つシングルトン同期エンティティを繰り返し破棄して再作成する方法は、実際のネットワーク通信では不安定です。ローカルの単一プロセスプレビューでは完全に動作していても、リモートクライアントで同期が正しく調整されず、デフォルトのコンポーネントデータしか表示されないことがあります。シングルトンや、よく知られた少数の固定同期エンティティには、予約済みの列挙型から安定した明示的な同期 ID を割り当ててください。多数のインスタンスを作成し、後で破棄するような本当に動的なエンティティには、自動 ID を使用します。また、固定 ID の同期エンティティに対して、同じフレーム内で
removeEntityを呼び出して同じ ID で再作成しないでください。内部のNetworkEntityは後続の CRDT フラッシュまで存続するため、すぐに再作成するとid provided is already in useが発生します。再スポーンは後続のティックまで延期してください。詳しい失敗パターン、その修正方法、および楽観的予測を組み合わせたパターンについては、{baseDir}/references/networking-patterns.mdの syncEntity identity セクションを参照してください。
自動生成 ID(プレイヤーがスポーンするエンティティ)
プレイヤーが実行時に作成するエンティティには、明示的な同期 ID は不要です。
function createProjectile() {
const projectile = engine.addEntity()
Transform.create(projectile, { position: Vector3.create(4, 1, 4) })
MeshRenderer.setSphere(projectile)
syncEntity(projectile, [Transform.componentId])
return projectile
}
一部のビジュアルは共有ではなく、プレイヤーごとに異なります。 カメラの方を向く
Billboardは各エクスプローラーでローカルに再計算されるため、各プレイヤーには自分の方を向いて表示されます。これは同期されないため、syncEntityは不要です。例外は、targetEntityを持つBillboardです。ターゲットの位置はシーンの状態であるため、すべてのプレイヤーに同じ向きのビルボードが表示されます。共有オブジェクトを指す看板など、共有された一貫性のある向きが必要な場合はtargetEntityを使用してください。Billboard の詳細については、player-avatar/sdk-scenesコンポーネントリファレンスを参照してください。
カスタム同期コンポーネント
カスタムコンポーネントを定義し、プレイヤー間で同期します。
import { engine, Schemas } from '@dcl/sdk/ecs'
import { syncEntity } from '@dcl/sdk/network'
const ScoreBoard = engine.defineComponent('scoreBoard', {
score: Schemas.Int,
playerName: Schemas.String,
lastUpdated: Schemas.Int64,
})
const board = engine.addEntity()
ScoreBoard.create(board, { score: 0, playerName: '', lastUpdated: 0 })
syncEntity(board, [ScoreBoard.componentId])
function addScore(points: number) {
const data = ScoreBoard.getMutable(board)
data.score += points
data.lastUpdated = Date.now()
}
タイムスタンプなどの大きな数値には
Schemas.Int64を使用してください。Schemas.Number/Schemas.Intは 13 桁を超える値(Date.now()など)を破損させます。そのような値は必ずSchemas.Int64に保存してください(上記のlastUpdatedのように)。
カスタムスキーマは決定論的でなければなりません。 同じコンポーネント名は、すべてのクライアントで同じスキーマに対応する必要があります。
プレイヤー固有のデータ
プレイヤーを識別するには PlayerIdentityData を使用します。
import { engine, PlayerIdentityData } from '@dcl/sdk/ecs'
engine.addSystem(() => {
for (const [entity] of engine.getEntitiesWith(PlayerIdentityData)) {
const data = PlayerIdentityData.get(entity)
console.log('Player:', data.address, 'Guest:', data.isGuest)
}
})
スキーマ型
カスタムコンポーネントで使用できるスキーマ型:
| Type | Usage |
|---|---|
Schemas.Boolean | true/false |
Schemas.Int | 整数 |
Schemas.Float | 小数 |
Schemas.String | テキスト文字列 |
Schemas.Int64 | 大きな整数(タイムスタンプ) |
Schemas.Vector3 | 3D 座標 |
Schemas.Quaternion | 回転 |
Schemas.Color3 | RGB カラー |
Schemas.Color4 | RGBA カラー |
Schemas.Entity | エンティティ参照 |
Schemas.Array(innerType) | 値の配列 |
Schemas.Map(spec, default?) | ネストされた構造体 — spec は名前付きフィールド({ x: Schemas.Int, ... })であり、均質なキー→値マップではありません |
Schemas.Optional(innerType) | null 許容値 |
Schemas.EnumNumber(enumObj, default) | 数値列挙型。default は必須です(例:Schemas.EnumNumber(State, State.Lobby)) |
Schemas.EnumString(enumObj, default) | 文字列列挙型。default は必須です |
Schemas.OneOf({ ... }) | 識別共用体($case + ペイロード) |
あまり一般的でないスキーマ型の例:
enum Rarity { Common = 0, Rare = 1, Legendary = 2 }
const Loot = engine.defineComponent('game::Loot', {
rarity: Schemas.EnumNumber<Rarity>(Rarity, Rarity.Common),
payload: Schemas.OneOf({
coins: Schemas.Int,
item: Schemas.String,
}),
label: Schemas.Optional(Schemas.String),
})
@dcl/sdk 7.28.0 でスキーマの修正が適用されました — 古い回避策は不要です
以下の4つはすべて実際のバグでした。7.28.0 以降を使用している場合は、直感的なコードを書けます。
| Fix | Old broken behavior | Now |
|---|---|---|
偽値を含む Schemas.Optional(...)(da82bfb0) | false、0、'' は未設定として扱われ、書き込まれず、読み戻すと undefined になっていました。Optional(Schemas.Boolean) に設定できるのは true か何もない状態だけでした。 | undefined/null かどうかで存在を判定するため、偽値も往復できます。Optional(Boolean) は実際の3状態を扱えるようになりました。 |
Schemas.OneOf(...) が未選択の場合(0dce4d2d) | create() は $case のない {} を返し、シリアライズ時に $case.toString() が呼び出され、例外が発生してシーン全体のエンジンティックが停止していました。 | 未設定のケースはインデックス 0(実際のケースは1から開始)としてエンコードされ、{} にデシリアライズされます。シーンで一度も設定されない OneOf フィールドも安全です。 |
プリミティブまたは配列スキーマから直接定義されたコンポーネント(5ec1a8a1) | すべてのアクセサーが格納値の真偽値を検査していたため、0、false、'' を保持するコンポーネントは存在しないと報告されていました。get は「not found」をスローし、getOrNull/deleteFrom は null を返し、getMutable は例外をスローし、getOrCreateMutable は「already exists」をスローし、create は既存の偽値を上書きして黙って成功していました。deepReadonly も非オブジェクトを展開していたため、数値は {} に、配列はインデックスをキーにしたオブジェクトになっていました。 | マップに値が存在するかどうかを確認し、deepReadonly はプリミティブをそのまま渡し、配列は配列としてコピーします。 |
ReadonlyPrimitive の型を狭めました(5ec1a8a1) | 配列型も含まれていたため、DeepReadonly<number[]> は number[] に戻っていました。.push() は型チェックを通過してから、配列がフリーズされているため実行時に例外をスローしていました。 | ReadonlyPrimitive = number | string | boolean になりました。配列の読み取りは ReadonlyArray 分岐を通るようになり、.push() は型チェックを通らなくなりました。 変更する前に配列をコピーしてください([...arr])。これはパブリック API の変更です。コンポーネントから読み取った配列を変更していた既存コードは、新たにコンパイルに失敗する場合があります。そのコードは実行時にすでに壊れていました。 |
同じリリースの関連変更として、@dcl/sdk のプレイヤーヘルパーが返す Player.wearables と Player.emotes は、フリーズされたコンポーネント配列のエイリアスではなくコピーになりました。そのため、これらに書き込んでも例外が発生しなくなりました。
親子同期関係
親子関係のある同期エンティティでは、Transform.parent を設定する代わりに parentEntity() を使用してください。
import {
syncEntity,
parentEntity,
getParent,
getChildren,
removeParent,
} from '@dcl/sdk/network'
const parent = engine.addEntity()
const child = engine.addEntity()
syncEntity(parent, [Transform.componentId], 1)
syncEntity(child, [Transform.componentId], 2)
// Use parentEntity() — NOT Transform.parent
parentEntity(child, parent)
const parentRef = getParent(child)
const childrenArray = Array.from(getChildren(parent))
// Remove parent relationship
removeParent(child)
接続状態
プレイヤーが同期ルームに接続されているか確認します。
import { isStateSyncronized } from '@dcl/sdk/network'
engine.addSystem(() => {
if (!isStateSyncronized()) return // wait for sync
// safe to read/write synced state
})
注: SDK では、この関数名は isStateSyncronized(「Synchronized」ではありません)と綴られています。
MessageBus
プレイヤー間でカスタムメッセージを送信します(送信専用で、永続化されません)。
import { MessageBus } from '@dcl/sdk/message-bus'
const bus = new MessageBus()
bus.on('hit', (data: { damage: number }) => {
console.log('Took damage:', data.damage)
})
bus.emit('hit', { damage: 10 })
権威サーバーのシーン:
MessageBusはクライアント専用です。ヘッドレスサーバーのランタイムでは、依存しているレガシー通信イベントが実装されていないため、モジュールスコープでnew MessageBus()を実行すると(上記のように)、サーバー上でRemoteError: not implementedが発生します。このようなシーンでは、クライアント側の分岐内(if (!isServer()))でのみインスタンスを生成してください。詳しくはauthoritative-serverスキルを参照してください。
syncEntity と MessageBus の比較
syncEntity:途中参加のプレイヤーも現在の状態を取得でき、競合は自動的に解決されます。CRDT は最終書き込み優先のため、2 人のプレイヤーが同じコンポーネントを同時に変更した場合は、最後の書き込みが優先されます。シーンに 1 人以上のプレイヤーが残っている限り、状態は保持されますMessageBus:送信専用のため、途中参加のプレイヤーは過去のメッセージを受信できません。一時的なエフェクトに適しています
バイナリ MessageBus(パフォーマンス最適化)
通常の MessageBus は、送信前にすべてのペイロードを JSON エンコードします。高頻度のメッセージや大きなペイロードには、raw の Uint8Array データを直接送信する、より低レベルなバイナリの代替手段があります。両端での JSON シリアライズを省略できるため、処理が高速になります。これは syncEntity が内部で使用しているものと同じトランスポートです。
次の場合に使用してください。
- 1 秒あたりに多数のメッセージを送信する場合(例:連続的な移動データのストリーム、頻繁に実行されるループ内でのパーティクルのトリガー)
- ペイロードのサイズが重要な場合(バイナリエンコードは JSON よりコンパクトです)
- すでにバイナリデータがある場合(例:事前エンコード済みのバッファー、CRDT の差分)
使いやすさよりパフォーマンスが重要でない低頻度のイベントには、通常の MessageBus を使用してください。
import { sendBinary } from '~system/CommunicationsController'
import { executeTask } from '@dcl/sdk/ecs'
// Send a binary message to all peers (or a specific subset via peerData)
executeTask(async () => {
const payload = new Uint8Array([1, 2, 3, 4]) // your encoded data
const response = await sendBinary({
data: [payload],
peerData: undefined, // optional: target specific peers
})
// response.data is a Uint8Array[] of messages received from other peers
for (const incoming of response.data) {
handleBinaryMessage(incoming)
}
})
Uint8Array ペイロードのエンコードとデコードは、ご自身で行う必要があります(例:DataView、TextEncoder/TextDecoder、protobuf を使用)。on/emit/topic のレイヤーはありません。sendBinary は、保留中の送信メッセージを送信し、受信メッセージを返す単一の呼び出しです。そのため通常は、各ティックでシステムから呼び出します。
REST API と署名付き Fetch
リーダーボード、ゲーム状態の永続化、プレイヤー認証など、ご自身のバックエンドとの通信には fetch または signedFetch を使用してください。すべてのネットワーク呼び出しは executeTask 内で実行する必要があります。signedFetch は、プレイヤーのウォレット ID を証明する暗号学的な証明を付加します。リクエストを行っているユーザーをサーバー側で検証する必要がある場合に使用してください。
fetch と signedFetch の詳しい使用パターンについては、scene-runtime スキルを参照してください。
WebSocket 接続
再接続、ハートビート、メッセージ形式を含む WebSocket の詳しい使用パターンについては、{baseDir}/references/networking-patterns.md を参照してください。
基本的な接続
executeTask(async () => {
const ws = new WebSocket('wss://example.com/ws')
ws.onopen = () => {
console.log('Connected to WebSocket')
ws.send(JSON.stringify({ type: 'join', playerId: 'player123' }))
}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
switch (msg.type) {
case 'gameState':
handleGameState(msg)
break
case 'playerJoin':
handlePlayerJoin(msg)
break
case 'playerLeave':
handlePlayerLeave(msg)
break
}
}
ws.onerror = (error) => console.error('WebSocket error:', error)
ws.onclose = () => console.log('Disconnected')
})
プレイヤーの入退室イベント
シーンにプレイヤーが入ったとき、またはシーンから退出したときを検出します。
import { onEnterScene, onLeaveScene } from '@dcl/sdk/src/players'
onEnterScene((player) => {
console.log('Player entered:', player.userId)
})
onLeaveScene((userId) => {
console.log('Player left:', userId)
})
SDK の Observable(低レベル)
SDK は、@dcl/sdk/observables から低レベルの Observable(onPlayerClickedObservable、onEnterSceneObservable、onLeaveSceneObservable、onRealmChangedObservable、onPlayerExpressionObservable、onProfileChangedObservable)も公開しています。これらは、上記の onEnterScene/onLeaveScene ヘルパーの基盤となるプリミティブです。最近の修正について、次の点に注意してください。
注意点:リモートプレイヤーの Transform はワールド座標です
ローカルプレイヤーの Transform はシーンローカルですが、別のプレイヤーのエンティティはワールド座標を報告します。座標を比較するには basePos * 16 を減算してください(player-avatar >「リモートプレイヤーの Transform はワールド座標」参照)。PlayerIdentityData を反復処理する距離判定、近接度によるリーダーボード、追従カメラでは、この点に注意してください。
プレイヤーごとのビジュアルは自動的には共有されません
マルチプレイヤー向けに見えるコンポーネントでも、クライアントローカルなものがあります。リモートプレイヤーのエンティティに書き込んでも、書き込んだクライアントにしか反映されません。これらは中継されず、syncEntity も役に立ちません。プレイヤーエンティティはシーンで作成されたものではなく、エンジンが所有しているためです。
AvatarNametag(アバターの上に表示されるランク/ロールのプレート。@dcl/sdk7.28.0+)はクライアントローカルです。すべてのプレイヤーに全員分の同じプレートを表示するには、各クライアントがプレート一式を計算して書き込む必要があります。モバイルではまだ実装されていません。以前は v1.14.0 で提供予定とされていましたが、2026 年 10 月の更新(ドキュメントのコミット5f212e3、モバイルアプリ v1.15.0)で 未実装、提供時期未定 に戻りました。モバイルプレイヤーにはプレートがまったく表示されないため、プレイヤーが必要とする情報をプレートだけで伝えないでください。ランク/ロールをTextShape、HUD リスト、またはアバターがすでに身に着けている色でも表示してください。
クライアントローカルのビジュアルをクライアント間で一致させる方法は 2 つあります。
- すべてのクライアントがすでに持っているデータから導出します。たとえば、すべての
PlayerIdentityDataアドレスを並べ替えて固定ロスターを参照します。同じアドレス一覧からは、参加順に関係なく、どのクライアントでも同じ結果が得られます。メッセージは不要で、ずれも生じません。scenes/4,24-avatar-nametagがこのパターンを使用しています。ロスター全体のコード(差分チェック、間隔を空けた再スキャン)は、player-avatar のreferences/avatar-nametag.mdにあります。 - ビジュアルではなく、入力を複製します。割り当てを同期し(同期コンポーネントまたは
MessageBusメッセージ)、各クライアントがそのユーザー ID に対応するプレイヤーエンティティをローカルで特定して適用します。
どちらの場合も、書き込むたびにユーザー ID から対象の Entity を解決してください。プレイヤーエンティティ ID は切断後に再利用されるため、キャッシュされた Entity に書き込むと別のプレイヤーに反映される場合があります。
マルチプレイヤーのテスト
ローカルプレビューのレルムに接続された各クライアントは、それぞれ別のプレイヤーとして扱われます。ただし、デスクトップ Explorer では一度に 1 つのインスタンスしか実行できません。Creator Hub で 2 回目の「プレビュー」を押すと、すでに開いているウィンドウにフォーカスが戻るだけで、2 人目のプレイヤーは追加されません。ローカルクライアントを 2 つ起動する方法は 2 つあります。
- 2 つ目のデスクトップインスタンス:Creator Hub で Play Options(Play ボタンのドロップダウン矢印)を開き、Desktop Client の行にカーソルを合わせてフライアウトを開き、Multi-Instance Preview にチェックを入れます(
@dcl/sdk>= 7.20.4 が必要です)。または、npx sdk-commands start --multi-instanceを実行します。その後、もう一度 Play を押すか、コマンドを再実行します。-n(「Client がすでに起動していても、新しいインスタンスを開く」)を指定すると、単独で新しいウィンドウが開きます。マルチインスタンスでは各ウィンドウで異なるアカウントを使って認証する必要があるため、Skip Auth Screen は無効になります。各インスタンスで認証画面が表示されるので、それぞれ異なるウォレットでサインインしてください。 - デスクトップ+ブラウザー:デスクトップクライアントを開いたまま、Bevy ウェブクライアントを 2 人目のプレイヤーとして追加します。Play Options から Web (Bevy) クライアントの行を選択するか、
npx sdk-commands start --web(別名--bevy-web)を実行すると、https://decentraland.org/bevy-web/?preview=true&realm=<local realm origin>(--dclenv zoneの場合は.zone)が開きます。ブラウザーのタブ数に上限はありません。
decentraland:// のディープリンクによるプレビュー起動(realm=…&position=…&dclenv=…&local-scene=true)にも同じシングルインスタンスのルールが適用されます。ただし、open-deeplink-in-new-instance=true(-n による指定)または multi-instance=true が含まれている場合は除きます。手入力するのではなく、CLI または Creator Hub でディープリンクを生成してください。
オフラインモード
マルチプレイヤーを必要としない Decentraland Worlds の場合:
{
"worldConfiguration": {
"fixedAdapter": "offline:offline"
}
}
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
Profile not initialized. Call syncEntity inside main() | syncEntity がモジュールのトップレベル(例:モジュール初期化時)で呼び出された | すべての syncEntity 呼び出し(およびそれらに依存するエンティティの作成)を、main() から呼び出される関数内に移動してください。モジュールの読み込み時に syncEntity を呼び出さないでください。engine.addSystem() にも同じことが当てはまります。 |
| プレイヤー間で状態が同期されない | syncEntity() の呼び出しがない | 共有したい各エンティティで、syncEntity(entity, [ComponentId1, ComponentId2]) を呼び出してください |
| 同期 ID の衝突 | 2 つのエンティティが同じ数値の同期 ID を共有している | 列挙型を使用して、事前定義された同期対象エンティティすべてに一意の ID を割り当ててください |
| 参加時に状態が準備できていない | 同期が完了する前に同期状態を読み取っている | システム内で if (!isStateSyncronized()) return を使用してチェックしてください |
| MessageBus のメッセージが失われる | 遅れて参加したプレイヤーが過去のメッセージを期待している | MessageBus は送信後に保持されません。永続的な状態には syncEntity を使用してください |
一貫性の保証、サーバー側での検証、またはチート対策が必要ですか?
syncEntityとMessageBusは完全には信頼できません。すべてのプレイヤーに同じ状態変更を表示することが重要な場合は、ヘッドレスサーバーのパターンについて、authoritative-server スキルを参照してください。競技性のあるゲームの完全なアーキテクチャ(サーバー側の近接検証によるチート対策、チェックポイント限定の Storage 永続化、変更頻度によるコンポーネントのアトミックな分割)については、Gem Rush のリファレンスシーン(92,-9-authoritative-server-gem-rush)を参照してください。
サンプルシーン
エンジンチームのテストシーンで、実際のエンジンを使って検証されています。
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/88,-13-avatar-masks — サーバーレスの
syncEntity。このスキルで説明しているパターンです。enum SyncIdで2つのシングルトンに固定IDを割り当て、syncEntity(crateAnchorEntity, [AvatarAttach.componentId], SyncId.CRATE_ANCHOR)とsyncEntity(crateEntity, [Transform.componentId], SyncId.CRATE)を呼び出し、@dcl/sdk/networkのparentEntity/removeParentを使って、プレイヤー間で共有クレートを受け渡します。どのクライアントも変更できます。validateBeforeChangeもisServer()分岐もありません。(このシーンの主な機能はエモートマスクですが、同期はその機能を支えるものです。)
対照的に、このセット内のもう一つのマルチプレイヤーシーンはサーバー権威型です。こちらを使って、このパターンとここで説明しているサーバーレスのパターンの違いを確認してください。
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/90,-9-authoritative-server-leaderboard — 権威型(サーバーレスではありません)。
syncEntityを呼び出すのはサーバーのみで、同期コンポーネントはvalidateBeforeChangeでロックされているため、クライアントは読み取りとメッセージの送信のみができます。代わりに、どのクライアントからも共有状態を直接変更できるようにしたい場合(このスキルで説明しているパターン)は、各クライアントが独自にsyncEntityを呼び出し、validateBeforeChangeは使いません。このシーンの詳しい説明については、authoritative-server スキルを参照してください。