multiplayer-sync
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMultiplayer Synchronization in Decentraland
Decentraland 中的多人同步
Decentraland runs scenes locally in a player's instance of the explorer. By default, players are able to see each other and interact directly, but each player interacts with the environment independently. Changes in the environment aren't shared between players by default.
To sync any changes in the scene state, SDK7 uses CRDT-based synchronization.
Runtime constraint: Decentraland runs in a QuickJS sandbox. No Node.js APIs (,fs,http,path). Useprocessandfetch()for network communication. See the scene-runtime skill for async patterns.WebSocket
Decentraland 会在玩家自己的 explorer 实例中本地运行场景。默认情况下,玩家能够看到彼此并直接互动,但每位玩家都独立与环境交互。环境中的更改默认不会在玩家之间共享。
要同步场景状态中的任何更改,SDK7 使用基于 CRDT 的同步。
运行时约束: Decentraland 在 QuickJS 沙箱中运行。不支持 Node.js APIs (,fs,http,path)。使用process和fetch()进行网络通信。有关异步模式,请参阅 scene-runtime skill。WebSocket
Sync Strategy Decision Tree
同步策略决策树
Choose the right networking approach based on what you need:
| Strategy | Use When | Persistence | Example |
|---|---|---|---|
| Shared state that all players see and that persists for new arrivals | Yes — state survives player join/leave, but only as long as at least one player remains in the scene. The state resets as soon as the scene is empty | Doors, switches, scoreboards, elevators |
| Ephemeral events that only matter in the moment | No — late joiners miss past messages | Chat messages, sound effects, particle triggers |
| Reading or writing data to an external server | Server-dependent | Leaderboards, inventory, external game state |
| Authenticated requests that prove player identity | Server-dependent | Claiming rewards, submitting verified scores |
| Real-time bidirectional communication with a server | Connection-dependent | Live game servers, real-time chat. |
Decision flow:
- Does every player need to see the same state, including late joiners? -->
syncEntity - Is it a fire-and-forget event only for players currently in the scene? -->
MessageBus - Do you need the information to be persisted even after all players leave, or to run secure validations on that information? --> or
fetchsignedFetch - Do you need continuous real-time server communication? -->
WebSocket - Combine approaches freely: use for world state,
syncEntityfor effects, andMessageBusfor persistence.fetch
根据需求选择合适的网络方式:
| 策略 | 使用场景 | 持久化 | 示例 |
|---|---|---|---|
| 所有玩家都能看到且需要为后来加入者保留的共享状态 | 是——状态会在玩家加入或离开后继续存在,但仅在场景中至少还有一名玩家时有效。场景一旦变空,状态就会重置 | 门、开关、记分板、电梯 |
| 只在当下有意义的临时事件 | 否——后来加入者会错过之前的消息 | 聊天消息、音效、粒子触发器 |
| 读取或写入外部服务器中的数据 | 取决于服务器 | 排行榜、物品栏、外部游戏状态 |
| 可证明玩家身份的已认证请求 | 取决于服务器 | 领取奖励、提交经验证的分数 |
| 与服务器进行实时双向通信 | 取决于连接 | 实时游戏服务器、实时聊天 |
决策流程:
- 每位玩家是否都需要看到相同的状态,包括后来加入者? -->
syncEntity - 是否只需向当前在场景中的玩家发送即发即弃事件? -->
MessageBus - 是否需要信息在所有玩家离开后仍持久保存,或需要对这些信息执行安全验证? --> 或
fetchsignedFetch - 是否需要与服务器持续进行实时通信? -->
WebSocket - 可以自由组合这些方式:使用 同步世界状态,使用
syncEntity同步效果,并使用MessageBus进行持久化。fetch
syncEntity Essentials
syncEntity 基础
Import and Basic Usage
导入与基本用法
typescript
import { engine, Transform, MeshRenderer, Material } from '@dcl/sdk/ecs'
import { syncEntity } from '@dcl/sdk/network'
import { Vector3, Color4 } from '@dcl/sdk/math'Signature:
syncEntity(entity, componentIds[], syncId?)- — the entity to synchronize
entity - — array of component IDs to keep in sync (e.g.,
componentIds[])[Transform.componentId] - — unique numeric identifier (required for predefined entities, optional for player-spawned entities)
syncId
typescript
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 - — 要保持同步的组件 ID 数组(例如
componentIds[])[Transform.componentId] - — 唯一的数字标识符(预定义实体必填,玩家生成的实体可选)
syncId
Enum Sync IDs (Predefined Entities)
枚举同步 ID(预定义实体)
Every predefined synced entity MUST have a unique numeric ID. Use an enum to avoid collisions:
typescript
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
)Predefined entities (with a sync ID) persist after the creating player leaves. Player-created entities (no sync ID) are removed when the player disconnects.
Best practice — always give singletons a stable sync ID. Auto IDs derive identity from the creating peer + its local engine entity number (which the engine recycles). A singleton synced entity that is destroyed and recreated repeatedly with an auto ID is fragile over real network comms: it may fail to reconcile on remote clients (they see only default component data) even though it works perfectly in local single-process preview. Assign any singleton or small fixed set of well-known synced entities a STABLE explicit sync ID from a reserved enum. Reserve auto IDs for genuinely dynamic, many-instance, create-and-forget entities. Also: nevera fixed-ID synced entity and recreate it with the same ID in the SAME frame — the internalremoveEntitysurvives until a later CRDT flush, so recreating immediately throwsNetworkEntity; defer the re-spawn to a later tick. Seeid provided is already in use(syncEntity identity section) for the full failure-mode signature, fix, and the optimistic-prediction companion pattern.{baseDir}/references/networking-patterns.md
每个预定义同步实体都必须有唯一的数字 ID。使用枚举可避免冲突:
typescript
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 根据创建该实体的对等端及其本地 engine 实体编号生成(engine 会复用实体编号)。在真实网络通信中,反复销毁并使用自动 ID 重新创建的单例同步实体非常脆弱:它可能无法在远程客户端上完成协调(远程客户端只能看到默认组件数据),尽管它在本地单进程预览中运行完全正常。请使用保留枚举中的稳定显式同步 ID,为任何单例实体或少量固定且已知的同步实体分配 ID。自动 ID 仅应用于真正动态、数量众多、创建后即用即弃的实体。另外:切勿在同一帧中固定 ID 的同步实体后,再使用相同 ID 重新创建——内部removeEntity会一直保留到后续 CRDT 刷新,因此立即重新创建会抛出NetworkEntity;应将重新生成操作推迟到后续 tick。有关完整的故障模式特征、修复方法和配套的乐观预测模式,请参阅id provided is already in use(syncEntity identity 部分)。{baseDir}/references/networking-patterns.md
Auto-Generated IDs (Player-Spawned Entities)
自动生成的 ID(玩家生成的实体)
Entities created at runtime by players do not need an explicit sync ID:
typescript
function createProjectile() {
const projectile = engine.addEntity()
Transform.create(projectile, { position: Vector3.create(4, 1, 4) })
MeshRenderer.setSphere(projectile)
syncEntity(projectile, [Transform.componentId])
return projectile
}Some visuals are inherently per-player, not shared. A(camera-facing) is recomputed locally in each explorer, so every player sees it facing themselves — this is not synced and needs noBillboard. The exception is asyncEntitywith aBillboard: because the target's position is scene state, all players see that billboard oriented the same way. UsetargetEntitywhen you need a shared, consistent orientation (e.g. a sign that points at a shared object). See thetargetEntity/player-avatarcomponent reference for Billboard details.sdk-scenes
运行时由玩家创建的实体无需显式同步 ID:
typescript
function createProjectile() {
const projectile = engine.addEntity()
Transform.create(projectile, { position: Vector3.create(4, 1, 4) })
MeshRenderer.setSphere(projectile)
syncEntity(projectile, [Transform.componentId])
return projectile
}某些视觉效果本质上仅属于单个玩家,而非共享状态。(朝向相机)会在每个 explorer 中本地重新计算,因此每位玩家都会看到它朝向自己;它不会被同步,也不需要Billboard。例外是带syncEntity的targetEntity:由于目标的位置属于场景状态,所有玩家看到的该 billboard 朝向都一致。当需要共享且一致的朝向时(例如指向共享对象的标牌),请使用Billboard。有关targetEntity的详细信息,请参阅Billboard/player-avatar组件参考。sdk-scenes
Custom Synced Components
自定义同步组件
Define custom components and sync them between players:
typescript
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()
}Usefor timestamps and other large numbers.Schemas.Int64/Schemas.Numbercorrupt values over 13 digits (likeSchemas.Int) — always store such values inDate.now()(asSchemas.Int64above does).lastUpdated
Custom schemas must be deterministic: the same component name must map to the same schema across all clients.
定义自定义组件并在玩家之间同步:
typescript
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会使超过 13 位的数值(例如Schemas.Int)失真——始终使用Date.now()存储此类值(如上面的Schemas.Int64)。lastUpdated
自定义 schema 必须具有确定性: 所有客户端中,同一组件名称必须映射到同一 schema。
Player-Specific Data
玩家专属数据
Use to distinguish players:
PlayerIdentityDatatypescript
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)
}
})使用 区分玩家:
PlayerIdentityDatatypescript
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)
}
})Schema Types
Schema 类型
Available schema types for custom components:
| Type | Usage |
|---|---|
| true/false |
| Integer numbers |
| Decimal numbers |
| Text strings |
| Large integers (timestamps) |
| 3D coordinates |
| Rotations |
| RGB colors |
| RGBA colors |
| Entity reference |
| Array of values |
| Nested struct — |
| Nullable values |
| Numeric enum; |
| String enum; |
| Discriminated union ( |
Example with the less common schema types:
typescript
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),
})自定义组件可用的 schema 类型:
| 类型 | 用途 |
|---|---|
| true/false |
| 整数 |
| 小数 |
| 文本字符串 |
| 大整数(时间戳) |
| 3D 坐标 |
| 旋转 |
| RGB 颜色 |
| RGBA 颜色 |
| 实体引用 |
| 数组值 |
| 嵌套结构—— |
| 可空值 |
| 数字枚举; |
| 字符串枚举; |
| 判别联合( |
较不常见的 schema 类型示例:
typescript
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),
})Parent-Child Sync Relationships
父子同步关系
For synced entities with parent-child relationships, use instead of setting :
parentEntity()Transform.parenttypescript
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)对于具有父子关系的同步实体,请使用 ,而不是设置 :
parentEntity()Transform.parenttypescript
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)Connection State
连接状态
Check if the player is connected to the sync room:
typescript
import { isStateSyncronized } from '@dcl/sdk/network'
engine.addSystem(() => {
if (!isStateSyncronized()) return // wait for sync
// safe to read/write synced state
})Note: The function is spelled (not "Synchronized") in the SDK.
isStateSyncronized检查玩家是否已连接到同步房间:
typescript
import { isStateSyncronized } from '@dcl/sdk/network'
engine.addSystem(() => {
if (!isStateSyncronized()) return // wait for sync
// safe to read/write synced state
})注意: 该函数在 SDK 中的拼写是 (而不是 “Synchronized”)。
isStateSyncronizedMessageBus
MessageBus
Send custom messages between players (fire-and-forget, no persistence):
typescript
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 })Authoritative-server scenes:is client-only — the headless server runtime does not implement the legacy comms event it relies on, and a module-scopeMessageBus(as above) fails on the server withnew MessageBus(). In those scenes construct it only inside the client branch (RemoteError: not implemented); see theif (!isServer())skill.authoritative-server
在玩家之间发送自定义消息(即发即弃,不持久化):
typescript
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 })服务器权威型场景:仅支持客户端——无头服务器运行时未实现其所依赖的旧版 comms 事件,因此如上所示在模块作用域中执行MessageBus会在服务器上失败并报错new MessageBus()。在这些场景中,只应在客户端分支(RemoteError: not implemented)内创建它;参阅if (!isServer())skill。authoritative-server
syncEntity vs MessageBus
syncEntity 与 MessageBus
- : late joiners get current state, automatic conflict resolution — CRDT last-write-wins, so if two players change the same component simultaneously the last write wins. The state persists as long as at least one player remains in the scene
syncEntity - : fire-and-forget, late joiners miss past messages, good for transient effects
MessageBus
- :后来加入者会获得当前状态,并自动解决冲突——CRDT 采用最后写入优先(last-write-wins),因此如果两名玩家同时修改同一组件,则最后一次写入获胜。只要场景中至少还有一名玩家,状态就会持续存在
syncEntity - :即发即弃,后来加入者会错过过去的消息,适合瞬时效果
MessageBus
Binary MessageBus (Performance Optimization)
二进制 MessageBus(性能优化)
The regular JSON-encodes every payload before sending. For high-frequency messages or large payloads, there's a lower-level binary alternative that sends raw data directly — faster to process because it skips JSON serialization on both ends. This is the same transport uses internally.
MessageBusUint8ArraysyncEntityUse it when:
- You are emitting many messages per second (e.g., continuous movement streams, particle triggers in tight loops)
- Payload size matters (binary encoding is more compact than JSON)
- You already have binary data (e.g., pre-encoded buffers, CRDT deltas)
Stick with the regular for low-frequency events where ergonomics beat performance.
MessageBustypescript
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)
}
})You are responsible for encoding/decoding the payloads yourself (e.g., with , /, or protobuf). There's no on/emit/topic layer — is a single call that both sends pending outgoing messages and returns incoming ones, so you typically drive it from a system on each tick.
Uint8ArrayDataViewTextEncoderTextDecodersendBinary常规 会在发送前将每个 payload 编码为 JSON。对于高频消息或大型 payload,可以使用更底层的二进制替代方案,直接发送原始 数据——由于两端都省去了 JSON 序列化,因此处理速度更快。这与 内部使用的传输机制相同。
MessageBusUint8ArraysyncEntity在以下情况下使用:
- 每秒发送大量消息(例如在紧凑循环中持续发送移动流、粒子触发器)
- payload 大小很重要(二进制编码比 JSON 更紧凑)
- 已有二进制数据(例如预先编码的缓冲区、CRDT 增量)
对于低频事件,如果易用性比性能更重要,请使用常规 。
MessageBustypescript
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)
}
})你需要自行负责编码和解码 payload(例如使用 、/ 或 protobuf)。它没有 on/emit/topic 抽象层—— 是一次调用,既会发送待发消息,也会返回收到的消息,因此通常应在每个 tick 中由 system 驱动它。
Uint8ArrayDataViewTextEncoderTextDecodersendBinaryREST API and Signed Fetch
REST API 与 Signed Fetch
For communicating with your own backend (leaderboards, game state persistence, player auth), use or . All network calls must run inside . attaches a cryptographic proof of the player's wallet identity — use it when your server needs to verify who is making the request.
fetchsignedFetchexecuteTasksignedFetchSee the scene-runtime skill for full and patterns.
fetchsignedFetch如需与自己的后端通信(排行榜、游戏状态持久化、玩家身份验证),请使用 或 。所有网络调用都必须在 中运行。 会附加玩家钱包身份的加密证明——当服务器需要验证请求者身份时,请使用它。
fetchsignedFetchexecuteTasksignedFetch有关完整的 和 模式,请参阅 scene-runtime skill。
fetchsignedFetchWebSocket Connections
WebSocket 连接
For full WebSocket patterns (reconnection, heartbeat, message format), see .
{baseDir}/references/networking-patterns.md有关完整的 WebSocket 模式(重连、心跳、消息格式),请参阅 。
{baseDir}/references/networking-patterns.mdBasic Connection
基础连接
typescript
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')
})typescript
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')
})Player Enter/Leave Events
玩家进入和离开事件
Detect players entering or leaving the scene:
typescript
import { onEnterScene, onLeaveScene } from '@dcl/sdk/src/players'
onEnterScene((player) => {
console.log('Player entered:', player.userId)
})
onLeaveScene((userId) => {
console.log('Player left:', userId)
})检测玩家进入或离开场景:
typescript
import { onEnterScene, onLeaveScene } from '@dcl/sdk/src/players'
onEnterScene((player) => {
console.log('Player entered:', player.userId)
})
onLeaveScene((userId) => {
console.log('Player left:', userId)
})Multiplayer Testing
多人游戏测试
Open multiple browser windows to test multiplayer locally. Each window is a separate player.
在本地打开多个浏览器窗口测试多人游戏。每个窗口代表一名独立玩家。
Offline Mode
离线模式
For Decentraland Worlds that do not need multiplayer:
json
{
"worldConfiguration": {
"fixedAdapter": "offline:offline"
}
}对于不需要多人游戏的 Decentraland World:
json
{
"worldConfiguration": {
"fixedAdapter": "offline:offline"
}
}Troubleshooting
故障排除
| Problem | Cause | Solution |
|---|---|---|
| | Move all |
| State not syncing between players | Missing | Every entity you want shared must call |
| Sync ID collision | Two entities share the same numeric sync ID | Use an enum to assign unique IDs to every predefined synced entity |
| State not ready on join | Reading synced state before sync completes | Guard with |
| MessageBus messages lost | Late joiner expecting past messages | MessageBus is fire-and-forget. Use |
Need guaranteed consistency, server-side validation, or anti-cheat?andsyncEntityare not entirely reliable — if it's important that all players see the same state change, see the authoritative-server skill for the headless server pattern. For a complete competitive game architecture (anti-cheat with server-side proximity validation, checkpoint-only Storage persistence, atomic component splits by change rate), see the Gem Rush reference scene (MessageBus).92,-9-authoritative-server-gem-rush
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 在模块顶层调用了 | 将所有 |
| 玩家之间的状态未同步 | 缺少 | 每个希望共享的实体都必须调用 |
| 同步 ID 冲突 | 两个实体共享同一个数字同步 ID | 使用枚举为每个预定义同步实体分配唯一 ID |
| 加入时状态尚未就绪 | 在同步完成前读取同步状态 | 在 system 中使用 |
| MessageBus 消息丢失 | 后来加入者期待收到过去的消息 | MessageBus 是即发即弃的。持久状态请使用 |
需要保证一致性、服务端验证或反作弊?和syncEntity并非完全可靠——如果必须确保所有玩家看到相同的状态更改,请参阅 authoritative-server skill,了解无头服务器模式。若需要完整的竞技游戏架构(包含带服务端邻近验证的反作弊、仅检查点 Storage 持久化、按变化速率进行原子组件拆分),请参阅 Gem Rush 参考场景(MessageBus)。92,-9-authoritative-server-gem-rush
Example scenes
示例场景
No serverless / reference scene is available in the engine-team test set yet. For contrast, the closest multiplayer scene is server-authoritative (use it to see how the authoritative pattern differs from the serverless one described here):
syncEntityMessageBus- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/90,-9-authoritative-server-leaderboard — authoritative (NOT serverless): only the server calls , and synced components are locked with
syncEntityso clients can only read them and send messages. If you instead want any client to mutate shared state directly (the pattern this skill documents), each client callsvalidateBeforeChangeon its own and there is nosyncEntity. See the authoritative-server skill for that scene's full breakdown.validateBeforeChange
引擎团队测试集中目前还没有无服务器的 / 参考场景。作为对比,最接近的多人场景采用服务器权威模式(可参考它了解权威模式与本文所述无服务器模式有何差异):
syncEntityMessageBus- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/90,-9-authoritative-server-leaderboard — 服务器权威型(非无服务器):只有服务器调用 ,并使用
syncEntity锁定同步组件,因此客户端只能读取这些组件并发送消息。如果希望任何客户端直接修改共享状态(也就是本 skill 记录的模式),则每个客户端都对自己的实体调用validateBeforeChange,且不使用syncEntity。有关该场景的完整详解,请参阅 authoritative-server skill。validateBeforeChange