Nebula

Spawning, game modes and mesh settings

How entities enter and leave the mesh, what a NebulaGameMode is for, and how workers coordinate through mesh-wide settings.

Spawn each networked entity from a worker. The worker creates its network ID, takes authority, and places it in a container. If another worker controls that container, Nebula transfers the entity on the next tick.

Use the APIs on this page to create player entities, register prefabs, create server-driven entities, and coordinate low-frequency game rules across workers.

NebulaGameMode

Create one NebulaGameMode subclass to define worker-side game startup and player spawning. Add exactly one instance to the game scene. Without one, workers cannot spawn players.

HookWhen
OnSpawnPlayer(NebulaWorker worker, in PlayerInfo player, Container container)The gateway asked this worker to create a player. Instantiate the prefab, spawn it and return the identity. player carries the session ID (ClientId), display name, bot flag, and the player's identity across sessions. By default it calls the overload below.
OnSpawnPlayer(NebulaWorker worker, ulong clientId, string playerName, Container container)The same hook without the identity. Override one of the two.
OnPlayerDespawn(NebulaWorker worker, NetworkIdentity player)The player's session ended: nobody reclaimed it within NebulaConfig.SessionReclaimSeconds, or the player left on purpose (NebulaClient.Leave) or was kicked. The worker despawns the entity after this returns.
OnPlayerDisconnected(NebulaWorker worker, NetworkIdentity player)The player's link dropped, or the gateway's link to this worker did, and the reclaim grace started. The pawn stays in the world. Drop what it holds, take it out of a seat, or save a checkpoint here.
OnPlayerReconnected(NebulaWorker worker, NetworkIdentity player)The player came back within the grace and has the same pawn again. OnSpawnPlayer is not called.
OnWorkerStarted(NebulaWorker worker)The worker is listening on its port. Note that registration with the control plane happens later; see IsRegistered below.

OnSpawnPlayer is called on whichever worker the gateway picked, with the container the gateway chose. If the hook returns an identity that is not yet spawned, the worker spawns it for that client; if it returns null, the worker logs an error and the client gets no pawn.

The client ID is a ulong session ID, unique across every gateway of the mesh. It stays the same when the player reconnects within the session, through any gateway, and the reconnecting player gets the same pawn back when the worker still holds it, so OnSpawnPlayer is not called again. When a client's link drops, the worker keeps the pawn for SessionReclaimSeconds (default 30 s) with no input; if nobody reclaims it, OnPlayerDespawn runs and the pawn is despawned. A player who reconnects after that gets a new pawn through OnSpawnPlayer with the same session ID. Store the ID as a ulong; see Sessions and reconnection.

Players who drop and come back

The worker that owns a player's pawn calls OnPlayerDisconnected when the reclaim grace starts: the client's link dropped, or the gateway's own link to the worker did. It calls OnPlayerReconnected if the player comes back in time, or OnPlayerDespawn when the grace runs out. A player who leaves with NebulaClient.Leave, or is kicked, goes straight to OnPlayerDespawn, and a grace of 0 calls neither of the other two.

NetworkIdentity.IsOwnerConnected is false for the whole grace, on the worker that owns the pawn, on every worker holding a ghost of it, and on every client that holds it. OwnerConnectedChanged fires on each copy when it changes, so a client can mark players who are away.

If the pawn is handed to another worker during the grace, the flag goes with it and the new owner does not call OnPlayerDisconnected again; it calls OnPlayerReconnected or OnPlayerDespawn when the grace ends. Each drop calls OnPlayerDisconnected once, on the worker that owned the pawn when the worker heard of the drop.

ExampleGameMode.cs
public override void OnPlayerDisconnected(NebulaWorker worker, NetworkIdentity player)
{
    // Don't leave a disconnected player holding anything or sitting in a vehicle.
    player.GetComponent<Carrier>()?.DropAll();
    player.GetComponent<Seat>()?.Leave();
    if (player.Persistent != null) worker.Persistence?.SaveNow(player);
}

Carrier and Seat stand for your own components.

NebulaContainerHelpers.RandomPointIn(container, margin, y) is a static helper: a uniform random point inside the container's floor footprint, margin metres in from the edge, at height y.

Create a game mode like this:

ExampleGameMode.cs
public sealed class ExampleGameMode : NebulaGameMode
{
    public GameObject PlayerPrefab;

    public override NetworkIdentity OnSpawnPlayer(NebulaWorker worker, ulong clientId, string playerName, Container container)
    {
        var position = NebulaContainerHelpers.RandomPointIn(container, margin: 2f, y: 0f);
        var identity = NetworkPrefabs.Instantiate(
            NetworkPrefabs.IdOf(PlayerPrefab),
            position,
            Quaternion.identity,
            container.transform);
        identity.name = $"Player {playerName} ({clientId})";
        worker.Spawn(identity, container, clientId);
        return identity;
    }

    public override void OnPlayerDespawn(NebulaWorker worker, NetworkIdentity player)
    {
        NebulaLog.Info($"player left: {player.name}");
    }
}

