Containers and handover
How containers determine worker authority and how Nebula transfers an entity across a container boundary.
Use containers to decide which worker controls each entity. When an entity approaches a container owned by another worker, Nebula sends that worker a non-authoritative copy called a ghost. When the entity crosses the boundary, Nebula transfers authority and the latest state to the new worker. Nebula calls this operation a handover.
Containers
Add the Container MonoBehaviour to a scene object to create a box volume. The object's transform defines the container's local coordinate space. When you add the component in the Editor, Nebula gives it a random ContainerId and sets Size and Center to the bounds of the object's renderers, or its colliders when it has no renderers. To wrap the box around the object again after you change it, select Fit Size and Center to Bounds in the Inspector. You can also call FitToBounds() from code. Keep the ID stable after the scene is in use.
Nebula parents contained entities under the container transform and sends positions in that local space. Moving the container therefore does not change the local coordinates of its contents.
Every container is described by three independent properties:
| Property | Read it from | Values |
|---|---|---|
| Where the box is | FrameMode | Fixed: placed relative to the container it is in. Entity: carried by an entity and moves with it (a dynamic container). |
| Who simulates what is inside | Authority (what you set), IsLeased (what applies now) | Leased: the container has its own lease and worker. Inherited: whoever simulates the container it is in simulates this one too. Auto, the default: leased for a fixed box, inherited for a carried one. |
| Where it comes from | Source | Baked: authored in a scene or baked into a world manifest. Runtime: registered while the mesh runs (a runtime container). Prefab: part of a carrier prefab. |
Any combination is allowed. For example, a building on a planet can be part of a prefab and leased, so a busy base moves to a worker of its own. IsDynamic and IsRuntime are shorthands for an Entity frame and a Runtime source.
A container can also have a physics frame of its own: a separate physics scene in which it stands still, so what is inside simulates in its local coordinates.
The container tree
Containers form a tree. The root of the tree is the scope, and every container has a Parent:
- A baked container's parent is the smallest baked container of the same scope that encloses it. Nebula computes it when it loads the scene or manifest, so every process builds the same tree.
- A runtime container names its parent when it is registered, and its box is given in the parent's local space.
- A carried container's parent is the container its carrier is in, so it changes as the carrier moves.
Children lists a container's baked and runtime children, and Depth counts the containers above it. An inherited container takes its owner from its parent, so when the orchestrator assigns the parent to another worker, everything in the inherited child goes with it.
A leased container may only sit under a parent that does not move, or under a moving parent that has a physics frame of its own. Under a moving parent without a frame, the leased container's worker would simulate against a pose that arrives one replication delay late. Nebula logs a warning and simulates it as inherited until it is somewhere else (AuthorityDemoted).
When an entity moves, Nebula places it in the deepest container that holds its position, and uses the smaller one when two are equally deep. An entity is never placed inside a container that it carries, directly or through anything that rides inside it. A carrier with a physics frame is also never placed inside a smaller carrier with a frame, so a ship set down over a larger carrier's origin goes inside the carrier, not the other way round.
At startup, ContainerRegistry.Rebuild() collects the containers in the loaded scene. It sorts them by ID and assigns each one an Index. Nebula uses the ID for control-plane assignments and the smaller index in network messages.
ContainerRef identifies a baked container by its index. It identifies a carried container by the network ID of its carrier and a runtime container by the 64-bit ID the game gave it.
Nebula treats two containers as neighbors when their bounds touch or overlap. The registry records static neighbors in Neighbors. Call ContainerRegistry.NeighborsOf when you also need dynamic containers that currently touch the volume.
Read OwnerWorkerId, OwnerWorkerIndex, and LeaseEpoch to inspect a container's current assignment. Use IsOwnedBy, Contains, and SignedDistance for ownership and bounds checks. Use ToLocal and ToWorld to convert positions.
Seams are implicit: every container boundary carries two parameters from NebulaConfig:
| Parameter | Default | Effect |
|---|---|---|
GhostBandMargin | 4 m | An authoritative entity within this distance of a neighbouring container owned by another worker is ghosted there ahead of time. |
HandoverHysteresis | 0.35 m | An entity must be this far inside a new container before authority flips, so an entity dithering on the line does not thrash. |
GhostLingerSeconds | 2 s | A ghost stays resident this long after its entity leaves the band, so a quick return is free. |
You do not need to create separate boundary or transition objects.
Leases and epochs
The orchestrator writes one lease row for each leased container. This row records the container ID, worker ID, epoch, and state. Every 500 milliseconds, the orchestrator distributes containers across active workers. It keeps current assignments when possible so a rebalance moves fewer containers. It never assigns an inherited container: that container has no worker of its own.
When the orchestrator assigns a container to a different worker, it increments the assignment's lease epoch. An epoch is a number used to order ownership changes.
Every entity has its own authority Epoch, which increases when its controlling worker changes. Entity messages include the network ID and epoch. The gateway and workers reject a message with an older epoch or an invalid sender. This prevents two workers from controlling the same entity after a temporary disagreement.
The epoch and the owning worker say who simulates an entity. Where the entity is, independent of that, is its location: a scope key, the container's ContainerId, and the pose in the container's local space. NetworkIdentity.Location reports it, it does not change on a handover, and every process resolves it to the same container. The entity location contract defines it.
The worker's tick
Each worker calculates its current tick from the wall clock and a fixed origin with NetworkTime.DerivedTick. This keeps tick numbers aligned without a coordinator. On every tick, a worker:
- Moves the ghosts it holds to the pose their owners streamed (kinematic, no physics solve of their own;
RemoteTickon their behaviours presents the sync channel). - Runs
NetworkTick(and thereforeSimulateon predicted behaviours) on every entity it has authority over, then lets PhysX step. - Re-evaluates container membership for its authoritative entities, with hysteresis, and transfers authority for entities that crossed into a container owned by another worker.
- Maintains the ghost band, which is the area within
GhostBandMarginof a neighboring container. It sendsGhostSpawnonce,GhostStatefor changed root transforms and reliable placement or recovery updates,GhostVarswhen aNetworkVariablechanges, andGhostSyncStatefor component sync data. It sends entities in a dynamic container to each worker that has a ghost of the carrier. - Streams
WorldState,OwnerState,EntityVarsandEntityStatefor its authoritative entities to the gateway. - Records every entity it has authority over for the lag-compensation history (
NetworkIdentity.StateAt); a ghost is recorded instead when its owner's stream arrives, tagged with the owner's tick.
Workers discover each other through the control plane and peer directly over UDP; the lower index dials.
What a ghost is
On a worker that does not control it, an entity exists as a ghost. GhostSpawn identifies the prefab and includes its container, pose, velocity, NetworkVariable values, and component sync keyframes.
On a ghost, IsGhost is true and HasAuthority is false. Nebula does not call NetworkTick, allow NetworkVariable writes, or send ClientRpc from that copy. A ghost can send AuthorityRpc to the entity's worker. The worker makes a ghost's Rigidbody kinematic, with or without NetworkRigidbody.
An enabled NetworkTransform moves a ghost from state sent by its controlling worker. Without one, the ghost keeps its initial placement until a container change or new spawn snapshot. Local raycasts and dynamic entities can detect the ghost. A local entity can collide with and respond to a ghost, but the collision does not change the ghost on its controlling worker.
The neighbour thus holds full warm state before the entity can cross. If the entity leaves the band without crossing, the ghost lingers for GhostLingerSeconds and is then despawned.
For what a collision with a ghost does and does not do, and why a joint across two workers is unsupported, see Distributed physics.
Ghost a large entity by its extent
By default, the ghost band measures an entity from its root position. That works for players, vehicles, and props a few meters across. It does not work for a building, a player-built structure, or a large ship whose root is far from a seam while its walls reach across it: no other worker gets a ghost, so a player simulated on the other side walks through the walls.
To fix this, give the entity an extent: a box in the entity's local space that the ghost band measures instead. The entity is then ghosted to every worker whose container the box comes within GhostBandMargin of, including containers that do not touch the entity's own container.
Choose the source in the Ghost band section of the entity's NetworkIdentity, or from code:
ExtentSource | What the ghost band measures |
|---|---|
None (default) | The root position, as for every entity without an extent. |
Explicit | The Extent box. The root's rotation and scale apply to it, as they do to a BoxCollider on the root. |
Colliders | A box around the entity's own enabled, non-trigger colliders. Colliders under another NetworkIdentity do not count. |
When the entity's shape changes at runtime, call SetExtent on the worker that has authority over it:
using Nebula;
using UnityEngine;
public class StructureSection : NetworkBehaviour
{
// Called on the authoritative worker after the section is edited.
public void OnEdited(Bounds localBounds)
{
Identity.SetExtent(localBounds);
}
}The next tick's ghost band uses the new box. An extent set at runtime travels with a handover. It is also saved and restored with the entity when the entity has a PersistentEntity. ClearExtent goes back to measuring the root, and UseColliderExtent switches to the colliders.
With Colliders, Nebula computes the box when it is first needed, when a direct child is added or removed, and every NetworkIdentity.ColliderExtentRefreshTicks (60) ticks. Call RefreshExtent after you add, remove, resize, or turn off a collider deeper in the hierarchy if the change must count sooner.
An extent changes only which workers hold a ghost:
- The root position still decides the entity's container, which worker has authority, and when a handover happens.
- The ghost is created from the prefab. If your game builds the entity's colliders at runtime, build them on the ghost too, from its
NetworkVariablevalues and sync state. - A ghost that the extent no longer reaches lingers for
GhostLingerSeconds, as for a root that left the band. - Client interest does not use the extent. Raise
RelevanceRadiusfor an entity that players should see from far away.
The extent is measured in the coordinates of the space the entity's container is in, so it works the same way inside a physics frame, such as a planet whose chunks are leased to several workers. It never reaches across a frame's boundary.
Entities without an extent cost nothing extra. Each entity with an extent adds one container registry query and a few box tests per tick on its authoritative worker, so give an extent only to entities that need it. The box is axis-aligned in the entity's space. A long, rotated entity is measured by the box around it, which can ghost it to a worker it does not quite reach. When the extent reaches out of its own container, the worker that owns an enclosing container, such as a planet's frame, also gets a ghost.
Complete the handover
When an entity moves farther than HandoverHysteresis into a container assigned to another worker, the current worker sends a reliable AuthorityTransfer message. It contains:
- the entity's complete final position and rotation in the new container's local space, local scale, and world-space velocity, even when ongoing transform synchronization omits those values,
- the new epoch,
- the full NetworkVariable blob,
- the not-yet-simulated inputs buffered for a predicted behaviour, so the input stream never breaks,
- a keyframe of the sync channel,
- whatever each behaviour wrote in
WriteHandoverState(angular velocity for a rigidbody, timers, RNG state), - the flags that say whether the entity is owned by a bot or is server-driven, so the per-worker counts stay right.
On the sending worker, the entity becomes a ghost and OnLostAuthority runs. For a short time, that worker forwards input that still arrives from the gateway.
On the receiving worker, Nebula updates the existing ghost or creates the prefab when no ghost exists. It applies the transferred state, calls ReadHandoverState, and then calls OnGainedAuthority. The entity becomes authoritative on the next tick.
Both workers report the change to the gateway. The gateway then accepts the new epoch and routes the owning client's input to the new worker.
If the two containers are owned by the same worker, none of this leaves the process: the entity is reparented, OnContainerChanged fires and LocalHandovers ticks up.
When the entity that crosses carries a container of its own, everything inside that container is transferred right after it, in the same tick and on the same ordered channel: a ship crossing a seam takes its crew along, and their container-local poses never change.
Moving into or out of a container with a physics frame of its own is a crossing: the worker that simulates the frame's carrier converts the entity's pose at that tick, and any other worker hands the entity to that worker first. See Cross the frame's boundary.
The client's copy keeps the same network ID during a handover. New snapshots include the updated epoch and worker index. The owning client can continue prediction because the transfer includes pending input.
Cross-container interaction
Use [AuthorityRpc] to apply an interaction to an entity controlled by another worker. For example, a worker can raycast against a ghost and call a damage method on the target. Nebula calls the method locally when the same worker controls both entities. Otherwise, it sends the call to the target's worker.
Nebula does not automatically validate the claim on the receiving worker. Implement validation in your game when needed. Use NetworkIdentity.StateAt to read recorded state for a lag-compensated query. See Lag compensation.
Container reassignment
When a container assignment changes, the previous worker sends one AuthorityTransfer for each authoritative entity in that container. Nebula does not currently prepare the new worker before changing the assignment. Persisted entities are read only after the new owner has held the lease for a grace period, so a handover always wins over a restore.
When you remove a worker, the orchestrator keeps it running until it has no assignments and reports no authoritative entities. It stops waiting when WorkerDrainTimeoutSeconds expires.
If a worker stops unexpectedly, its transient entities are lost. Entities carrying a PersistentEntity come back from their last checkpoint once another worker has held the container's lease for NebulaConfig.PersistenceRestoreGraceSeconds; see Persistence. The orchestrator reassigns its containers, and the gateway requests replacement player entities from another worker.
Tuning
Increase GhostBandMargin if fast entities reach a boundary before the next worker receives a ghost. A receiving worker that must create a new instance during handover is a sign that the margin may be too small.
Increase HandoverHysteresis if an entity near a boundary repeatedly moves between workers. To test values for one run, use -nebula-ghost-band and -nebula-hysteresis.
Partitioned worlds
For a world larger than one scene, use world partition to make each grid cell a container. Containers authored inside a cell become its children.
Nebula reads this container tree from a baked manifest. The manifest keeps container indices consistent while each process loads only the cells it needs.