Nebula

Connecting clients

Let players choose a gateway with the optional NebulaTitleScreen, or connect from your own UI with NebulaClient.ConnectTo.

To let a player join a mesh, connect the client to the gateway. The gateway is the process that accepts client connections. Nebula does not show a connection screen unless you add one. Choose one of these options:

OptionUse it when
Add NebulaTitleScreenYou want a working connection screen during development.
Call NebulaClient.ConnectTo from your own UIYou want the connection flow to match your game.
Pass connection arguments on the command lineYou start bots, test clients, or clients from a launcher.

Use the title screen

Add NebulaTitleScreen to the same GameObject as NebulaBootstrap. Nebula > Set Up Active Scene adds it for you.

The title screen asks for the gateway address and a display name. It saves both values in PlayerPrefs and fills them in the next time the game starts. After the local player spawns, the title screen hides. Press Escape to show it again, disconnect, or choose another gateway.

The title screen stays hidden in processes without the client role. It also stays hidden when the client connects on its own at startup.

Keep the title screen on the bootstrap object

The bootstrap keeps its GameObject when it loads the game scene. If your boot scene differs from NebulaConfig.GameScene, a title screen on another object in the boot scene is destroyed when the game scene loads.

Connect from your own UI

Remove NebulaTitleScreen or leave it out of the scene. A client started without connection arguments then stays idle until your code connects it.

Get the client from NebulaBootstrap.Instance.Client. The bootstrap creates the client after the game scene loads, so check for null before you use it.

ConnectMenu.cs
using Nebula;
using UnityEngine;

public sealed class ConnectMenu : MonoBehaviour
{
    public string GatewayAddress = "127.0.0.1";
    public ushort GatewayPort = 7000;
    public string PlayerName = "Player";

    // Call this from your Connect button.
    public void Connect()
    {
        var client = NebulaBootstrap.Instance != null ? NebulaBootstrap.Instance.Client : null;
        if (client == null) return; // The client role has not started yet.
        client.ConnectTo(GatewayAddress, GatewayPort, PlayerName);
    }
}

ConnectTo replaces the configured gateway address and retries connection failures (after 1 second, then every 2 seconds by default) until it connects, receives a refusal that stops automatic retries, or you call Disconnect. A blank player name keeps the current name, which comes from -nebula-name or the operating system user name (player in a web build).

The gateway also identifies the player when it connects: anonymously by default, or from an OpenID Connect token you set in NebulaClient.AuthToken before connecting. See Authentication and player identity.

Use these NebulaClient members to drive your UI:

MemberUse it to
ConnectionState and ConnectionStateChangedShow Disconnected, Connecting, Connected, or InGame.
Join, JoinEstimatedSeconds and JoinStateChangedShow a "world starting" screen while the mesh boots a worker (see below).
LocalPlayer and LocalPlayerSpawnedHide your menu when the player's entity arrives.
LocalPlayerLostFind out that the player's entity was despawned while the connection stayed up, and whether its worker failed or the server removed it (see Notice a stalled server).
ServerStalled, ServerResumed, IsServerStalled and SecondsSinceServerStateShow "server not responding" while no state arrives (see below).
ClientIdRead the local session ID, a ulong the gateway assigned in its welcome. NebulaRuntime.LocalClientId holds the same value. It is 0 before the welcome.
IdentityRead the player's identity across sessions (see Authentication).
SessionToken and SessionReclaimedKeep or drop the session across reconnections (see below).
JoinRejectedShow why the gateway refused the join. When the refusal stops automatic retries, ConnectionState is already Disconnected and WantsConnection is false.
GatewayDrainingTell the player that the client is switching gateways (see below).
Disconnected and LastDisconnectFind out why the connection ended, as a typed reason, and whether the client will retry (see below).
IsReconnecting, ReconnectAttempt and ReconnectElapsedSecondsShow a "reconnecting" screen with its progress (see below).
Reconnecting, Reconnected and ReconnectGaveUpShow and hide that screen, and return to your menu when the client gives up.
LastErrorShow why the last connection attempt failed or ended. It is empty while the connection is healthy.
WantsConnectionTell whether the client is connecting or will retry.
ConnectsAutomaticallyHide your menu when the command line already supplied the connection.
Disconnect()Leave the gateway and stop retrying. The worker keeps the pawn for the reclaim time, so the player can come back to it.
Leave(), LeaveAndWait(), IsLeaving and LeaveFinishedLeave the world for good when the player quits: the session ends at once and other players stop seeing the pawn (see Leave the game).

