Search documentation

Find pages, sections, and content across all docs.

WavedashDocs

Multiplayer lobbies

Create and manage multiplayer game lobbies

The Lobbies API lets you create multiplayer rooms where players can gather before or during gameplay. Lobbies support real-time messaging, metadata storage, and automatic P2P connection setup.

Listening for LOBBY_JOINED

Both createLobby() and joinLobby() put the current player into a lobby, and in both cases the SDK fires a LOBBY_JOINED event once membership is confirmed. This is your single source of truth for "I'm now in a lobby" — subscribe before you call create or join, then drive your UI from the event handler.

func _ready():
    WavedashSDK.lobby_joined.connect(_on_lobby_joined)

func _on_lobby_joined(payload):
    var lobby_id = payload["lobbyId"]
    var users = payload["users"]
    var host_id = payload["hostId"]
    print("Joined ", lobby_id, " with ", users.size(), " players")
void Awake()
{
    Wavedash.SDK.OnLobbyJoined += OnLobbyJoined;
}

void OnLobbyJoined(Dictionary<string, object> payload)
{
    string lobbyId = (string)payload["lobbyId"];
    var users = (List<object>)payload["users"];
    string hostId = (string)payload["hostId"];
    Debug.Log($"Joined {lobbyId} with {users.Count} players");
}
Wavedash.on(Wavedash.Events.LOBBY_JOINED, (payload) => {
  console.log(`Joined ${payload.lobbyId} with ${payload.users.length} players`);
});
function init(self)
    wavedash.init({}, function(_, event, payload)
        if event == wavedash.EVENT_LOBBY_JOINED then
            print("Joined", payload.lobbyId, "with", #payload.users, "players")
        end
    end)
end

Creating a lobby already joins it. Do not call joinLobby() with the ID returned by createLobby() — the player is in the lobby as soon as LOBBY_JOINED fires, and calling join again will error.

Creating a lobby

Make sure your LOBBY_JOINED listener is wired up first (see above), then call create. The returned lobby ID is useful for invite links and logging, but you do not need to call joinLobby() with it.

func create_lobby():
    await WavedashSDK.create_lobby(WavedashConstants.LOBBY_TYPE_PUBLIC, 4)
    # lobby_joined signal fires once the player is in — handle your UI there
public async void CreateLobby()
{
    // OnLobbyJoined (wired up in Awake) fires once the player is in.
    await Wavedash.SDK.CreateLobby(
        WavedashConstants.LobbyVisibility.PUBLIC,
        maxPlayers: 4);
}
// LOBBY_JOINED fires once the player is in — handle your UI there.
await Wavedash.createLobby(Wavedash.LobbyVisibility.PUBLIC, 4);
local function create_lobby()
    -- wavedash.EVENT_LOBBY_JOINED fires once the player is in — handle your UI there
    wavedash.create_lobby_async(wavedash.LOBBY_VISIBILITY_PUBLIC, 4)
end

Visibility options

ValueConstantDescription
0PUBLICAnyone can find and join
1FRIENDS_ONLYOnly friends can see and join
2PRIVATEOnly joinable with the lobby ID

In JavaScript use Wavedash.LobbyVisibility.PUBLIC / FRIENDS_ONLY / PRIVATE. In Defold use wavedash.LOBBY_VISIBILITY_PUBLIC / LOBBY_VISIBILITY_FRIENDS_ONLY / LOBBY_VISIBILITY_PRIVATE.

Joining and listing

Same rule as creating: set up the LOBBY_JOINED listener first, then call joinLobby().

# Make sure lobby_joined is connected before calling join_lobby (see above).
var response = await WavedashSDK.list_available_lobbies()
if response.success and response.data.size() > 0:
    WavedashSDK.join_lobby(response.data[0].lobbyId)
// Make sure OnLobbyJoined is subscribed before calling JoinLobby (see above).
var lobbies = await Wavedash.SDK.ListAvailableLobbies();
if (lobbies != null && lobbies.Count > 0)
    await Wavedash.SDK.JoinLobby((string)lobbies[0]["lobbyId"]);
-- Make sure wavedash.EVENT_LOBBY_JOINED is handled before calling join_lobby_async (see above).
local co = coroutine.create(function()
    local response = wavedash.list_available_lobbies_async()
    if response.success and #response.data > 0 then
        wavedash.join_lobby_async(response.data[1].lobbyId)
    end
end)
assert(coroutine.resume(co))
// Make sure LOBBY_JOINED is listened for before calling joinLobby (see above).
const response = await Wavedash.listAvailableLobbies(); // pass true for friends only
if (response.success && response.data.length > 0) {
  await Wavedash.joinLobby(response.data[0].lobbyId);
}

Leaving a lobby

Call leaveLobby() when the player intentionally exits — returning to the main menu, disconnecting, or swapping rooms. The server removes them from the lobby and notifies the remaining members via LOBBY_USERS_UPDATED. If the player was the host, hosting transfers to the next member automatically.

await WavedashSDK.leave_lobby(lobby_id)
await Wavedash.SDK.LeaveLobby(lobbyId);
local co = coroutine.create(function()
    wavedash.leave_lobby_async(lobby_id)
end)
assert(coroutine.resume(co))
await Wavedash.leaveLobby(lobbyId);

You do not need to call leaveLobby() when the tab closes — the session ends and the server cleans up membership automatically.

Lobby users

var users = WavedashSDK.get_lobby_users(lobby_id)
var host_id = WavedashSDK.get_lobby_host_id(lobby_id)
var count = WavedashSDK.get_num_lobby_users(lobby_id)
var users = Wavedash.SDK.GetLobbyUsers(lobbyId);
var hostId = Wavedash.SDK.GetLobbyHostId(lobbyId);
int count = Wavedash.SDK.GetNumLobbyUsers(lobbyId);
local users = wavedash.get_lobby_users(lobby_id)
local host_id = wavedash.get_lobby_host_id(lobby_id)
local count = wavedash.get_num_lobby_users(lobby_id)
const users = Wavedash.getLobbyUsers(lobbyId);
const hostId = Wavedash.getLobbyHostId(lobbyId);
const count = Wavedash.getNumLobbyUsers(lobbyId);

Messaging

WavedashSDK.send_lobby_chat_message(lobby_id, message)

func _ready():
    WavedashSDK.lobby_message.connect(_on_lobby_message)

func _on_lobby_message(payload):
    print(payload["username"], ": ", payload["message"])
Wavedash.SDK.SendLobbyChatMessage(lobbyId, message);

void Awake() { Wavedash.SDK.OnLobbyMessage += HandleMessage; }
void HandleMessage(Dictionary<string, object> data)
{
    Debug.Log($"{data["username"]}: {data["message"]}");
}
function init(self)
    wavedash.init({}, function(_, event, payload)
        if event == wavedash.EVENT_LOBBY_MESSAGE then
            print(payload.username .. ": " .. payload.message)
        end
    end)

    wavedash.send_lobby_message(lobby_id, message)
end
Wavedash.sendLobbyMessage(lobbyId, "Hello everyone!");

Wavedash.on(Wavedash.Events.LOBBY_MESSAGE, (payload) => {
  console.log(`${payload.username}: ${payload.message}`);
});

Messages have a maximum length of 500 characters.

Invites

WavedashSDK.invite_user_to_lobby(lobby_id, user_id)

func _ready():
    WavedashSDK.sent_lobby_invite.connect(func(response): print("Sent: ", response.success))
    WavedashSDK.lobby_invite.connect(_on_lobby_invite)

func _on_lobby_invite(data):
    WavedashSDK.join_lobby(data["lobbyId"])
await Wavedash.SDK.InviteUserToLobby(lobbyId, userId);

void Awake() { Wavedash.SDK.OnLobbyInvite += HandleInvite; }
void HandleInvite(Dictionary<string, object> data)
{
    Debug.Log($"Invited by: {data["inviterUsername"]}");
}
function init(self)
    wavedash.init({}, function(_, event, payload)
        if event == wavedash.EVENT_LOBBY_INVITE then
            local co = coroutine.create(function()
                wavedash.join_lobby_async(payload.lobbyId)
            end)
            assert(coroutine.resume(co))
        end
    end)

    wavedash.invite_user_to_lobby_async(lobby_id, user_id)
end
await Wavedash.inviteUserToLobby(lobbyId, friendUserId);

Wavedash.on(Wavedash.Events.LOBBY_INVITE, (payload) => {
  Wavedash.joinLobby(payload.lobbyId);
});

Metadata

Lobby metadata is a key/value map every member stays synced on. Only the host can write it; any member can read it. Values are strings, numbers, or booleans — no nested objects or arrays.

Each write changes one key. Members get LOBBY_DATA_UPDATED with the full map after every change, so keep it to match settings and send anything bigger over P2P.

Godot and Unity expose typed getters and setters, one per value type. JavaScript and Defold pass values through a single polymorphic pair.

# Strings
WavedashSDK.set_lobby_data_string(lobby_id, "gameMode", "deathmatch")
var mode = WavedashSDK.get_lobby_data_string(lobby_id, "gameMode")

# Ints — values are stored as JS Numbers, so only ±2^53 is exact.
# Anything larger returns false; store it as a string instead.
WavedashSDK.set_lobby_data_int(lobby_id, "round", 3)
var round = WavedashSDK.get_lobby_data_int(lobby_id, "round")

# Floats
WavedashSDK.set_lobby_data_float(lobby_id, "matchTimer", 120.0)
var timer = WavedashSDK.get_lobby_data_float(lobby_id, "matchTimer")

# Bools
WavedashSDK.set_lobby_data_bool(lobby_id, "friendlyFire", false)
var friendly_fire = WavedashSDK.get_lobby_data_bool(lobby_id, "friendlyFire")

WavedashSDK.delete_lobby_data(lobby_id, "gameMode")
// Strings
Wavedash.SDK.SetLobbyData(lobbyId, "gameMode", "deathmatch");
string mode = Wavedash.SDK.GetLobbyDataString(lobbyId, "gameMode");

// Ints
Wavedash.SDK.SetLobbyData(lobbyId, "round", 3);
int round = Wavedash.SDK.GetLobbyDataInt(lobbyId, "round");

// Longs — values are stored as JS Numbers, so only ±2^53 is exact.
// Anything larger throws ArgumentOutOfRangeException; store it as a string instead.
Wavedash.SDK.SetLobbyData(lobbyId, "startedAt", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
long startedAt = Wavedash.SDK.GetLobbyDataLong(lobbyId, "startedAt");

// Floats and doubles
Wavedash.SDK.SetLobbyData(lobbyId, "matchTimer", 120.0f);
float timer = Wavedash.SDK.GetLobbyDataFloat(lobbyId, "matchTimer");
Wavedash.SDK.SetLobbyData(lobbyId, "gravity", 9.80665);
double gravity = Wavedash.SDK.GetLobbyDataDouble(lobbyId, "gravity");

// Bools
Wavedash.SDK.SetLobbyData(lobbyId, "friendlyFire", false);
bool friendlyFire = Wavedash.SDK.GetLobbyDataBool(lobbyId, "friendlyFire");

Wavedash.SDK.DeleteLobbyData(lobbyId, "gameMode");
wavedash.set_lobby_data(lobby_id, "gameMode", "deathmatch")
wavedash.set_lobby_data(lobby_id, "round", 3)
wavedash.set_lobby_data(lobby_id, "matchTimer", 120.0)
wavedash.set_lobby_data(lobby_id, "friendlyFire", false)

local mode = wavedash.get_lobby_data(lobby_id, "gameMode")
wavedash.delete_lobby_data(lobby_id, "gameMode")
Wavedash.setLobbyData(lobbyId, "gameMode", "deathmatch");
Wavedash.setLobbyData(lobbyId, "round", 3);
Wavedash.setLobbyData(lobbyId, "matchTimer", 120.0);
Wavedash.setLobbyData(lobbyId, "friendlyFire", false);

const mode = Wavedash.getLobbyData(lobbyId, "gameMode");
Wavedash.deleteLobbyData(lobbyId, "gameMode");

Setters return false if you aren't the host. Writes apply locally at once and sync to the server a moment later. In JavaScript, an unsupported value type (object, undefined, NaN) throws.

SetLobbyData is overloaded in Unity — the typed variant is picked at compile time from the value argument. For reads, use the GetLobbyData{String,Int,Float,Double,Long,Bool} variant matching the stored type — in both Unity and Godot the typed getters coerce a mismatched value rather than erroring.

Integers

Lobby data is stored as JavaScript numbers — IEEE 754 doubles — which represent integers exactly only between -253 and 253 (±9,007,199,254,740,992). Past that, neighbouring integers collapse onto the same double and the value you read back is not the one you wrote. A millisecond timestamp fits comfortably; a 64-bit hash or snowflake ID does not.

The 64-bit setters check the range before sending anything:

  • UnitySetLobbyData(lobbyId, key, long value) throws ArgumentOutOfRangeException for a value outside ±253: Must be within ±2^53 (JS Number precision). Store larger integers as strings. The check runs before the WebGL bridge, so it throws in the editor too. int and float writes are never affected.
  • Godotset_lobby_data_int logs exceeds JS safe integer range (±2^53) and returns false. Godot's int is 64-bit, so this applies to every int write.
  • JavaScript and Defold have no guard: a number past 253 has already lost precision before the SDK sees it.

Reads cast the stored double back to a 64-bit integer, so anything the setter accepted round-trips exactly. In Unity, GetLobbyDataInt returns a 32-bit int — use GetLobbyDataLong for values past 231.

Store larger integers as strings and parse them on read:

WavedashSDK.set_lobby_data_string(lobby_id, "seed", str(seed))
var seed: int = WavedashSDK.get_lobby_data_string(lobby_id, "seed").to_int()
Wavedash.SDK.SetLobbyData(lobbyId, "seed", seed.ToString());
long seed = long.Parse(Wavedash.SDK.GetLobbyDataString(lobbyId, "seed"));

Deleting a key

deleteLobbyData removes a key. In JavaScript and Defold, setting null / nil does the same. Godot and Unity's typed setters can't take null — use delete_lobby_data / DeleteLobbyData.

A deleted key is absent, not null: getLobbyData returns null for it, and it's missing from the LOBBY_DATA_UPDATED payload.

Checking whether a key is set

Godot and Unity's typed getters return "", 0, or false for a missing key, so a false read is ambiguous. has_lobby_data / HasLobbyData is true for any stored value, including false, 0, and "". In JavaScript and Defold, compare against null / nil.

if WavedashSDK.has_lobby_data(lobby_id, "friendlyFire"):
    var friendly_fire = WavedashSDK.get_lobby_data_bool(lobby_id, "friendlyFire")
if (Wavedash.SDK.HasLobbyData(lobbyId, "friendlyFire"))
{
    bool friendlyFire = Wavedash.SDK.GetLobbyDataBool(lobbyId, "friendlyFire");
}
local friendly_fire = wavedash.get_lobby_data(lobby_id, "friendlyFire")
if friendly_fire ~= nil then
    -- friendly_fire is a string, number, or boolean
end
const friendlyFire = Wavedash.getLobbyData(lobbyId, "friendlyFire");
if (friendlyFire !== null) {
  // friendlyFire is string | number | boolean
}

Outside a web export (Unity editor, Godot desktop run) every lobby data call is a stub: setters and HasLobbyData return false, getters return their missing-key value.

Reading before you join

getLobbyData reads local state: the lobby you're in, or one you've fetched with listAvailableLobbies or getLobby(lobbyId) (get_lobby in Godot, GetLobby in Unity). Use it to check a host's settings before joinLobby. For any other lobby it returns null.

Generate a shareable invite link for the current lobby. Pass true to also copy it to the user's clipboard.

func _ready():
    WavedashSDK.got_lobby_invite_link.connect(_on_invite_link)

func copy_link():
    WavedashSDK.get_lobby_invite_link(true)

func _on_invite_link(response):
    if response.get("success", false):
        print("Invite link: ", response["data"])
string link = await Wavedash.SDK.GetLobbyInviteLink(copyToClipboard: true);
Debug.Log($"Invite link: {link}");
local co = coroutine.create(function()
    local response = wavedash.get_lobby_invite_link_async(true)
    if response.success then
        print("Invite link:", response.data)
    end
end)
assert(coroutine.resume(co))
const response = await Wavedash.getLobbyInviteLink(true);
if (response.success) {
  console.log("Invite link:", response.data);
}

When a player opens an invite link, the lobby ID is passed to your game as a launch param. Your game must check for it on startup and join the lobby — it does not happen automatically:

var params = WavedashSDK.get_launch_params()
if params.has("lobby"):
    WavedashSDK.join_lobby(params["lobby"])
var parameters = Wavedash.SDK.GetLaunchParams();
if (parameters.TryGetValue("lobby", out var lobbyId))
    await Wavedash.SDK.JoinLobby(lobbyId);
local co = coroutine.create(function()
    local params = wavedash.get_launch_params()
    if params and params.lobby then
        wavedash.join_lobby_async(params.lobby)
    end
end)
assert(coroutine.resume(co))
const params = Wavedash.getLaunchParams();
if (params.lobby) {
  await Wavedash.joinLobby(params.lobby);
}

Events

EventDescription
LOBBY_JOINEDSuccessfully joined a lobby
LOBBY_USERS_UPDATEDA user joined or left
LOBBY_MESSAGENew message received
LOBBY_DATA_UPDATEDMetadata changed
LOBBY_KICKEDRemoved from lobby
LOBBY_INVITEReceived lobby invitation

Handling lifecycle events

Subscribe to lobby-lifecycle events so your UI stays in sync with membership and metadata changes.

func _ready():
    WavedashSDK.lobby_users_updated.connect(_on_users_updated)
    WavedashSDK.lobby_data_updated.connect(_on_data_updated)
    WavedashSDK.lobby_kicked.connect(_on_kicked)

func _on_users_updated(payload):
    print("User ", payload["username"], " ", payload["changeType"])

func _on_data_updated(metadata):
    print("Lobby metadata: ", metadata)

func _on_kicked(payload):
    print("Kicked: ", payload["reason"])
void Awake()
{
    Wavedash.SDK.OnLobbyUsersUpdated += data =>
        Debug.Log($"User {data["username"]} {data["changeType"]}");
    Wavedash.SDK.OnLobbyDataUpdated += metadata =>
        Debug.Log($"Lobby metadata: {metadata}");
    Wavedash.SDK.OnLobbyKicked += data =>
        Debug.Log($"Kicked: {data["reason"]}");
}
function init(self)
    wavedash.init({}, function(_, event, payload)
        if event == wavedash.EVENT_LOBBY_USERS_UPDATED then
            print("User", payload.username, payload.changeType)
        elseif event == wavedash.EVENT_LOBBY_DATA_UPDATED then
            print("Lobby metadata:", payload)
        elseif event == wavedash.EVENT_LOBBY_KICKED then
            print("Kicked:", payload.reason)
        end
    end)
end
Wavedash.on(Wavedash.Events.LOBBY_USERS_UPDATED, (payload) => {
  console.log(`User ${payload.username} ${payload.changeType}`);
});
Wavedash.on(Wavedash.Events.LOBBY_DATA_UPDATED, (payload) => {
  console.log("Lobby metadata:", payload);
});
Wavedash.on(Wavedash.Events.LOBBY_KICKED, (payload) => {
  console.log("Kicked:", payload.reason);
});