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 aColor, 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.