ネットワークバッファ
概要
Fusionは、状態権限を持たない各ネットワークオブジェクトについて、それらの状態を最新のサーバースナップショットから受信し、その履歴を保持します。つまり、ローカルでシミュレーションしている(予測する/しないにかかわらず)各オブジェクトには、現在と過去のスナップショットが存在するということです。
NetworkBehaviourのネットワークプロパティ([Networked])は、最新のスナップショット(予測する/しないにかかわらず)に直接アクセスできます。
TryGetSnapshotBuffers()を使用すると、NetworkBehaviourの直近2つのスナップショットと、それらの相対的な時間オフセット(補間で使用できる)を取得できます。
NetworkBehaviour基底クラスは、スナップショットの個別のプロパティにアクセスするための、低レベルのデータアクセスと、型指定のPropertyReaderを提供しています。
さらにNetworkBehaviourは、ChangeDetectorクラスを使用してネットワーク上の状態の変化を監視できます。
NetworkBehaviourBufferとPropertyReader
NetworkBehaviourBufferは、Fusion内部のデータストレージを抽象化し、スナップショットを低コストで効率的に読み取ることができます。
高度なユースケースとして、ネットワークバッファが提供している低レベルのReinterpretState<T>(offset)メソッドで、任意のオフセットのバッファデータを、特定のデータ型にキャストして取得できます。ただし、非常に高いパフォーマンスを要求される場合や、ネットワーク上の状態を独自に解釈する必要がある場合以外は、一般的には推奨されません。
そのかわりに、FusionではPropertyReaderクラスを提供しています。PropertyReaderは、プロパティ(C#の型と名前)と、バッファのオフセットを関連付けます。PropertyReaderはFusion内部でキャッシュされるため、非常にパフォーマンスが重要な場合を除いて、ローカルでキャッシュする必要はありません。
NetworkBehaviourBufferからプロパティを読み取るには、単にRead()メソッドを呼び出します。
C#
var reader = GetPropertyReader<T>(nameof(SomeProperty));
T t = reader.Read(buffer);
// or
t = buffer.Read(reader);
プロパティから2つのバッファ(現在とその前のスナップショット)を読み取る一般的なケースでは、以下のようなショートカットがあります。
C#
var reader = GetPropertyReader<T>(propertyname);
(previous,current) = reader.Read(previousBuffer, currentBuffer);
スナップショットと補間
TryGetSnapshotBuffers()を呼び出すと、2つの直近のスナップショットにアクセスできます。これによって、2つのバッファと、そのfromとtoのティック、補間係数(alpha)が返されます。
C#
if (TryGetSnapshotBuffers(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;
}
バッファはそのまま使用したり、補間係数で2つのバッファを補間してレンダリング時の正確な値として使用できます。
NetworkBehaviourBufferInterpolator
FusionのNetworkBehviourBufferInterpolator構造体によって、補間が簡単になります。この構造体にNetworkBehaviourを渡すと、スナップショットのバッファとPropertyReaderを取得し、指定した名前のプロパティの補間された値を返します。
C#
var interpolator = new NetworkBehaviourBufferInterpolator(this);
Vector3 v = interpolator.Vector3(nameof(SomeVector3Property));
NetworkBehaviourBufferInterpolatorは、数学的な定義を持つ型の補間のみに対応していますが、C#の拡張メソッドを使用して、独自の型にも簡単に対応させることができます。
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
ChangeDetectorによって、ネットワーク上の状態の変化を追跡することができます。NetworkBehaviourのChangeDetectorを取得するには、GetChangeDetector(source, copy)を呼び出して、その戻り値を変数に格納してください。
C#
private ChangeDetector _changeDetector;
public override void Spawned()
{
    _changeDetector = GetChangeDetector(ChangeDetector.Source.SimulationState);
}
sourceパラメーターで、どのタイムラインの「新しい値」をサンプルするかどうかを決定します。これは、ローカルのタイムフレーム(SimulationState)か、リモートのタイムフレーム(SnapshotFrom/SnapshotTo)のいずれかになります。
SimulationStateは、シミュレーションに含まれるネットワークオブジェクトにのみ存在することに注意してください。デフォルトでは、ローカルピアが状態権限または入力権限を持つオブジェクトと、シミュレーションを明示的に有効にしているオブジェクトを指します。
copyパラメーターは、ChangeDetectorの初期状態として生成時の状態をコピーするかどうかを決定します。(コピーしない場合、最初に変更が検知された時に、その状態が「変更前の値」となり、空の列挙子が返ります)
「変更前の値」は、単に前回に変更が検知された時の値です。そのため、実行中のいつでも(FixedUpdateNetwork()のティック間や、Render()/Update()の描画フレーム間で)変更を検知することができます。
各ChangeDetectorは、過去に作成されたChangeDetectorとは独立して状態を保持するため、1つのNetworkBehaviourで複数のChangeDetectorを作成することができます。
ChangeDetectorの使用
ChangeDetectorの主要なエントリーポイントは、DetectChanges()メソッドになります。指定のタイムフレームにおける、メソッドが最後に呼び出された後に変更されたすべてのプロパティの列挙子と、前回と現在のバッファを返します。
C#
foreach (var propertyname in _changeDetector.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;
        }
    }
}
前回の値が不要なら、以下のように簡略化できます。
C#
foreach (var change in _changeDetector.DetectChanges(this))
{
    switch (change)
    {
        case nameof(SomeProperty):
            var current = SomeProperty;
            ....
            break;
    }
}
ボイラープレートの回避
NetworkBehaviourBufferとPropertyReaderは、パフォーマンスと完全性を考慮して設計されています。最小のオーバーヘッドで非常に高い柔軟性を提供する一方、NetworkBehaviourが多くのプロパティを持ち、個別のプロパティの変更を気にしない場合には、大量のボイラープレートコードが生まれてしまう可能性があります。
このような場合には、INetworkStructを実装した構造体にネットワークプロパティをまとめて、その構造体を1つのプロパティとして扱うと便利です。
NetworkBehaviourWithState
以下は、柔軟性を犠牲にして使いやすくした基底クラスの例です。オブジェクトの状態全体を1つの構造体に格納し、構造体内の値の変更をチェックするメソッドを用意して、変更前と変更後の値を返すことで、変更検知処理を1つのメソッドに削減できます。
同様に、PropertyReaderを明示的に扱うことなく、スナップショットから一度にすべてのプロパティにアクセスすることができます。
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 (TryGetSnapshotBuffers(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;
    }
}
使用例
このクラスを使用するには、クラスを継承して、ネットワーク上の状態の構造体を定義し、抽象プロパティを宣言してください。
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"
        }
    }
}