Docs / Quick start - Unity

Quick start - Unity

Connect a Unity 2021.3+ 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 SDK

In Unity: Window → Package Manager → + → Add package from git URL, paste https://github.com/widgrensit/asobi-unity.git (append #v0.1.0 or a later tag to pin). Asobi then appears under Packages in the Project window.

2. Where this code lives

Everything below goes on one MonoBehaviour in your scene, split across Unity's lifecycle methods:

  • Start() - authenticate, subscribe to events, connect, and queue (in that order).
  • Update() - read input and send it, once per frame.
  • OnDestroy() - unsubscribe, so handlers don't leak across scene reloads.

Threading. Realtime events fire on a background thread. Logging is fine, but the moment a handler touches a UnityEngine.Object (a Transform, GameObject, ...) you must marshal to the main thread first, or Unity throws "can only be called from the main thread". Copy the UnityMainThread helper from the demo and wrap those calls in UnityMainThread.Enqueue(() => ...).

3. The complete client

Realtime events deliver the raw WebSocket JSON as a string - parse the fields you need with JsonUtility (simple shapes) or Newtonsoft (nested). The whole flow:

using System;
using Asobi;
using UnityEngine;

public class AsobiClientBehaviour : MonoBehaviour
{
    public string Host = "localhost";
    public int    Port = 8084;

    private AsobiClient _client;

    // Events are raw JSON; pull out just the fields you need.
    [Serializable] private struct Matched { public string match_id; }

    private async void Start()
    {
        _client = new AsobiClient(Host, port: Port);

        // 1. Authenticate. await it before connecting - the WebSocket uses
        //    this session. A fresh username each run - registering the same
        //    one twice returns 409 username_taken (see below).
        var name = $"player_{UnityEngine.Random.Range(100000, 999999)}";
        await _client.Auth.RegisterAsync(name, "secret1234", "Player One");

        // 2. Subscribe BEFORE connecting so you don't miss early events.
        _client.Realtime.OnMatchmakerMatched += OnMatched;
        _client.Realtime.OnMatchState        += OnState;

        // 3. Open the WebSocket, then queue. "default" is the mode your
        //    server's match.lua registers.
        await _client.Realtime.ConnectAsync();
        await _client.Matchmaker.AddAsync("default");
    }

    private async void OnMatched(string rawJson)
    {
        // Queuing matched you; join before state flows.
        var m = JsonUtility.FromJson<Matched>(rawJson);
        await _client.Realtime.JoinMatchAsync(m.match_id);
    }

    private void OnState(string rawJson)
    {
        // Server's authoritative tick. Touching a Transform/GameObject here?
        // Wrap it in UnityMainThread.Enqueue(...) first.
        Debug.Log($"state: {rawJson}");
    }

    private void Update()
    {
        // Send input each frame a key is held. Fire-and-forget.
        if (Input.GetKey(KeyCode.RightArrow))
            _ = _client.Realtime.SendMatchInputAsync("{\"action\":\"move\",\"x\":1,\"y\":0}");
    }

    private void OnDestroy()
    {
        if (_client == null) return;
        _client.Realtime.OnMatchmakerMatched -= OnMatched;
        _client.Realtime.OnMatchState        -= OnState;
    }
}

4. What each part does, and when

  • Authenticate first (in Start). The WebSocket authenticates with your session, so await RegisterAsync before you connect. Registering a username that already exists returns 409 username_taken - a real game registers once and calls LoginAsync on return (this quickstart uses a fresh random name each run). Auth is rate-limited to 5/sec per IP - bursting returns 429 rate_limited. See Auth & rate limiting.
  • Subscribe before Connect/Add (in Start). Add your += handlers, then ConnectAsync() and Matchmaker.AddAsync("default"). The sequential awaits make the ordering explicit.
  • Join on OnMatchmakerMatched. Parse match_id from the raw JSON and call JoinMatchAsync. Without the join, OnMatchState never fires.
  • Send input from Update. Gather Input and call SendMatchInputAsync. It is fire-and-forget; the next OnMatchState reflects the server's response.
  • Unsubscribe in OnDestroy. The -= pairs stop handler leaks when the scene reloads.

WebGL is not supported - the underlying ClientWebSocket is unavailable on the WebGL runtime. Standalone, Android, and iOS (Mono or IL2CPP) all work.

What's next