Events are transient messages relayed to the other members of your room — shots fired, emotes, chat lines, "round started" pings. One call to send, one signal to receive.
Send and receive
# Sender — fire and forget, no await:
SpawnWeaver.send_event("player_fired", {"dir": [0, 1], "weapon": "bow"})
# Every OTHER member receives it:
SpawnWeaver.event_received.connect(func(event_name, data, sender):
match event_name:
"player_fired":
spawn_arrow(sender.id, data["dir"], data["weapon"]))
event_nameis any string you choose;datais any JSON-serializable Dictionary.senderis theSWPlayerwho sent it — usesender.idandsender.display_name().- You must be in a room; events go to your current room's members only.
Sender-excluded semantics
The sender never receives its own event. Apply the local effect immediately when
you send, and handle event_received for everyone else:
func fire(dir: Vector2) -> void:
spawn_arrow(SpawnWeaver.player.id, [dir.x, dir.y], "bow") # local, instant
SpawnWeaver.send_event("player_fired", {"dir": [dir.x, dir.y], "weapon": "bow"})
This is the opposite of state changes (room_state_changed / entity_changed), which
do echo back to the sender as the acknowledgment.
Fire-and-forget, with queuing during reconnect
send_event() returns immediately — there is no success reply to await. Failures (rate
limit, oversized payload, not in a room) arrive on the error signal:
SpawnWeaver.error.connect(func(err):
if err.code == "rate-limited":
pass # back off — see Limits
)
If the connection is mid-reconnect, events are queued automatically and flushed the
moment the session resumes (up to 128 queued messages; oldest are dropped beyond that).
If you are fully offline (start() never succeeded, or after stop()), the event is
dropped with an editor warning instead.
Example: chat
# --- sending ---
func _on_chat_submitted(text: String) -> void:
if text.strip_edges().is_empty():
return
_append_line(SpawnWeaver.player.display_name(), text) # your own line, locally
SpawnWeaver.send_event("chat", {"text": text})
_input.clear()
# --- receiving ---
func _ready() -> void:
SpawnWeaver.event_received.connect(_on_event)
func _on_event(event_name: String, data: Dictionary, sender: SWPlayer) -> void:
if event_name == "chat":
_append_line(sender.display_name(), str(data.get("text", "")))
func _append_line(who: String, text: String) -> void:
_log.append_text("[b]%s:[/b] %s\n" % [who, text])
Chat is a perfect event: transient (late joiners don't need old lines), sender-known, and low-rate. If you do want history for late joiners, keep the last N lines in host-written room state instead.
Events vs. state
| Question | Events | State |
|---|---|---|
| Does a late joiner need it? | No — events are never replayed | Yes — the snapshot arrives on join |
| Is it a moment or a value? | A moment ("fired", "emoted") | A value (position, hp, phase) |
| Delivery | Relayed once to current members | Kept on the server, changes broadcast |
| Sender receives it? | No | Yes (the ack copy) |
Rule of thumb: transient → event, persistent → state. A door opening is an event
if it's cosmetic; it's state ({"door_3": "open"}) if players joining later must see
the door open. When in doubt, make it state — it survives reconnects for free.
Rate and size budget
Events share the connection's message budget: 20 messages/s sustained (burst 40)
and 16 KB per message. Exceeding them raises rate-limited (retryable — back off
briefly) or payload-too-large on the error signal. Don't stream continuous values
through events — that's what SpawnSync and its dirty-check are
for.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Waiting for your own event to render the effect | Nothing happens — sender is excluded | Apply the local effect when sending |
| Sending movement every frame via events | rate-limited, jittery remotes |
Use SpawnSync / entities for continuous values |
| Sending before joining a room | error signal with not-in-room |
Events need a current room |
| Using events for "current game phase" | Late joiners are lost | Put it in host-written room state |
| Huge payloads (base64 images, full inventories) | payload-too-large |
Keep events small; store big data in storage |
Next
- State sync — persistent live values
- Best practices — choosing the right channel