Docs / Quick start - Defold

Quick start - Defold

Connect a Defold 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. Add the SDK as a library dependency

Open game.project → Project → Dependencies and add both the SDK (pinned to a release tag) and the WebSocket extension it needs:

https://github.com/widgrensit/asobi-defold/archive/refs/tags/v1.2.1.zip
https://github.com/defold/extension-websocket/archive/refs/tags/4.2.2.zip

Pin to a tag - main is unstable. Then Project → Fetch Libraries. The SDK shows up as asobi in your project tree.

2. Where this code lives

Put this on a .script attached to a game object that lives for the whole app - a script in your main.collection, not a gui_script. Defold invalidates the WebSocket callbacks when their owning script is unloaded, so a short-lived script drops your connection. Add main/boot.script and assign it to a game object in your main collection. It uses two Defold callbacks:

  • init(self) - runs once. Authenticate, wire up realtime handlers, and connect here.
  • on_input(self, action_id, action) - runs when a bound input fires. Send match input here.

3. The complete boot.script

The whole flow - authenticate, connect, queue, join, receive state, send input - in one script. Note how everything after auth is nested inside the register callback: that is what keeps connect from racing ahead of your session.

local asobi = require("asobi.client")

function init(self)
    -- Route input to this script's on_input (needs an input binding too).
    msg.post(".", "acquire_input_focus")

    -- Local engine: host, port. Use (host, 443, true) for a hosted env over SSL.
    self.client = asobi.create("localhost", 8084)

    -- Authenticate first. Everything else happens inside this callback,
    -- once you actually have a session. The callback gets (data, err). A fresh
    -- username each run - registering the same one twice returns 409 (below).
    self.client.auth.register(self.client, "player_" .. math.random(100000, 999999),
        "secret1234", nil, function(data, err)
            if err then print("auth failed: " .. tostring(err.error)) return end

            -- A match was formed. Join it before state starts flowing.
            self.client.realtime:on("match_matched", function(payload)
                self.client.realtime:join_match(payload.match_id)
            end)

            -- The server's authoritative tick. Update your game world here.
            self.client.realtime:on("match_state", function(state)
                -- e.g. move your player game objects from state
            end)

            -- connect() authenticates asynchronously; wait for "connected"
            -- before queuing, or the queue races ahead of the session.
            self.client.realtime:on("connected", function()
                self.client.realtime:add_to_matchmaker("default")
            end)

            self.client.realtime:connect()
        end)
end

function on_input(self, action_id, action)
    -- Fires when a bound action changes. send_match_input is fire-and-forget.
    if action_id == hash("right") and (action.pressed or action.repeated) then
        self.client.realtime:send_match_input({action = "move", x = 1, y = 0})
    end
end

4. What each part does, and when

  • Authenticate first (in init). The client is the first argument to every API call. Nest the rest inside the register callback so it only runs once you have a session. 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).
  • Queue only after "connected". connect() authenticates asynchronously, so wait for the connected event before add_to_matchmaker("default"). The mode string must match a mode your server registers.
  • Join on match_matched. Queuing gets you matched; you then join_match(payload.match_id). Without the join, match_state never arrives.
  • Send input from on_input. init calls acquire_input_focus so this script receives input; bind the right action in game.input_binding. The next match_state reflects the server's authoritative response.

What's next