This document is about: FUSION 2
SWITCH TO

Network Buffers

Overview

Fusion keeps a history of received state snapshots for each NetworkObject that it does not have StateAuthority over, representing the most recently known authoritative state. For each locally simulated object (Whether authoritative or predicted) there's an additional current and previous snapshot.

The [Networked] properties on a NetworkBehaviour provides direct access to the most recent of these snapshots (whether this is in the history or the local simulated state).

To obtain the two most recent snapshots for any NetworkBehaviour as well as a scaled relative time offset between them (for interpolation), use TryGetSnapshotsBuffers().

To access individual property values in the snapshots, the NetworkBehaviour base-class provides both low-level data access as well as the typed PropertyReader.

In addition to this, network behaviours allow monitoring of changes in its network state using the ChangeDetector class.

NetworkBehaviourBuffer and PropertyReader

The NetworkBehaviourBuffer is an abstraction of Fusion's internal data storage and provides an effective low cost read-only peek into networked state snapshots.

For advanced use-cases, the network buffer provides the low-level ReinterpretState<T>(offset) method which allow buffer data at any offset to be cast to specific data types. This is generally not recommended except in situations that are extremely performance sensitive or needs customized interpretation of the networked state.

Instead, Fusion provides the PropertyReader class. Property readers associates a C# type with a named property and its offset in the buffer. Property readers are cached internally by Fusion so local caching is not required except in highly performance critical scenarios.

To read a property from a NetworkBehaviourBuffer, simply call the Read() method on either the buffer or the reader:

C#

var reader = GetPropertyReader<T>(nameof(SomeProperty));
T t = reader.Read(buffer);
// or
t = buffer.Read(reader);

For the common case of reading the same property from two buffers (usually a previous and a current snapshot), Fusion provides a shortcut with:

C#

var reader = GetPropertyReader<T>(propertyname);
(previous,current) = reader.Read(previousBuffer, currentBuffer);

Snapshots and Interpolation

To access the two most recent snapshots of a NetworkBehaviourBuffer, call TryGetSnapshotsBuffers(). This will return the two buffers as well as their from and to ticks and an interpolation offset (alpha):

C#

if (TryGetSnapshotsBuffers(out var fromBuffer, out var toBuffer, out alpha))
{
    var reader = GetPropertyReader<T>(nameof(State));
    (from,to) = reader.Read(fromBuffer,toBuffer);
    fromTick = fromBuffer.Tick;
    toTick = toBuffer.Tick;
}

The buffers may be used as-is or a render-time accurate value may be interpolated between the two, using the returned interpolation offset.

NetworkBehaviourBufferInterpolator

To simplify interpolation, Fusion provides the NetworkBehaviourBufferInterpolator struct. This struct, given a NetworkBehaviour, will get the snapshot buffers and property readers and return interpolated values for named properties:

C#

var interpolator = new NetworkBehaviourBufferInterpolator(this);
Vector3 v = interpolator.Vector3(nameof(SomeVector3Property));

The NetworkBehaviourBufferInterpolator only supports types that has a sensible mathematical definition of interpolated values, but support for custom types can be added easily using C# extension methods:

C#

public static class NBBIExtension
{
    public static NameSpace.CustomType CustomType(this NetworkBehaviourBufferInterpolator interpolator, string propertyname)
    {
        (var from, var to) = interpolator.Behaviour.GetPropertyReader<NameSpace.CustomType>(propertyname).Read(interpolator.From, interpolator.To);
        // Do custom interpolation betwen "from" and "to" using "interpolator.Alpha"
        return CustomType.Interpolate(from,to,interpolator.Alpha);
    }
}

ChangeDetector

Fusion provides a way to track changes in the network state with the ChangeDetector. To obtain a change detector for a NetworkBehaviour, call GetChangeDetector(source, copy) on the NetworkBehaviour, and keep a reference to the returned instance:

C#

public override void Spawned()
{
    _changes = GetChangeDetector(ChangeDetector.Source.SimulationState);
}

The provided source parameter decides where on the timeline "new values" will be sampled. This can either be the local timeframe (SimulationState) or it can be the remote timeframe from or to tick (SnapshotFrom or SnapshotTo, respectively).

It is important to note that the SimulationState only exist for network objects that are included in the simulation. By default this means any object that the local peer has either state- or input-authority over, but it also exist for objects for which simulation has been explicitly enabled.

The copy parameter determines whether the initial state for the change detector should be copied at the time of construction (if not, then the first call to detect changes will establish the "before" state and always return an empty enumerator).

