🎮 Godot SDK Examples

Complete GDScript examples for integrating OddSockets into your Godot 4 projects

🚀 Basic Setup

Connect, subscribe and publish from any scene node.

extends Node var client: OddSocketsClient var lobby: OddSocketsChannel func _ready() -> void: client = OddSocketsClient.new() add_child(client) # the client polls its connection every frame var config := OddSocketsConfig.new() config.api_key = "YOUR_API_KEY" config.user_id = "player-123" client.initialize(config) client.connection_error.connect(func(msg): push_warning("connect failed: " + msg)) var result := await client.connect_async() if result.has("error"): return lobby = client.get_channel("game-lobby") lobby.message_received.connect(_on_lobby_message) await lobby.subscribe_async({"enable_presence": true, "retain_history": true}) func _on_lobby_message(envelope: Dictionary) -> void: print("%s: %s" % [envelope.publisher, envelope.message]) func send_hello() -> void: await lobby.publish_async({"type": "hello", "from": client._config.user_id})

đŸ’Ŧ Chat System

A chat room with history catch-up for late joiners. Remember: retention is not replay — subscribe first, then fetch history.

extends Control @onready var messages_list: ItemList = $Messages @onready var input: LineEdit = $Input var client: OddSocketsClient var room: OddSocketsChannel func join_room(room_name: String) -> void: room = client.get_channel(room_name) room.message_received.connect(_on_chat_message) await room.subscribe_async({"enable_presence": true, "retain_history": true}) # Late-join catch-up: history is stored server-side but never replayed var history := await room.get_history_async(50) for msg in history.get("messages", []): _render(msg) func _on_chat_message(envelope: Dictionary) -> void: _render(envelope) func _render(envelope: Dictionary) -> void: var body: Variant = JSON.parse_string(str(envelope.get("message", ""))) var text: String = body.get("text", "") if typeof(body) == TYPE_DICTIONARY else str(envelope.get("message")) messages_list.add_item("%s: %s" % [envelope.get("publisher", "?"), text]) func _on_input_submitted(text: String) -> void: input.clear() var result := await room.publish_async({"type": "chat", "text": text}) if result.has("error"): messages_list.add_item("!! send failed: " + str(result.error))

đŸ•šī¸ Multiplayer State Sync

Broadcast player positions at a fixed cadence; apply everyone else's.

extends CharacterBody2D var client: OddSocketsClient var arena: OddSocketsChannel var sync_accumulator := 0.0 const SYNC_INTERVAL := 0.1 # 10Hz func _physics_process(delta: float) -> void: sync_accumulator += delta if sync_accumulator >= SYNC_INTERVAL and client.is_client_connected(): sync_accumulator = 0.0 arena.publish_async({ "type": "pos", "x": global_position.x, "y": global_position.y, "rot": rotation, }) func _on_arena_message(envelope: Dictionary) -> void: if envelope.get("publisher") == client._config.user_id: return # our own broadcast coming back var body: Variant = JSON.parse_string(str(envelope.get("message", ""))) if typeof(body) == TYPE_DICTIONARY and body.get("type") == "pos": get_node("/root/Arena/Remotes").update_remote( envelope.publisher, Vector2(body.x, body.y), body.rot)

đŸ‘Ĩ Presence Roster

Live who's-here list: seed it with a presence query, keep it fresh from join/leave pushes.

extends VBoxContainer var roster := {} func watch(channel: OddSocketsChannel) -> void: channel.presence_changed.connect(_on_presence_change) var snapshot := await channel.get_presence_async() for occupant in snapshot.get("occupants", []): var uid := str(occupant.userId) if typeof(occupant) == TYPE_DICTIONARY else str(occupant) roster[uid] = true _redraw(int(snapshot.get("occupancy", roster.size()))) func _on_presence_change(data: Dictionary) -> void: var uid := str(data.user.userId) if data.action == "join": roster[uid] = true else: roster.erase(uid) _redraw(int(data.get("occupancy", roster.size()))) func _redraw(occupancy: int) -> void: for child in get_children(): child.queue_free() var header := Label.new() header.text = "%d PLAYING NOW" % occupancy add_child(header) for uid in roster: var row := Label.new() row.text = uid add_child(row)

âš”ī¸ Match Invites

Directed 1:1 invites with object payloads both ways — the reply's payload reaches the inviter as replyPayload.

extends Node var game := OddSocketsEnhancedFeatures.new() func _ready() -> void: game.initialize(client) game.challenge_invited.connect(_on_invited) game.challenge_reply_received.connect(_on_reply) func invite(opponent_id: String) -> void: var result := await game.send_challenge_invite_async( opponent_id, "match", {"arena": "ruins", "seed": randi()}, 120) if result.has("error"): push_warning("invite failed: " + str(result.error)) func _on_invited(data: Dictionary) -> void: # Show an accept/decline dialog built from data.payload (the inviter's object) var accepted: bool = await $InviteDialog.ask(data.from.userId, data.payload) await game.reply_challenge_invite_async( data.inviteId, accepted, {"loadout": "bow"}, "gl hf" if accepted else "busy") func _on_reply(data: Dictionary) -> void: # data.payload = our original invite payload, echoed # data.replyPayload = the opponent's own object # data.reason = their note if data.accept: start_match(data.payload.seed, data.replyPayload)
Full API documentation →