The "world starting" state

A mesh with scale to zero turned on (MinWorkers = 0) keeps no workers while nobody is playing. A client that joins then is welcomed normally, but there is nowhere to spawn it yet, so the gateway holds the join instead of rejecting it: Join becomes JoinState.Starting and JoinEstimatedSeconds is roughly how long a worker takes to boot on that host. When the worker is up the gateway places the player, Join becomes JoinState.Joined and LocalPlayer arrives - all on the same connection, with no reconnect and nothing for your UI to retry.

client.JoinStateChanged += (state, seconds) =>
{
    StartingPanel.SetActive(state == JoinState.Starting);
    StartingLabel.text = seconds > 0 ? $"World starting, about {seconds} s..." : "World starting...";
};

NebulaTitleScreen already does this: it shows "world starting, about 75 s..." in place of "waiting for a pawn...".

JoinHoldReason says why the join is held. Besides WorldStarting, it can be ScopeNotReady, ScopeRestoring or ScopeRetiring for a scope, AtCapacity for a full target, or Recovering when the worker that held the player's pawn failed and the gateway is placing the player again. Recovering holds a player who was in the world a moment ago, so say "getting you back in".

NebulaTitleScreen.TryParse splits text such as 127.0.0.1:7000 into an address and port. You can use it to validate a single address field.

Find out why the connection ended

NebulaClient.Disconnected fires when the connection to the gateway ends, and when the first attempt to connect fails. Its DisconnectInfo argument has three fields:

  • Reason, a DisconnectReason your code can branch on.
  • Message, readable text for logs and the player. It matches LastError when the client set one.
  • WillRetry, which is true when the client keeps trying to connect by itself.

The event fires once per outage. When a retry fails again, the client updates LastDisconnect but does not raise the event again. It fires again with WillRetry set to false when the client stops trying. ConnectionStateChanged fires first, so ConnectionState is already Disconnected in your handler.

DisconnectReasonWhat happened
TimeoutThe connection was up, and the gateway stopped answering for longer than the transport's disconnect timeout. The gateway or the network between went away.
NetworkUnreachableThe player's own network is down.
HostUnreachableNothing answered at the gateway address, the connection was refused, or the name didn't resolve.
ClosedByServerThe gateway closed the connection without saying why, or restarted and no longer knows the client.
JoinRefusedThe gateway refused the join. JoinRejectReason has the typed refusal. WillRetry is true only for a refusal the gateway asked the client to retry, such as gateway is draining.
SessionReplacedThe player connected again somewhere else. The client doesn't reconnect.
GatewayDrainingThe gateway is being taken out of service. The client reconnects at once.
ServerShutdownThe gateway is shutting down and no other gateway was serving: the server is going away or restarting. The client reconnects on its normal schedule.
KickedThe server removed the player. Message is the server's reason and DisconnectInfo.Code is your game's own code for it. The client doesn't reconnect.
EncryptionFailedThe encrypted connection was refused: the key exchange failed or the gateway's certificate didn't match GatewayFingerprint.
ClientRequestedYour code called Disconnect() or Leave(), directly or through ConnectTo.
UnknownThe transport didn't say why. A custom ITransport that reports no reason produces this.
client.Disconnected += info =>
{
    if (info.WillRetry) ShowReconnecting(info.Reason == DisconnectReason.NetworkUnreachable
        ? "Check your internet connection"
        : "Connection lost, reconnecting...");
    else if (info.Reason != DisconnectReason.ClientRequested) ShowMainMenu(info.Message);
};

A custom transport reports why a link closed through TransportEvent.Reason: create its Disconnected event with TransportEvent.Disconnected(peerId, reason).

The client keeps its session across a reconnection. The gateway's welcome carries a session token, which the client stores in NebulaClient.SessionToken and presents on every later connection. A gateway that verifies it, any gateway of the mesh, welcomes the client with the same ClientId and, when the worker still holds the pawn, the same pawn; SessionReclaimed is then true. See Sessions and reconnection for what the token does and does not guarantee.

