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:
| Option | Use it when |
|---|---|
Add NebulaTitleScreen | You want a working connection screen during development. |
Call NebulaClient.ConnectTo from your own UI | You want the connection flow to match your game. |
| Pass connection arguments on the command line | You 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.
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:
| Member | Use it to |
|---|---|
ConnectionState and ConnectionStateChanged | Show Disconnected, Connecting, Connected, or InGame. |
Join, JoinEstimatedSeconds and JoinStateChanged | Show a "world starting" screen while the mesh boots a worker (see below). |
LocalPlayer and LocalPlayerSpawned | Hide your menu when the player's entity arrives. |
LocalPlayerLost | Find 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 SecondsSinceServerState | Show "server not responding" while no state arrives (see below). |
ClientId | Read the local session ID, a ulong the gateway assigned in its welcome. NebulaRuntime.LocalClientId holds the same value. It is 0 before the welcome. |
Identity | Read the player's identity across sessions (see Authentication). |
SessionToken and SessionReclaimed | Keep or drop the session across reconnections (see below). |
JoinRejected | Show why the gateway refused the join. When the refusal stops automatic retries, ConnectionState is already Disconnected and WantsConnection is false. |
GatewayDraining | Tell the player that the client is switching gateways (see below). |
Disconnected and LastDisconnect | Find out why the connection ended, as a typed reason, and whether the client will retry (see below). |
IsReconnecting, ReconnectAttempt and ReconnectElapsedSeconds | Show a "reconnecting" screen with its progress (see below). |
Reconnecting, Reconnected and ReconnectGaveUp | Show and hide that screen, and return to your menu when the client gives up. |
LastError | Show why the last connection attempt failed or ended. It is empty while the connection is healthy. |
WantsConnection | Tell whether the client is connecting or will retry. |
ConnectsAutomatically | Hide 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 LeaveFinished | Leave 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, aDisconnectReasonyour code can branch on.Message, readable text for logs and the player. It matchesLastErrorwhen the client set one.WillRetry, which istruewhen 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.
DisconnectReason | What happened |
|---|---|
Timeout | The connection was up, and the gateway stopped answering for longer than the transport's disconnect timeout. The gateway or the network between went away. |
NetworkUnreachable | The player's own network is down. |
HostUnreachable | Nothing answered at the gateway address, the connection was refused, or the name didn't resolve. |
ClosedByServer | The gateway closed the connection without saying why, or restarted and no longer knows the client. |
JoinRefused | The 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. |
SessionReplaced | The player connected again somewhere else. The client doesn't reconnect. |
GatewayDraining | The gateway is being taken out of service. The client reconnects at once. |
ServerShutdown | The gateway is shutting down and no other gateway was serving: the server is going away or restarting. The client reconnects on its normal schedule. |
Kicked | The 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. |
EncryptionFailed | The encrypted connection was refused: the key exchange failed or the gateway's certificate didn't match GatewayFingerprint. |
ClientRequested | Your code called Disconnect() or Leave(), directly or through ConnectTo. |
Unknown | The 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).
Reconnect after a lost link or a draining gateway
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.
ConnectionStatebecomesDisconnected,Disconnectedfires with the reason andWillRetryset totrue,LastErrorreadsdisconnected from gateway (retrying), and the client starts reconnecting to the same address (see below). The worker keeps the pawn forNebulaConfig.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, theSessionReplacedevent 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
GatewayDrainingwith the number of seconds it gives its clients, the client drops the link, theGatewayDrainingevent 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 reasongateway 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,Disconnectedfires withServerShutdownandWillRetryset totrue, 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.KickorNebulaGateway.Kick.Disconnectedfires withKicked, the server's reason inMessageand your game's code inDisconnectInfo.Code. The session is over: the client clearsSessionToken, setsWantsConnectiontofalseand 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:
IsReconnectingbecomestrueandReconnectingfires, afterDisconnected.ConnectionStatekeeps moving betweenDisconnected,ConnectingandConnectedwhile the client retries, so useIsReconnectingto decide what to show.ReconnectAttemptcounts the attempts started since the drop, andReconnectElapsedSecondscounts the seconds.- When a gateway welcomes the client again,
Reconnectedfires withtrueif the session and pawn came back (SessionReclaimed), orfalseif 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.Recoveringand then despawns the pawn.LocalPlayerLostfires withLocalPlayerLossCause.WorkerLost, andLocalPlayerSpawnedfires again when the gateway has placed the player.LocalPlayerLostfires withDespawnedwhen game code on the server despawned the pawn instead. It doesn't fire when the connection ends;Disconnectedcovers 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),ServerStalledfires andIsServerStalledbecomestrue.ServerResumedfollows exactly once: when state arrives again, or when the watch stops because the join went back toStartingor the connection ended.SecondsSinceServerStateis 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 forNebulaConfig.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'sOnPlayerDespawnand 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:
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 as127.0.0.1:7000.-nebula-connectconnects toNebulaConfig.GatewayAddressandGatewayPort.-nebula-botstarts 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 PlayerFor 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.
Private sync state
Choose which clients receive a behavior's sync state, such as only the owner, no client at all, or clients your own rule selects.
Authentication and player identity
Give every player a stable identity across sessions, either anonymously or from an OpenID Connect provider such as Google, Auth0, Firebase, or Clerk.