This document is about: FUSION 1
SWITCH TO

This page is a work in progress and could be pending updates.

5 - Property Changes

Overview

When defining networked properties, Fusion will replace the provided get and set stubs with custom code to access the network state. This means that the application cannot use these methods to deal with changes in property values, and creating separate setter methods will only work locally.

To solve this problem, Fusion provides an argument to the [Networked] attribute that can be used to specify a static callback method to be invoked whenever that property changes.

This is useful for spawning local visual effects in response to state changes or perform other tasks that does not directly impact gameplay logic. This is an important caveat since property changes may occur multiple times due to re-simulation (or, twice, to be exact, one on prediction and again if the prediction was incorrect) or it may be skipped entirely if the property changes back and forth between two values faster than network states are being sent (or a packet is dropped).

The OnChanged callback is invoked as part of the Update() cycle of Unity (strictly speaking, it's called before Update), but it is not part of the simulation and simply reacts to changes from one Unity frame to the next. This is one more reason why a brief toggle of values may go undetected: If multiple snapshots are handled in a single frame, and the last of them returns the networked property to its original value, no change will be detected.

The primary benefit of using change callbacks over common messages like RPCs is that the callback is executed immediately after the tick in which the value is changed, where as an RPC may arrive later when the game is in an completely different state.

Consult the Manual for an in-depth description of this topic

Adding a Change Listener

The property changed callback is defined with the OnChanged parameter on the [Networked] attribute itself and takes the name of a static method accepting a single parameter of type Changed<T> where T is the type of the enclosing Behaviour.

The provided callback must be a static method because this allows Fusion to have a single delegate for the type rather than allocating delegates for each instance of the object.

In this example, the goal is to color the cube white when a ball is fired and then fade it towards blue.

To trigger the effect, the host will toggle a single bit in a networked variable. Since no more than one ball can ever be spawned in a single tick (by a given player), each new spawn will change the value of the bit to something that is different from the previous tick and thus trigger the OnChanged callback.

Before adding the code, note that this design can fail - as already mentioned, changes can go undetected, especially if they are of the flip/flop kind. To make it more resilient to this, one could replace the NetworkBool with a byte or an int and bump it on each invocation instead. In the end, it is a question of how important the visual effect is vs. how much bandwidth it consumes.

With that out of the way, open the Player class and add a new property as well as a simple implementation of the callback:

C#

[Networked(OnChanged = nameof(OnBallSpawned))]
public NetworkBool spawned { get; set; }

public static void OnBallSpawned(Changed<Player> changed)
{
  changed.Behaviour.material.color = Color.white;
}

This obviously assumes that the Player has a material property which can be used to change the color of the cube mesh, so go ahead and add a new property for this:

C#

private Material _material;
Material material
{
  get
  {
    if(_material==null)
      _material = GetComponentInChildren<MeshRenderer>().material;
    return _material;
  }
}

The color should be updated in Render() as a linear interpolation from the current color towards blue. This is done in Render() rather than Update() because it is guaranteed to run after FixedUpdateNetwork() and it uses Time.deltaTime rather than Runner.DeltaTime because it is running in Unity's render loop and not as part of the Fusion simulation.

C#

public override void Render()
{
  material.color = Color.Lerp(material.color, Color.blue, Time.deltaTime );
}

All that remains is to trigger the callback by toggling the spawned property after calling Runner.Spawn():

C#

...
Runner.Spawn(_prefabBall, transform.position+_forward, Quaternion.LookRotation(_forward));
spawned = !spawned;
...

Keep in mind that there are two places where Spawn() is called and both should toggle spawned.

But Why?

Q: But why not simply set the color immediately when calling spawn?

While this would work for the host and for the client with Input Authority because both are predicting based on the players input, it would not work for proxies.

Q: But what if the color was a networked property that was simply applied locally on all clients in Render(), surely there would be no need for an OnChanged handler?

That would indeed work, but it would need to be animated on the host and it would generate a lot of unnecessary traffic. Generally, visual effects should be triggered by the state authority and then left to run autonomously on each client. As much as everyone loves sparks, nobody cares if a particular spark is flying in one direction or the other.

Accessing Previous States

This simple example did not care about the actual value of the changed property, but it should be noted that the Changed<T> parameter provided to the callback has a reference to the source behaviour as it was when the change occurred, allowing the application to access all the networked properties of the behaviour during that tick.

What is maybe less obvious, is that there is also a method to load the prior state of the behaviour, effectively providing the application with the state from the previous tick for all networked properties:

C#

  var newValue = changed.Behaviour.someNetworkedProperty;
  changed.LoadOld();
  var oldValue = changed.Behaviour.someNetworkedProperty;

Similarly, the application can load the new state again with LoadNew(), but it does not need to worry about resetting the state before exiting, since the state is only relevant in the context of this particular callback.

Back to top