The client handles these connection events automatically:

  • The link drops. ConnectionState becomes Disconnected, Disconnected fires with the reason and WillRetry set to true, LastError reads disconnected from gateway (retrying), and the client starts reconnecting to the same address (see below). The worker keeps the pawn for NebulaConfig.SessionReclaimSeconds (default 30 s); a player who comes back within that time continues with it.
  • This player connected again somewhere else. The gateway sends SessionReplaced, the SessionReplaced event fires with a reason fit to show the player, and the client leaves the world and stays out: the newer connection has the player's session and pawn. Reconnect only when the player asks to, which takes the player back. See One connection per player.
  • The gateway is being taken out of service. The gateway sends GatewayDraining with the number of seconds it gives its clients, the client drops the link, the GatewayDraining event fires with that number, and the client reconnects at once with its session token. When the gateway address is a load balancer, the retry lands on another gateway; when it is one gateway, the client keeps retrying until that gateway is back in service. A gateway that is draining refuses a new join with the reason gateway is draining; the client retries after a second instead of giving up.
  • The gateway process stops. Before it exits, a gateway tells its clients, then waits up to NebulaConfig.GatewayShutdownDrainSeconds (default 0.5 s) for the notice to go out. When another gateway is serving, the notice is the same drain notice. When none is, Disconnected fires with ServerShutdown and WillRetry set to true, and the client reconnects on its normal schedule with its session token, so a server that comes back within the reclaim time gives the pawn back. Either way the client doesn't wait out the transport's disconnect timeout.
  • The server removed the player. Server code called NebulaWorker.Kick or NebulaGateway.Kick. Disconnected fires with Kicked, the server's reason in Message and your game's code in DisconnectInfo.Code. The session is over: the client clears SessionToken, sets WantsConnection to false and doesn't reconnect. It keeps the saved identity token, so connecting again later is the same player in a new session.

If a gateway process fails, session coordination can reclaim its sessions after its heartbeat exceeds the configured WorkerTimeoutSeconds (default 5 s). Reclaim time also depends on failure detection, retries, network delays, and control-plane availability. Tests with default settings use a 9-second threshold measured from a reconnect attempt reaching a live gateway; this is not a deployment guarantee. See the scale suite.

The client retries its configured GatewayAddress. Nebula does not provide a load balancer: configure your address and infrastructure so a retry can reach a surviving gateway. If the address still points only to the failed gateway, the session cannot be reclaimed there.

The replicated world is cleared on every reconnection and replayed by the gateway that answers, so entities disappear for a moment and LocalPlayerSpawned fires again. Show a short "reconnecting" message while IsReconnecting is true rather than returning to your main menu, and return to the menu only when Disconnected reports WillRetry as false.

Show the reconnecting state

When a connection the client was welcomed on drops, or its gateway drains, the client enters the reconnecting state until it is welcomed again:

  1. IsReconnecting becomes true and Reconnecting fires, after Disconnected. ConnectionState keeps moving between Disconnected, Connecting and Connected while the client retries, so use IsReconnecting to decide what to show.
  2. ReconnectAttempt counts the attempts started since the drop, and ReconnectElapsedSeconds counts the seconds.
  3. When a gateway welcomes the client again, Reconnected fires with true if the session and pawn came back (SessionReclaimed), or false if the worker had already let the pawn go.

A first connection that fails is not a reconnection: the client retries it with the same schedule, but IsReconnecting stays false and the give-up time doesn't apply.

The client waits ReconnectFirstDelaySeconds (1 s) before the first attempt, then multiplies the wait by ReconnectBackoffFactor (2) after each failed attempt, up to ReconnectMaxDelaySeconds (2 s). By default it never gives up. Set ReconnectGiveUpSeconds to stop after that many seconds: ReconnectGiveUp fires, WantsConnection becomes false, ConnectionState stays Disconnected, and Disconnected fires again with WillRetry set to false and the reason of the last failure. A value a little above SessionReclaimSeconds, such as 40 s with the default 30 s reclaim time, stops once the pawn is gone. See Client reconnection for the settings, including the transport's disconnect timeout.

client.Reconnecting += () => ReconnectPanel.SetActive(true);
client.Reconnected += reclaimed =>
{
    ReconnectPanel.SetActive(false);
    if (!reclaimed) ShowToast("Your session expired. You are back at your spawn point.");
};
client.ReconnectGaveUp += () =>
{
    ReconnectPanel.SetActive(false);
    ShowMainMenu(client.LastError);
};

void Update()
{
    if (client.IsReconnecting)
        ReconnectLabel.text = $"Reconnecting (attempt {client.ReconnectAttempt}, {client.ReconnectElapsedSeconds:0} s)...";
}

