Network Behaviour

Network Behaviours are the primary way to implement custom multiplayer behavior in Spatial. You create custom network behaviors by inheriting from the SpatialNetworkBehaviour class. A NetworkBehaviour requires a NetworkObject component to be present on the same GameObject or its parent (a behavior needs to be associated with a network object).

SpatialNetworkBehaviour is an abstract class that provides a set of methods and properties to interact with the network system.

Callbacks

CallbackDescription
Spawned()Called after the NetworkObject is bound to the network context and is ready to be used. When this is called, all of the behaviours' Network Variables will be initialized and their value can be accessed and modified by the client that has control of it.
Despawned()Called when the NetworkObject was destroyed and before it will be destroyed locally. The state of the object (such as Network Variables) can still be accessed at this point.

Optional Callback Interfaces

IOwnershipChanged

The IOwnershipChanged interface provides a method OnOwnershipChanged(NetworkObjectOwnershipChangedEventArgs args) that is called when the ownership of the object changes.

public class OwnershipChangeLogger : SpatialNetworkBehaviour, IOwnershipChanged
{
public void OnOwnershipChanged(NetworkObjectOwnershipChangedEventArgs args)
{
Debug.Log($"[{objectID}] Ownership changed: prev={args.previousOwnerActorNumber} new={args.newOwnerActorNumber}");
}
}
public class OwnershipChangeLogger : SpatialNetworkBehaviour, IOwnershipChanged
{
public void OnOwnershipChanged(NetworkObjectOwnershipChangedEventArgs args)
{
Debug.Log($"[{objectID}] Ownership changed: prev={args.previousOwnerActorNumber} new={args.newOwnerActorNumber}");
}
}

IVariablesChanged

The IVariablesChanged interface provides a method OnVariablesChanged(NetworkObjectVariablesChangedEventArgs args) that is called when any network variable on the object changes. This includes variables defined in other behaviours attached to the same object, and any synchronized Visual Scripting variables if used.

public class ExampleBehaviour : SpatialNetworkBehaviour, IVariablesChanged
{
private NetworkVariable<int> _health = new();

public void OnVariablesChanged(NetworkObjectVariablesChangedEventArgs args)
{
// Shows how to check if a specific behaviour variable has changed
if (args.changedVariables.ContainsKey(_health.id))
Debug.Log($"Health changed: {args.changedVariables[_health.id]}");

Debug.Log($"[{objectID}] Variables changed: {string.Join(",", args.changedVariables.Select(x => $"{x.Key}={x.Value}"))}");
}
}
public class ExampleBehaviour : SpatialNetworkBehaviour, IVariablesChanged
{
private NetworkVariable<int> _health = new();

public void OnVariablesChanged(NetworkObjectVariablesChangedEventArgs args)
{
// Shows how to check if a specific behaviour variable has changed
if (args.changedVariables.ContainsKey(_health.id))
Debug.Log($"Health changed: {args.changedVariables[_health.id]}");

Debug.Log($"[{objectID}] Variables changed: {string.Join(",", args.changedVariables.Select(x => $"{x.Key}={x.Value}"))}");
}
}

Callback Order

  1. Spawned() is called after the object is bound to the network context and is ready to be used.
  2. After Spawned() is called, we call the following once if they are implemented. This is useful for initializing the owner logic or the object's visual state if it depends on network variables.
    1. IOwnershipChanged.OnOwnershipChanged()
    2. IVariablesChanged.OnVariablesChanged() called with args.changedVariables containing all variables on the object.
  3. During the lifetime of the object, IVariablesChanged.OnVariablesChanged() and IOwnershipChanged.OnOwnershipChanged() will be called as the object changes state.
  4. Despawned() is called before the object is destroyed.

Network Variables

Behaviours can define synchronized state using NetworkVariable<T> fields or properties. Changes to these variables are automatically synchronized across clients.

public class Player : SpatialNetworkBehaviour
{
private NetworkVariable<int> _health = new(initialValue: 100);
private NetworkVariable<float> _jetFuel = new(initialValue: 100.0f);
private NetworkVariable<Color32> _color = new();

public override void Spawned()
{
// health and jetFuel are initialized to their initial values before this method is called
Debug.Log($"Player spawned with health: {_health.value} and jetFuel: {_jetFuel.value}");

// Initialize random color: we do this here because Random cannot be used in class initialization
// We check for `hasControl` to check if we are allowed to change object state (ownership related)
if (hasControl)
_color.value = Random.ColorHSV();
}
}
public class Player : SpatialNetworkBehaviour
{
private NetworkVariable<int> _health = new(initialValue: 100);
private NetworkVariable<float> _jetFuel = new(initialValue: 100.0f);
private NetworkVariable<Color32> _color = new();

public override void Spawned()
{
// health and jetFuel are initialized to their initial values before this method is called
Debug.Log($"Player spawned with health: {_health.value} and jetFuel: {_jetFuel.value}");

// Initialize random color: we do this here because Random cannot be used in class initialization
// We check for `hasControl` to check if we are allowed to change object state (ownership related)
if (hasControl)
_color.value = Random.ColorHSV();
}
}

Network variables are covered in-depth in the Network Variables section.

Adding or Destroying Behaviours at Runtime

Adding additional behaviours to a network object at runtime is not supported because the additional behaviours would not be synchronized across clients. Same goes for destroying behaviours at runtime.

Examples

Colored Object

public class ColoredObject : SpatialNetworkBehaviour, IVariablesChanged
{
[SerializeField] private MeshRenderer _meshRenderer;

private NetworkVariable<Color32> _color = new();

public override void Spawned()
{
// Spawner client will initialize the color
if (hasControl)
_color.value = Random.ColorHSV(0, 1, 1, 1, 0.5f, 1);
}

public void OnVariablesChanged(NetworkObjectVariablesChangedEventArgs args)
{
// We refresh the visual state here. We don't need to initialize the material color here
// because OnVariablesChanged is called after Spawned()
if (args.changedVariables.ContainsKey(_color.id)) // _color.id is the unique variableID (within the object)
_meshRenderer.material.color = _color.value;
}
}
public class ColoredObject : SpatialNetworkBehaviour, IVariablesChanged
{
[SerializeField] private MeshRenderer _meshRenderer;

private NetworkVariable<Color32> _color = new();

public override void Spawned()
{
// Spawner client will initialize the color
if (hasControl)
_color.value = Random.ColorHSV(0, 1, 1, 1, 0.5f, 1);
}

public void OnVariablesChanged(NetworkObjectVariablesChangedEventArgs args)
{
// We refresh the visual state here. We don't need to initialize the material color here
// because OnVariablesChanged is called after Spawned()
if (args.changedVariables.ContainsKey(_color.id)) // _color.id is the unique variableID (within the object)
_meshRenderer.material.color = _color.value;
}
}