Patterns that keep SpawnWeaver games simple, cheap, and robust. Most of this boils down to two habits: put each piece of data in the right channel, and let the host decide.
Choose the right channel
Four channels, four jobs:
| Channel | Persistence | Visibility | Writer | Use for |
|---|---|---|---|---|
Events (send_event) |
none — relayed once | other room members | anyone | Moments: shots, emotes, chat, "ready" clicks |
Entities (set_entity/SpawnSync) |
life of the owner's membership | whole room + late joiners | the owner | Per-object live state: transforms, hp, a bomb's fuse |
Room state (set_room_state) |
life of the room | whole room + late joiners | the host | Room-wide facts: phase, scores, round timer, map |
Storage (storage_set) |
forever | that player only | that player | Progression: saves, unlocks, settings |
Decision shortcuts:
- Would a late joiner need it? → not an event.
- Does exactly one player own it? → entity. Is it about the whole room? → room state.
- Should it outlive the room? → storage.
- When torn between event and state, pick state — it survives reconnects for free.
Host-authoritative patterns
Without dedicated servers, the room host is your referee. Keep game-deciding logic on the host and let everyone else render:
# Everyone reports their moment:
SpawnWeaver.send_event("flag_touched", {})
# Only the host adjudicates and writes the result:
func _on_event(name: String, data: Dictionary, sender: SWPlayer) -> void:
if name == "flag_touched" and SpawnWeaver.is_host:
if _flag_untouched:
_flag_untouched = false
SpawnWeaver.set_room_state({"winner": sender.id, "phase": "ended"})
# Everyone (host included) reacts to the verdict:
SpawnWeaver.room_state_changed.connect(func(state, patch):
if patch.has("phase") and state["phase"] == "ended":
show_victory(state["winner"]))
Rules of the pattern:
- Inputs up as events, verdicts down as room state. Ties are resolved by the host processing events in arrival order.
- Always handle
host_changed. The new host must be able to pick up refereeing from the current room state alone — keep everything the referee needs in room state, not in host-local variables. - Idempotent verdicts: a re-sent event or a resumed session must not double-award.
Trusting clients (and when not to)
SpawnWeaver clients are authoritative over their own entities — a modified client can teleport its own avatar or lie in events. For casual and co-op games between friends, accept this; the simplicity is worth more than cheat-proofing.
Where it matters:
- Let the host validate. Sanity-check event data (
speed, positions, cooldowns) before writing verdicts. - Never trust clients with persistent rewards. If gold/unlocks matter, have the host write results, and treat storage as the player's own save (they can only hurt themselves).
- Competitive/ranked play needs server authority, which SpawnWeaver does not provide today — design around it or wait for it.
Bandwidth budgeting
You have 20 messages/s per connection (state changes: 10/s). Spend them deliberately:
- Leave
SpawnSync.send_rateat 8 — it looks smooth with interpolation and leaves headroom. - The dirty-check means idle objects are free: stop moving = stop sending. Design levels where things rest.
- Patch, don't set:
patch_entity(id, {"hp": 90})beats re-sending the whole state. - Coalesce events: one
"round_results"event, not ten"player_scored"events in a burst. - Don't mirror
SpawnSyncdata in events or room state — one channel per datum.
Testing with multiple instances
- Debug → Run Multiple Instances → 2 (or more) is the core loop; every feature should be exercised with two windows before you trust it.
- Test the ugly paths deliberately: call
SpawnWeaver.simulate_connection_loss()mid-game and watch your reconnect UX; kill a window to watch the grace window and host migration. - Keep a debug key bound to
create_debug_report_string()→ clipboard from day one. - Run against a local server (
./quickstart.ps1) for fast iteration; hit the hosted service before releases to catch TLS/latency differences.
Handling player_disconnected in UI
Disconnections are usually 5-second blips, not departures. The good pattern:
SpawnWeaver.player_disconnected.connect(func(p):
grey_out(p.id) # ghost the avatar, pause their turn
show_toast("%s is reconnecting…" % p.display_name()))
SpawnWeaver.player_reconnected.connect(func(p):
restore(p.id))
SpawnWeaver.player_left.connect(func(p, reason):
remove(p.id) # only NOW is it a real departure
if reason == "disconnected":
show_toast("%s lost connection." % p.display_name()))
Never remove players (or their entities) on player_disconnected — the server keeps
their seat and their entities for the grace window, and SpawnSync will resume
seamlessly when they return.
Graceful degradation
Multiplayer fails sometimes; the game should bend, not break:
- Boot without blocking. If
start()fails, open the main menu anyway with multiplayer buttons disabled and a retry — don't gate the whole game on connectivity. - Show reconnect state. On
reconnecting(attempt, delay), freeze gameplay input and show a banner; onconnected+room_joined, resume. Awaited calls made during the gap fail fast withdisconnected— treat that as "try again shortly", not fatal. - Expect
room_left. A room can end under you ("expired","disconnected"after a long outage) — always have a path back to the menu with a human message. - Cache storage locally. Keep the last-known save in
user://and reconcile withstorage_geton boot, so an offline launch still plays. - Single-player fallback. If your design allows, let a solo player play against
the environment while waiting for
find_match— the await pattern makes it easy to swap in the real room when it resolves.
Small habits that pay off
- One
await SpawnWeaver.start()at boot, in an autoload of your own — everything else assumes online-or-reconnecting. - Key all game logic by
player.id, never by display name or roster index. - Namespace event names (
"chat","combat/fired") and entity ids ("bomb_3","pickup_hp_1") from the start. - Check
result.okon every await in shipped code — the compiler won't make you. - Read Limits once; design inside them rather than discovering them in production.