To start a new session on purpose, for example when the player picks another character, clear SessionToken before connecting. Disconnect() keeps it, so a deliberate disconnect and reconnect continues the session. Leave() clears it.

Notice a stalled server

Two things can go wrong on the server while the client's link to the gateway stays up:

  • The worker that simulated the player's pawn fails. The gateway holds the join with JoinHoldReason.Recovering and then despawns the pawn. LocalPlayerLost fires with LocalPlayerLossCause.WorkerLost, and LocalPlayerSpawned fires again when the gateway has placed the player. LocalPlayerLost fires with Despawned when game code on the server despawned the pawn instead. It doesn't fire when the connection ends; Disconnected covers that.
  • The worker stops without failing. Nothing arrives and the world stands still until the orchestrator declares the worker dead. When no state has arrived from the server for NebulaConfig.ClientStallSeconds (default 2 s), ServerStalled fires and IsServerStalled becomes true. ServerResumed follows exactly once: when state arrives again, or when the watch stops because the join went back to Starting or the connection ended. SecondsSinceServerState is the time since the last state.
client.ServerStalled += () => StallBanner.SetActive(true);
client.ServerResumed += () => StallBanner.SetActive(false);
client.LocalPlayerLost += (pawn, cause) =>
{
    if (cause == LocalPlayerLossCause.WorkerLost) ShowToast("Server hiccup, getting you back in...");
};

The stall watch counts anything a worker produced as state: entity and owner state, variables, maps, RPCs, spawns and despawns. It is off until the join completes and the first state arrives after it, so a join in progress, or a scope that sends nothing, never looks stalled. A predicted pawn's owner state arrives every tick, so a world with one never goes quiet for long. If your world can legitimately send nothing for longer than ClientStallSeconds, raise the setting, or set it to 0 to turn the watch off.

Leave the game

When the player quits, or goes back to the main menu, call Leave() instead of Disconnect(). The difference is what the server does with the player's pawn:

  • After Disconnect(), or a lost link, the worker keeps the pawn for NebulaConfig.SessionReclaimSeconds (default 30 s) in case the player comes back. Other players see it standing where the player left it.
  • After Leave(), the client sends the gateway a goodbye. The gateway ends the session at once: the worker calls your game mode's OnPlayerDespawn and removes the pawn, and a persistent pawn keeps its saved record for the player's next session. The player's next connection is a new session, admitted at once.

Leave() returns straight away, with ConnectionState set to Disconnected and Disconnected raised with ClientRequested. It keeps the link open until the gateway closes it, which confirms that the goodbye arrived, or until the timeout passes (0.5 s by default). IsLeaving is true until then, and LeaveFinished fires with true when the gateway confirmed or false when it didn't. A goodbye that doesn't arrive costs only the reclaim time: the server then treats the player as disconnected.

To quit the application, wait for the goodbye first. LeaveAndWait() is a coroutine that calls Leave() and waits:

QuitButton.cs
using System.Collections;
using Nebula;
using UnityEngine;

public sealed class QuitButton : MonoBehaviour
{
    // Call this from your Quit button.
    public void Quit() => StartCoroutine(LeaveThenQuit());

    private IEnumerator LeaveThenQuit()
    {
        var client = NebulaBootstrap.Instance != null ? NebulaBootstrap.Instance.Client : null;
        if (client != null) yield return client.LeaveAndWait();
        Application.Quit();
    }
}

A client that is not in the world yet has nobody to say goodbye to: Leave() then acts like Disconnect() and raises LeaveFinished with false at once.

Connect from the command line

A client connects to the gateway at startup when you pass one of these options:

  • -nebula-gateway <address:port> connects to that gateway. Replace <address:port> with the gateway's address, such as 127.0.0.1:7000.
  • -nebula-connect connects to NebulaConfig.GatewayAddress and GatewayPort.
  • -nebula-bot starts a bot client, which connects to the configured gateway.
Builds\Win64\Nebula.exe -nebula-role client -nebula-gateway 127.0.0.1:7000 -nebula-name Player

For these clients, ConnectsAutomatically is true. See Configuration for every runtime option.

Connect from a web build

A web build connects to the gateway over WebRTC and reads the same options from the page's query string, such as ?nebula-name=Player&nebula-connect. By default it connects to the gateway that served the page. See Build for the web.

UDP connections are not encrypted by default

A native client's UDP link is encrypted only when you turn it on. See Encrypt client connections. By default anyone who can reach the gateway address can join with an anonymous identity; to require a sign-in, see Authentication.

On this page