NetworkBehaviour and NetworkVariable
Create a networked entity, check its current role and authority, respond to lifecycle events, and replicate fields.
Add one NetworkIdentity to the root GameObject of each networked entity. Add one or more NetworkBehaviour components to the root or its children.
Use IsServer, IsClient, and IsOwner to inspect the process and client owner. Use HasAuthority to check whether the current worker controls the entity. Use IsGhost to check whether the worker holds a non-authoritative copy.
This page covers identities, behavior components, lifecycle methods, and replicated fields. See RPCs and worker messages for remote calls. See Prediction and reconciliation for predicted movement.
NetworkIdentity
Add NetworkIdentity to mark a GameObject as a networked entity. Put one identity on the root. Nebula associates each NetworkBehaviour on the root and its children with that identity.
| Member | Type | What it holds |
|---|---|---|
NetId | ulong | The entity ID created by the worker that spawned it: [workerIndex:16][sequence:48]. |
Epoch | uint | Monotonic authority epoch, bumped on every authority change. Every entity message carries it and stale-epoch messages are dropped everywhere. |
Container | Container | The container the entity is currently in. Positions travel container-local on the wire. |
OwnerClientId | ulong | The owning client's session ID, or 0 when no client owns it. It stays the same when that client reconnects. |
IsServerDriven | bool | No client owns this entity. The worker with authority drives it. Spawn it with NebulaWorker.SpawnServerDriven. |
HasAuthority | bool | Worker side: this process simulates the entity right now. Always false on a client. |
IsLocalPlayer | bool | Client side: the local client owns this entity. |
IsSpawned | bool | True between OnNetworkSpawn and OnNetworkDespawn. |
Motion.Velocity | Vector3 | World-space velocity reported by the authority. Carried through handover and streamed for extrapolation when the root NetworkTransform enables SyncVelocity. NetworkRigidbody mirrors its body's linear velocity here. |
OwnerWorkerIndex | ushort | Index of the worker last heard to be authoritative. Debug and overlay only. |
PrefabId | ushort | Index into NebulaConfig.NetworkPrefabs. Set at spawn time. |
Behaviours contains the NetworkBehaviour components in GetComponentsInChildren order. Nebula uses each component's position in this array as its network index. Use the same prefab and component order in every process.
An identity synchronizes initial placement and container changes. Add NetworkTransform for ongoing movement and NetworkRigidbody for worker-simulated physics. Use Identity.Motion.Velocity for the velocity reported by the authority.
NetworkIdentity also records a per-tick state history on workers for lag-compensated hit tests (StateAt). See Lag compensation.
NetworkBehaviour
Derive a component from NetworkBehaviour when it must inspect its role, replicate fields, or send RPCs. Add NetworkIdentity to the same GameObject or a parent.
public sealed class PlayerController : PredictedBehaviour<PlayerInput>
{
public NetworkVariable<string> PlayerName = new NetworkVariable<string>("");
public NetworkVariable<float> Health = new NetworkVariable<float>(MaxHealth);
public NetworkVariable<bool> IsDead = new NetworkVariable<bool>(false);
public override void OnNetworkSpawn()
{
_inputYaw = transform.eulerAngles.y;
}
}Role flags
A process is either a worker (server) or a client, never both. The flags on a behavior combine the process role with the identity's state:
| Flag | Definition |
|---|---|
IsServer | This process is a worker. |
IsClient | This process is a client. |
IsOwner | IsClient and the identity is the local player. |
HasAuthority | IsServer and this worker simulates the entity right now. |
IsGhost | IsServer and this worker holds a non-authoritative replica driven by a neighboring worker. |
What each process sees for one entity:
| Process | IsServer | IsClient | IsOwner | HasAuthority | IsGhost |
|---|---|---|---|---|---|
| Owning client | no | yes | yes | no | no |
| Any other client | no | yes | no | no | no |
| Authoritative worker | yes | no | no | yes | no |
| Worker holding a ghost | yes | no | no | no | yes |
Check IsOwner only for client ownership. On a worker, check HasAuthority and read OwnerClientId. A server-driven entity has OwnerClientId == 0 and no owning client.
Convenience accessors on the behavior forward to the identity: NetId, IsSpawned, OwnerClientId, Container and Identity itself.
Lifecycle hooks
All hooks are virtual with empty defaults.
| Hook | When |
|---|---|
OnNetworkSpawn() | The entity became known to this process: on the spawning worker, on a worker that just received a ghost, and on every client that instantiated it. IsSpawned is already true. |
OnGainedAuthority() | Worker only. This worker became authoritative: right after OnNetworkSpawn on the spawning worker, or on the receiving side of a handover after the handed state has been applied. |
NetworkTick(uint tick, float deltaTime) | Worker only, authoritative entities only, once per tick at 60 Hz. Ghosts and clients never get it. |
OnLostAuthority() | Worker only. Authority moved to a neighbor. The object stays alive as a ghost. |
OnContainerChanged(Container previous, Container current) | The identity's container changed. Fires on every process that tracks the entity, including during spawn (with previous == null) and whenever the entity crosses a boundary. |
OnNetworkDespawn() | The entity is about to be destroyed on this process. |
Expect this order on the worker that spawns an entity:
OnContainerChanged(null, spawnContainer)OnNetworkSpawnOnGainedAuthorityNetworkTickon each tick
During a cross-worker handover, the previous worker calls OnContainerChanged, WriteHandoverState, and OnLostAuthority. The new worker calls OnContainerChanged, ReadHandoverState, and OnGainedAuthority, then begins calling NetworkTick. When the authoritative worker despawns the entity, each process calls OnNetworkDespawn before removing its copy.
Two more pairs of hooks exist for state that is not a NetworkVariable: WriteHandoverState / ReadHandoverState for state only the next simulating worker needs, and WriteSyncState / ReadSyncState for per-tick delta channels. Both are described in /docs/guides/physics-and-handover-state.
Ghost semantics
A ghost is a non-authoritative copy of an entity on another worker. Nebula creates one near a container boundary so the next worker has the entity's state before a handover. On a ghost:
HasAuthorityis false andIsGhostis true.NetworkTickis not called. The pose is driven by the neighbor'sGhostStatestream through an interpolator, one tick behind the simulation tick; any Rigidbody is set kinematic.NetworkVariablevalues arrive throughGhostVarsand raiseOnValueChanged; writes are refused.- The ghost's colliders are real, so raycasts and overlaps on the authoritative worker hit it. That is how a hitscan resolves against a victim simulated elsewhere.
RemoteTick(double renderTick)is called once per tick before physics syncs, so interpolating behaviors can present their buffers.
OnNetworkSpawn runs on a ghost exactly as it does everywhere else, so gate authority-only setup on HasAuthority or put it in OnGainedAuthority. A ghost may later become authoritative (handover in) and an authoritative entity may become a ghost (handover out) any number of times without being respawned.
Do not branch on IsServer for simulation
IsServer is true on a ghost too. Simulation, spawning and variable writes belong behind HasAuthority; IsServer only tells you that you are in a worker process.
NetworkVariable<T>
NetworkVariable<T> is a replicated field. Declare it as a field on a NetworkBehaviour, optionally with an initial value, and Nebula discovers it.
public NetworkVariable<float> Health = new NetworkVariable<float>(100f);
public NetworkVariable<int> Kills; // null fields are created for you with default(T)Writing and reading
Only the authoritative worker may assign Value. Assignments from a client or a ghost are logged as a warning and ignored:
NetworkVariable on PlayerController written without authority (netId 281474976710657); ignoredThe check applies once the entity is spawned. Before that (in a constructor or Awake, or on a prefab you are configuring before Spawn) any process may set the initial value.
Assigning the current value makes no change. Assigning a different value marks the entity's variables as dirty and immediately raises OnValueChanged(previous, current) on the authoritative worker.
At the end of the tick, the worker sends all variables for that entity to the gateway and each worker that holds a ghost. Receiving copies apply the values and raise OnValueChanged for values that differ. A spawn or authority-transfer message also includes the current values.
[AuthorityRpc]
private void TakeDamage(float amount)
{
if (!HasAuthority || IsDead.Value) return;
Health.Value = Mathf.Max(0f, Health.Value - amount);
if (Health.Value <= 0f) IsDead.Value = true;
}Subscribe to OnValueChanged on any copy for presentation (a health bar on other clients, a tint on a ghost):
Health.OnValueChanged += (previous, current) => _bar.fillAmount = current / MaxHealth;NetworkVariable<T> converts implicitly to T, so float hp = Health; works. Assignment still goes through Value.
Discovery
When an identity starts, Nebula finds every public or private field whose type derives from NetworkVariableBase. It checks the concrete behavior class and each base class up to NetworkBehaviour.
Nebula assigns each field a numeric index based on declaration order. It also creates an instance for a field that is null. Follow these rules to keep the indices consistent:
- Declare variables as fields, not properties.
- Keep the same class and the same field order on every process (the same assembly on client and worker guarantees this).
- Adding or removing a variable changes every index after it. Network messages do not include the field name.
Supported value types
T must be something NetworkSerialization knows: the primitives, string, Vector2, Vector3, Quaternion, Color, byte[], any enum, anything implementing INetworkSerializable, or a type registered with NetworkSerialization.Register<T>. The exact list and how to extend it are in /docs/guides/serialization.
Equality uses EqualityComparer<T>.Default, so a struct T needs a sensible Equals (or IEquatable<T>) for the no-op check and OnValueChanged filtering to work; without one, every assignment counts as a change.
What travels on the wire
Nebula does not send a per-variable delta. When any NetworkVariable on an entity changes, it sends every variable on that entity in discovery order. Receivers still raise OnValueChanged only for values that differ.
For a collection that changes a few entries at a time, such as an inventory, use a NetworkMap: it sends only the entries that change, and it is not part of the variable block.
Every client that holds the entity receives its variables. For state only some clients may see, such as a player's inventory, use the sync channel with a sync audience instead.
Use NetworkVariable for state that changes occasionally, such as health, a display name, or a target. Put per-tick values in a NetworkTransform, predicted state, or a custom sync component.
Sending a string every tick
A NetworkVariable<string> that changes often re-sends every other variable on the entity with it. Keep text that changes every tick out of NetworkVariables; send it through the sync channel or an RPC instead.
Related
Nebula and other Unity networking libraries
An honest comparison of Nebula with Netcode for GameObjects, Mirror, FishNet, and Photon Fusion, and the gaps Nebula intends to close.
Replicated maps
Replicate a keyed collection, such as an inventory or a set of discovered places, sending only the entries that change.