ネットワークバッファ
概要
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"
}
}
}