This document is about: FUSION 2
SWITCH TO

Network Buffers

概要

Fusionは、StateAuthorityを持っていない各NetworkObjectについて、受信した状態のスナップショットの履歴を保持します。ローカルにシミュレートされた各オブジェクト(権限を持っているか予測されているかに関わらず)には、さらに現在と以前のスナップショットがあります。

NetworkBehaviour[Networked]プロパティは、これらのスナップショットのうち最新のものに直接アクセスすることができます(これが履歴にあるかローカルでシミュレートされた状態にあるかにかかわりません)。

任意の NetworkBehaviour の最新のスナップショット 2 つと、その間の相対的な時間オフセット(補間用)を取得するには、TryGetSnapshotBuffers() を使用します。

スナップショット内の個々のプロパティ値にアクセスするには、NetworkBehaviour ベースクラスは低レベルのデータアクセスと型付き PropertyReader の両方を提供します。

これに加えて、Network Behaviorは ChangeDetector クラスを使用してネットワーク状態の変化を監視できます。

NetworkBehaviourBuffer と PropertyReader

NetworkBehaviourBufferはFusionの内部データストレージを抽象化したもので、ネットワーク状態のスナップショットを低コストで効果的に読み取ることができます。

高度な使い方をする場合、ネットワークバッファは低レベルの ReinterpretState<T>(offset) メソッドを提供し、任意のオフセットにあるバッファデータを特定のデータ型にキャストすることができます。このメソッドは、パフォーマンスに非常に敏感な場合や、ネットワーク状態の解釈をカスタマイズする必要がある場合を除き、一般的には推奨されません。

その代わりに、Fusionでは PropertyReader クラスを用意しています。PropertyReaderは、C#の型と名前付きプロパティ、およびバッファ内のオフセットを関連付けます。プロパティリーダーはFusionによって内部的にキャッシュされるので、パフォーマンスが非常に重要なシナリオを除いて、ローカルキャッシュは必要ありません。

NetworkBehaviourBufferからプロパティを読み込むには、バッファまたはリーダーに対して Read() メソッドを呼び出します:

C#

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

2つのバッファ(通常、前のスナップショットと現在のスナップショット)から同じプロパティを読み込む一般的なケースでは、Fusionはショートカットを提供します。

C#

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

スナップショットと内挿

NetworkBehaviourBufferの最新の2つのスナップショットにアクセスするには、TryGetSnapshotBuffers()を呼び出します。これにより、2つのバッファと、その fromto のティック、内挿オフセット(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 は NetworkBehaviourBufferInterpolator 構造体を提供します。この構造体は、NetworkBehaviourが指定されると、スナップショットバッファとプロパティリーダーを取得し、指定されたプロパティの内挿値を返します。

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

Fusion では、ChangeDetector を使ってネットワークの状態の変化を追跡することができます。NetworkBehaviour のチェンジディテクタを取得するには、NetworkBehaviourGetChangeDetector(source, copy) を呼び出し、返されたインスタンスへの参照を保持します。

C#

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

Sourceパラメータは、タイムライン上のどこで "新しい値 "をサンプリングするかを決定します。これは、ローカルタイムフレーム(SimulationState)、またはティック(それぞれ SnapshotFromSnapshotTo)からのリモートタイムフレーム(SnapshotFromSnapshotTo)のいずれかになります。

注意すべき点は、SimulationStateはシミュレーションに含まれるネットワークオブジェクトに対してのみ存在するということです。デフォルトでは、ローカル・ピアに状態権限または入力権限があるオブジェクトを指しますが、シミュレーションが明示的に有効になっているオブジェクトにも存在します。

copyパラメータは、変更検出器の初期状態を構築時にコピーするかどうかを決定します(コピーしない場合、変更を検出する最初の呼び出しは「変更前」の状態を確立し、常に空の列挙体を返します)。

変更の元となる「古い値」は、ディテクタが最後に呼び出されたときの値です。したがって、それが FixedUpdateNetwork() での正確なティック境界であろうと、Render()Update() での中間レンダリングフレームであろうと、実行のどの時点でも変更を検出することができます。

作成されたそれぞれの ChangeDetector は、以前に作成されたディテクターとは独立して独自の状態を維持します。

ChangeDetector の使用

ChangeDetectorの主なエントリポイントは DetectChanges() メソッドで、このメソッドが最後に呼び出されてから、与えられた時間枠内で変更されたすべてのプロパティの列挙と、前回と現在のバッファを返します。

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;
        }
    }
}

ボイラープレートの回避

NetworkBehaviourBufferPropertyReaderは、パフォーマンスと完全性を考慮して設計されています。これは最小限のオーバーヘッドで非常に高い柔軟性を提供しますが、ビヘイビアが多くのプロパティを持ち、個々のプロパティの変更を気にしないような場合には、多くの定型的なコードになる可能性があります。

このようなシナリオでは、ネットワーク上のプロパティを INetworkStruct を実装した構造体に集めて、そのような構造体ごとに 1つのプロパティとして扱うと便利です。

NetworkBehaviourWithState

以下は、使いやすさと柔軟性を引き換えにしたベースクラスの例です。オブジェクトの状態全体を1つの構造体に格納し、その構造体内の何かが変更されたかどうかをチェックするメソッドを提供し、変更前と変更後の値も返すことで、変更検出を単一のメソッド呼び出しに減らすことができます。

同様に、スナップショットは、プロパティリーダーを明示的に扱うことなく、すべてのプロパティに一度にアクセスすることができます。

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"
        }
    }
}
Back to top