Docs / Quick start - Godot

Quick start - Godot

Connect a Godot 4.x project to a running Asobi server in about five minutes. Don't have a server yet? Run the server quickstart first - it gives you a backend on localhost:8084 with a default match mode, which is what this page connects to.

1. Install the addon

Clone or copy asobi-godot into addons/asobi/, then enable it via Project → Project Settings → Plugins. The plugin registers an Asobi autoload for you, so you use Asobi directly from any script - you do not create a client node yourself.

2. Where this code lives

Put the code below on a script attached to a node in your main scene (or your own autoload). Asobi is driven by two Godot lifecycle callbacks:

  • _ready() - runs once when the node enters the tree. Authenticate, wire up signals, and connect here.
  • _process(delta) - runs every frame. Read input and send it here.

Realtime events arrive as Godot signals (match_matched, match_state, ...), each carrying a Dictionary payload. You connect them to your own handler functions.

3. The complete client

This is the whole flow - authenticate, connect, queue, join, receive state, send input - in one script. Each part is explained below.

extends Node

func _ready() -> void:
    # Point the autoload at your server (matches the server quickstart).
    Asobi.host = "localhost"
    Asobi.port = 8084

    # 1. Authenticate. await the result before doing anything else - the
    #    WebSocket connects with this session.
    # A fresh username each run - registering the same one twice returns
    # 409 username_taken (see below).
    var resp := await Asobi.auth.register("player_%d" % randi(), "secret1234", "Player One")
    if resp.has("error"):
        push_error("auth failed: %s" % resp.error)
        return

    # 2. Wire up the signals you care about BEFORE connecting, so you don't
    #    miss the first events.
    Asobi.realtime.match_matched.connect(_on_matched)
    Asobi.realtime.match_state.connect(_on_state)

    # 3. Open the WebSocket, then queue for a match. "default" is the mode
    #    your server's match.lua registers.
    Asobi.realtime.connect_to_server()
    Asobi.realtime.add_to_matchmaker("default")

func _on_matched(payload: Dictionary) -> void:
    # The matchmaker found a match. You must join it before state flows.
    Asobi.realtime.join_match(payload["match_id"])

func _on_state(payload: Dictionary) -> void:
    # The server's authoritative tick. Update your game world from here.
    var players: Dictionary = payload.get("players", {})
    print("tick %s, %d players" % [payload.get("tick", 0), players.size()])

func _process(_delta: float) -> void:
    # Send input every frame a key is held. send_match_input takes a
    # Dictionary - no JSON encoding.
    if Input.is_action_pressed("ui_right"):
        Asobi.realtime.send_match_input({"action": "move", "x": 1, "y": 0})

4. What each part does, and when

  • Authenticate first (in _ready). The WebSocket authenticates with your session, so await the register (or login) call and check resp.has("error") before you connect. Registering a username that already exists returns 409 username_taken - a real game registers once and calls login on return (this quickstart uses a fresh random name each run). Auth is rate-limited to 5/sec per IP; production builds should use a platform provider (see Auth & rate limiting).
  • Connect signals before connect_to_server (in _ready). Connect your handlers to match_matched and match_state first, then call connect_to_server() and add_to_matchmaker("default"). The mode string must match a mode your server registers.
  • Join on match_matched. Queuing gets you matched; you then have to join_match(payload["match_id"]). Without the join, match_state never arrives - you sit idle after matching.
  • Send input from _process. Poll Input and call send_match_input(dict) once per frame. It is fire-and-forget: the next match_state reflects the server's authoritative response.

What's next