Setting NetworkVariable values between Instantiate and Spawn is the normal way to give an entity its initial state: the spawn message carries the full variable blob, so late joiners and ghosts see the values from the first frame.

If your game uses authored spawn points, find one inside the container passed to OnSpawnPlayer. Nebula provides container geometry, but it does not provide a spawn-point component or placement rules.

Wait for player data before spawning

Override BeginSpawnPlayer when a player needs saved data or game content before entering the world. Start the load, then call its complete callback on the Unity main thread with a factory that creates and returns the player's NetworkIdentity. Put entity creation inside that factory. Nebula skips the factory if the join was cancelled or replaced and invokes an accepted factory at most once.

Apply saved state inside the factory before spawning or returning the entity. Nebula does not move callbacks from background threads to the main thread; your loading code must arrange that. The default BeginSpawnPlayer calls the existing OnSpawnPlayer hook synchronously, so existing game modes need no change.

Registering prefabs

Add NetworkTransform to each moving prefab's root before registration. A bare NetworkIdentity receives its full initial placement, including scale, but does not send ongoing movement. Predicted controllers, NetworkRigidbody, and a Container on the root require an enabled root transform component. Keep static entities opted out unless their transform must change remotely. See NetworkTransform for component choices.

Every prefab that can be spawned over the network goes in NebulaConfig.NetworkPrefabs, the list on the Resources/NebulaConfig asset loaded by workers and clients. NebulaBootstrap passes that list to NetworkPrefabs.Register at startup. The standalone gateway forwards prefab IDs without loading prefabs; the orchestrator reads exported field schemas for its persistence dashboard.

RuleDetail
Index is the wire idNetworkPrefabs.IdOf(prefab) returns the list index as a ushort; it is what travels in spawn messages. Reordering the list changes ids on every process at once, which is fine as long as every process runs the same build.
NetworkIdentity requiredRegister logs an error for a prefab without one.
Instantiate through the tableNetworkPrefabs.Instantiate(prefabId, position, rotation, parent) instantiates, stamps PrefabId on the identity and calls Initialize(), which discovers the behaviours, their NetworkVariable fields and the sync-channel behaviours in a deterministic order.

An identity spawned with PrefabId still at its default logs an error: instantiate through NetworkPrefabs (or NebulaWorker.SpawnPrefab, which does it for you).

The worker spawning API

All spawning happens on a worker through NebulaWorker. On a worker the instance is NebulaBootstrap.Instance.Worker; game code on a NetworkBehaviour also receives it as the worker argument of the game mode hooks.

MemberWhat it does
Spawn(NetworkIdentity identity, Container container, ulong ownerClientId = 0)Make an already-instantiated identity live: mint the id, set epoch 1, take authority, put it in the container (or resolve one from its position when container is null), run OnNetworkSpawn then OnGainedAuthority on every behaviour, and announce it to the gateway. A non-zero ownerClientId makes it that client's player entity.
SpawnPrefab(GameObject prefab, Vector3 position, Quaternion rotation, Container container, ulong ownerClientId = 0)NetworkPrefabs.Instantiate followed by Spawn, parented under the container's transform. Returns the identity.
SpawnServerDriven(NetworkIdentity identity, Container container)Spawn with no owning client and NetworkIdentity.IsServerDriven set. Whichever worker holds authority drives it; a PredictedBehaviour<TInput> on it gets its input from GatherServerInput instead of a client.
Despawn(NetworkIdentity identity)Authority only. Sends EntityDespawn to the gateway and GhostDespawn to every worker holding a ghost, runs OnNetworkDespawn, destroys the GameObject. Calling it on a ghost logs a warning and does nothing.
Find(ulong netId)Any entity resident on this worker, authoritative or ghost, or null.
FindPlayer(ulong clientId)The entity owned by a client (session ID), or null.
EntitiesEvery resident entity (authoritative and ghosts).
AuthoritativeThe entities this worker simulates right now.

Spawning is synchronous. When Spawn returns, the entity has a network ID, appears in Authoritative, and has received its spawn and authority callbacks.

To spawn and launch a networked prop, use:

PlayerController.cs
private void SpawnProjectile(GameObject projectilePrefab, Vector3 origin, Vector3 direction)
{
    var worker = NebulaBootstrap.Instance != null ? NebulaBootstrap.Instance.Worker : null;
    if (worker == null || projectilePrefab == null) return;

    var identity = worker.SpawnPrefab(
        projectilePrefab,
        origin,
        Quaternion.LookRotation(direction),
        Container);
    var body = identity.GetComponent<NetworkRigidbody>();
    if (body != null) body.SetVelocity(direction * 20f);
    else identity.Velocity = direction * 20f;
}

Entity ids

Each spawning worker creates a 64-bit entity ID: [workerIndex:16][sequence:48]. In code, this is ((ulong)WorkerIndex << 48) | ++sequence.