The "old values" from which the changes are derived, are simply the previous values from the last time the detector was invoked. Hence, changes can be detected at any point of execution - whether this is on exact tick boundaries in FixedUpdateNetwork() or at intermediate rendering frames in Render() or even Update().

Each created ChangeDetector maintains its own state independent of previously created detectors and as such, multiple change detectors can co-exist on the same NetworkBehaviour.

Using a ChangeDetector

The main entry-point for the ChangeDetector is the DetectChanges() method which returns an enumeration of all changed properties in the given timeframe, since the method was last called, as well as the previous and current buffers:

C#

foreach (var propertyname in _changes.DetectChanges( this, out var previousBuffer, out var currentBuffer))
{
    switch (propertyname)
    {
        case nameof(SomeProperty):
        {
            var reader = GetPropertyReader<T>(propertyname);
            (previous,current) = reader.Read(previousBuffer, currentBuffer);
            ...
            break;
        }
    }
}

Avoiding Boilerplate

The NetworkBehaviourBuffer as well as the PropertyReader were designed with performance and completeness in mind. It provides a very high degree of flexibility with minimal overhead, but it can also result in a lot of boilerplate code in cases where the behaviour has many properties and maybe does not care about individual property changes.

For these scenarios it can be beneficial to collect networked properties in structs implementing INetworkStruct and treat each such struct as a single property.

NetworkBehaviourWithState

The following is an example of a baseclass that trades some flexibility for ease of use. By storing the entire state of the object in a single struct and providing a method to check if anything in that struct changes, while also returning the before and after values, change detection can be reduced to a single method call.

Similarly, snapshots can be accessed for all properties at once without explicitly having to deal with property readers.

C#

public abstract class NetworkBehaviourWithState<T> : NetworkBehaviour where T : unmanaged, INetworkStruct
{
    public abstract ref T State { get; }

    private ChangeDetector _changesSimulation;
    private ChangeDetector _changesFrom;
    private ChangeDetector _changesTo;

    protected bool TryGetStateChanges(out T previous, out T current, ChangeDetector.Source source = ChangeDetector.Source.SimulationState)
    {
        switch (source)
        {
            default:
            case ChangeDetector.Source.SimulationState:
                return TryGetStateChanges(source, ref _changesSimulation, out previous, out current);
            case ChangeDetector.Source.SnapshotFrom:
                return TryGetStateChanges(source, ref _changesFrom, out previous, out current);
            case ChangeDetector.Source.SnapshotTo:
                return TryGetStateChanges(source, ref _changesTo, out previous, out current);
        }
    }

    private bool TryGetStateChanges(ChangeDetector.Source source, ref ChangeDetector changes, out T previous, out T current)
    {
        if(changes==null)
            changes = GetChangeDetector(source);

        if (changes != null)
        {
            foreach (var change in changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
            {
                switch (change)
                {
                    case nameof(State):
                        var reader = GetPropertyReader<T>(change);
                        (previous,current) = reader.Read(previousBuffer, currentBuffer);
                        return true;
                }
            }
        }
        current = default;
        previous = default;
        return false;
    }

    protected bool TryGetStateSnapshots(out T from, out Tick fromTick, out T to, out Tick toTick, out float alpha)
    {
        if (TryGetSnapshotsBuffers(out var fromBuffer, out var toBuffer, out alpha))
        {
            var reader = GetPropertyReader<T>(nameof(State));
            (from, to) = reader.Read(fromBuffer, toBuffer);
            fromTick = fromBuffer.Tick;
            toTick = toBuffer.Tick;
            return true;
        }

        from = default;
        to = default;
        fromTick = default;
        toTick = default;
        return false;
    }
}

Example Usage

To use the extension, simply derive from it, define the struct of networked state and declare the abstract property:

C#

public class SomeBehaviour : NetworkBehaviourWithState<SomeBehaviour.NetworkState> 
{
    [Networked] public override ref NetworkState State => ref MakeRef<NetworkState>();
    public struct NetworkState : INetworkStruct
    {
        public TickTimer SomeTimer;
        public int SomeInt;
        public Vector3 SomePoint;
    }
    
    public override void Render()
    {
        if ( TryGetStateChanges(out NetworkState old, out NetworkState current) )
        {
            // Something changed in the networked state - "old" and "current" has the before and after values.
        }
        
        if (TryGetStateSnapshots(out NetworkState from, out Tick fromTick, out NetworkState to, out Tick toTick, out float alpha))
        {
            // We're currently rendering the state between "from" and "to" at offset "alpha"
        }
    }
}
Back to top