multiplayer-sync

Synchronize state between players in Decentraland multiplayer scenes using CRDT-based networking. Use when user wants multiplayer, sync state, network entities, shared world state, or real-time collaboration.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "multiplayer-sync" with this command: npx skills add dcl-regenesislabs/opendcl/dcl-regenesislabs-opendcl-multiplayer-sync

Multiplayer Synchronization in Decentraland

Decentraland scenes are inherently multiplayer. All players in the same scene share the same space. SDK7 uses CRDT-based synchronization.

How Sync Works

  • Entities must be explicitly synced using syncEntity() from @dcl/sdk/network.
  • The Decentraland runtime uses CRDTs (Conflict-free Replicated Data Types) to resolve conflicts.
  • Last-write-wins semantics for most components (Transform, Material, etc.).
  • No server code needed — sync is built into the runtime.

Basic Synced Entity

Use syncEntity() to mark an entity and its components for multiplayer sync:

import { engine, Transform, MeshRenderer, Material } from '@dcl/sdk/ecs'
import { syncEntity } from '@dcl/sdk/network'
import { Vector3, Color4 } from '@dcl/sdk/math'

// Create entity
const sharedCube = engine.addEntity()
Transform.create(sharedCube, { position: Vector3.create(8, 1, 8) })
MeshRenderer.setBox(sharedCube)
Material.setPbrMaterial(sharedCube, { albedoColor: Color4.Red() })

// Sync this entity's Transform to all players
syncEntity(sharedCube, [Transform.componentId])

// When any player changes the transform, all players see it
function moveCube() {
  const transform = Transform.getMutable(sharedCube)
  transform.position.x += 1  // All players see this change
}

Custom Synced Components

Define custom components and sync them between players:

import { engine, Schemas } from '@dcl/sdk/ecs'
import { syncEntity } from '@dcl/sdk/network'

// Define a custom component
const ScoreBoard = engine.defineComponent('scoreBoard', {
  score: Schemas.Int,
  playerName: Schemas.String,
  lastUpdated: Schemas.Int64
})

// Create and sync the entity
const board = engine.addEntity()
ScoreBoard.create(board, { score: 0, playerName: '', lastUpdated: 0 })
syncEntity(board, [ScoreBoard.componentId])

// Update from any player — synced via CRDT
function addScore(points: number) {
  const data = ScoreBoard.getMutable(board)
  data.score += points
  data.lastUpdated = Date.now()
}

Player-Specific Data

Use PlayerIdentityData to distinguish players:

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

Available schema types for custom components:

TypeUsage
Schemas.Booleantrue/false
Schemas.IntInteger numbers
Schemas.FloatDecimal numbers
Schemas.StringText strings
Schemas.Int64Large integers (timestamps)
Schemas.Vector33D coordinates
Schemas.QuaternionRotations
Schemas.Color3RGB colors
Schemas.Color4RGBA colors
Schemas.EntityEntity reference
Schemas.Array(innerType)Array of values
Schemas.Map(valueType)Key-value maps
Schemas.Optional(innerType)Nullable values
Schemas.Enum(enumType)Enum values

Communication Patterns

Global State (Shared Object)

// One entity holds shared game state
const gameState = engine.addEntity()
const GameState = engine.defineComponent('gameState', {
  phase: Schemas.String,
  timeRemaining: Schemas.Int,
  isActive: Schemas.Boolean
})
GameState.create(gameState, { phase: 'waiting', timeRemaining: 60, isActive: false })

Per-Player State

// Track each player's state separately using their entity
engine.addSystem(() => {
  for (const [entity] of engine.getEntitiesWith(PlayerIdentityData)) {
    // Each player's entity is unique to them
    // Attach custom components to player entities for per-player data
  }
})

Entity Enum IDs

Distinguish predefined entities from player-created ones using entityEnumId:

syncEntity(door, [Transform.componentId], 1)   // predefined entity (enum ID 1)
syncEntity(door2, [Transform.componentId], 2)  // predefined entity (enum ID 2)
syncEntity(playerBox, [Transform.componentId]) // no enum ID = player-created, lives with the player

Predefined entities (with an entityEnumId) persist after the creating player leaves. Player-created entities (no enum ID) are removed when the player disconnects.

Parent-Child Relationships

Use parentEntity to create entity hierarchies that sync correctly:

import { parentEntity, getParent, getChildren } from '@dcl/sdk/ecs'

parentEntity(child, parent)
const parent = getParent(child)
const children = getChildren(parent)

Connection State

Check if the player is connected to the sync room:

import { isStateSynchronized } from '@dcl/sdk/ecs'

engine.addSystem(() => {
  if (!isStateSynchronized()) return // wait for sync
  // safe to read/write synced state
})

MessageBus

Send custom messages between players (fire-and-forget, no persistence):

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 })

Player Enter/Leave Events

Detect players entering or leaving the scene:

import { onEnterScene, onLeaveScene } from '@dcl/sdk/observables'

onEnterScene.add((player) => {
  console.log('Player entered:', player.userId)
})
onLeaveScene.add((player) => {
  console.log('Player left:', player.userId)
})

Offline Testing

Test multiplayer locally without a server using the offline adapter:

{
  "worldConfiguration": {
    "fixedAdapter": "offline:offline"
  }
}

Important Notes

  • Entities must be explicitly synced via syncEntity(entity, [componentIds]) — pass the componentId of each component to sync
  • CRDT resolution: If two players change the same component simultaneously, last-write-wins
  • No server-side code: Decentraland scenes run entirely client-side with CRDT sync
  • Entity limits apply: Each synced entity counts toward the scene's entity budget
  • Custom schemas must be deterministic: Same component name = same schema across all clients
  • Use Schemas.Int64 for timestamps: Schemas.Number corrupts large numbers (13+ digits). Always use Schemas.Int64 for values like Date.now()
  • For server-authoritative multiplayer with validation and anti-cheat, see the authoritative-server skill

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

audio-video

No summary provided by upstream source.

Repository SourceNeeds Review
General

build-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

lighting-environment

No summary provided by upstream source.

Repository SourceNeeds Review
General

deploy-scene

No summary provided by upstream source.

Repository SourceNeeds Review