The ID stays with the entity through every handover. Its prefix identifies the worker that created it, not the worker that currently controls it. Use NetworkIdentity.OwnerWorkerIndex to find the current worker.

Spawning into a container you do not own

Spawn initially gives authority to the worker that called it, even when another worker controls the target container. On the next tick, the worker detects the assignment and sends an AuthorityTransfer to the container's worker.

Two properties tell you whether that handover can happen yet:

PropertyMeaning
IsRegisteredThe worker has registered with the control plane. Leases and settings are not visible before this.
MeshReadyEvery container has an owner, and every owner that is not this worker is a connected peer that has completed its hello. Spawning into a foreign container before this leaves the entity on the spawner, which keeps authority and logs a warning every five seconds until the owner connects.

Wait for MeshReady

Before you spawn into containers controlled by other workers, wait for MeshReady. Add a timeout so your game can report a connection or assignment problem instead of waiting forever. A container without an active worker cannot satisfy MeshReady until the orchestrator assigns it again.

Worker events

NebulaWorker raises four events, all on the main thread:

EventSignatureFires
EntitySpawnedAction<NetworkIdentity>After Spawn has announced the entity.
EntityDespawnedAction<NetworkIdentity>After OnNetworkDespawn, just before the GameObject is destroyed.
AuthorityHandedOffAction<NetworkIdentity, string toWorkerId>This worker sent an AuthorityTransfer and is now a ghost holder.
AuthorityReceivedAction<NetworkIdentity, string fromWorkerId>This worker applied an incoming transfer and is now authoritative.

Per-behaviour code usually wants the OnGainedAuthority / OnLostAuthority hooks on NetworkBehaviour instead; the events are for code that watches the whole population, such as a director.

Mesh settings

Workers do not share memory and the control plane is never on the per-tick path, so Nebula gives game code exactly one low-volume way to agree on something across the mesh: mesh settings, string key/values on IControlPlane that Nebula attaches no meaning to.

MemberWhat it does
IControlPlane.SettingsIReadOnlyDictionary<string, string> of the current values. Can be null before the control plane has connected.
IControlPlane.SetSetting(string key, string value)Create or overwrite one setting. Every subscriber sees the change through the Changed event on its next Tick.
GetSetting(key, fallback)Extension method: the string value or fallback.
GetSettingInt(key, fallback)Extension method: the value parsed as an int, or fallback when missing or unparsable.

Settings come from three places:

  • Seed settings when the orchestrator starts with -nebula-settings key=value,key=value.
  • Edit settings in the Nebula Dashboard or send POST /api/settings with a key and value. See Orchestrator and Nebula Dashboard.
  • Call SetSetting from game code on a worker.

Settings are a coordination channel, not a state store: never write them per tick, and never put anything in them that a specific entity owns.

Coordinate work from a setting

Nebula does not provide a singleton gameplay worker. When every worker must respond to a shared setting, make each worker reconcile its local state with that value.

Use this pattern:

  1. Wait until IsRegistered is true and the setting exists.
  2. Read active worker information from IControlPlane.Workers.
  3. Divide the work deterministically by worker index.
  4. Change only entities for which the current worker has authority.
  5. Repeat the check at a low rate so workers can recover from a delayed update, handover, or restart.

Make the operation idempotent, which means running it twice with the same inputs produces no additional change. Store any identifier needed for later reconciliation in a NetworkVariable so it moves with the entity during a handover.

Rules

  • Workers spawn; clients ask. A client that wants something created sends a ServerRpc and the owning worker spawns it (see RPCs).
  • Reconcile mesh-wide logic. There is no singleton worker. Store low-frequency targets in mesh settings, then let each worker reconcile its own authoritative entities.
  • Entities live on workers. A worker that dies takes its authoritative entities with it; the gateway drops them and players are respawned elsewhere through OnSpawnPlayer, with the same session ID. Durable state belongs in your own persistence layer, written from OnLostAuthority or on your own schedule.

What the client sees when its worker fails

While the gateway places a player again after a worker failure, it holds the join with JoinHoldReason.Recovering rather than WorldStarting, and it says so before it despawns the pawn. On the client:

  1. JoinStateChanged fires with JoinState.Starting, and NebulaClient.JoinHoldReason is Recovering. Show "getting you back in" rather than a loading screen for a new world.
  2. LocalPlayerLost fires with LocalPlayerLossCause.WorkerLost as the pawn is despawned. When game code on the worker despawns the pawn instead, the cause is Despawned.
  3. LocalPlayerSpawned fires when the pawn is back, from its last checkpoint if it is persistent, and Join is Joined again.

A worker can also stall without dying: it stops sending state, and its entities stand still until the orchestrator declares it dead after WorkerTimeoutSeconds (default 5 s). A client notices first. When no state has arrived for NebulaConfig.ClientStallSeconds (default 2 s) while the player is in the world, NebulaClient.ServerStalled fires, and ServerResumed fires when state arrives again or the join goes back to Starting. SecondsSinceServerState gives the current gap. See Notice a stalled server.

On this page