Drop-in sync nodes

Drop-in sync nodes & interpolation

Replicate a node's movement across the network without writing any send/receive code. Add a SpawnSync node as a child, tick one box on the copy you control, and remote players move smoothly thanks to built-in interpolation.

Zero boilerplate Node2D & Node3D Smooth interpolation Auto register & despawn

What it does

SpawnSync wraps SpawnWeaver's entity-state sync in a node so you don't call set_entity_state/patch_entity_state yourself:

  • On the copy you control (local = true) it registers an entity and sends the parent's position/rotation (and any custom properties) several times a second.
  • On copies of other players (local = false) it listens for that entity's updates and interpolates the parent toward them, so motion looks smooth even though packets arrive a few times per second.
  • It registers on connect and deletes itself when the node leaves the tree — and remote copies free themselves when the entity is removed.

1. Add it to your player scene

Build a normal player scene (a CharacterBody2D, Node2D, Node3D, …). Add a child node, search for SpawnSync, and name it SpawnSync. In the inspector pick what to replicate:

  • Sync position / rotation / scale — toggles for the parent's transform.
  • Synced properties — names of extra parent variables to replicate (e.g. hp, team). Primitives only — for a Color, sync its components.
  • Send rate — updates per second a local node sends (default 20).
  • Interpolate + Interpolation speed — higher is snappier, lower is smoother. Frame-rate independent.

2. Mark the copy you control

The instance this client drives sets local = true. With an empty entity_id, a local node uses your own player id — perfect for one avatar per player. (Objects you spawn need a unique entity_id you choose.)

# On your own avatar, before adding it to the tree:
var me := PlayerScene.instantiate()
me.get_node("SpawnSync").local = true     # this client sends; entity_id defaults to player_id
add_child(me)

3. Spawn copies of other players

SpawnSync handles sending and interpolation, but you decide when to create the remote scenes. A tiny manager watches for entity ids and spawns one player scene each:

extends Node2D
const PlayerScene := preload("res://player.tscn")
var _remotes := {}        # entity_id -> node

func _ready() -> void:
    MultiplayerService.match_found.connect(func(_r, _c, _p): _spawn_local())
    MultiplayerService.state_snapshot_received.connect(_on_snapshot)
    MultiplayerService.entity_state_changed.connect(func(id, _patch, _full): _ensure_remote(id))
    MultiplayerService.entity_state_deleted.connect(func(id): _remotes.erase(id))
    MultiplayerService.connect_using_config()

func _spawn_local() -> void:
    var me := PlayerScene.instantiate()
    me.get_node("SpawnSync").local = true     # registers + sends automatically
    add_child(me)

func _ensure_remote(id: String) -> void:
    if id == MultiplayerService.player_id or _remotes.has(id):
        return
    var p := PlayerScene.instantiate()
    var sync := p.get_node("SpawnSync")
    sync.entity_id = id                        # set before it enters the tree
    add_child(p)                               # now it interpolates this entity
    _remotes[id] = p

func _on_snapshot(snapshot: Dictionary) -> void:
    for e in snapshot.get("entities", []):     # late join: everyone already present
        _ensure_remote(str(e.get("entityId", "")))

That's the whole networking layer. Move your local avatar however you like (input, physics); its SpawnSync streams the transform out, and every remote copy eases into place. When a player leaves, their entity is deleted and the remote scene frees itself.

Custom properties

List extra variables in Synced properties and they ride along with the transform. They're applied immediately on remote copies (not interpolated), which is what you want for things like health or team:

# SpawnSync "Synced properties" = ["hp", "team"]
# Your player script just sets them locally; SpawnSync replicates them:
hp -= 25
# remote copies receive the new hp on the next update

How interpolation works

Updates arrive ~20×/second, but you render at 60+ fps. Instead of snapping on each packet, a remote SpawnSync eases the parent toward the latest received transform every frame (frame-rate-independent smoothing). Turn it off with interpolate = false for instant snapping, or raise interpolation_speed for a tighter follow.

When to use the lower-level API instead

SpawnSync is ideal for transforms and simple shared variables. For one-off, non-positional messages (a chat line, "I fired", a score event) use send_event; for authoritative shared values use room/entity state directly. The shooters combine both: SpawnSync for movement, events for shots.