ネットコード
概要
Fusionのシミュレーションは、FixedUpdateNetwork()
と呼ばれる実行周期を持ちます。これは、UnityのFixedUpdate()
と根本的に似ています。FixedUpdate()
でのシミュレーションは、固定間隔の実行周期を持ち、経過した現実時間に合わせて実行されます。
FixedUpdateNetwork()
はFusionによって管理されます。FixedUpdateNetwork()
とFixedUpdate()
との大きな違いは、Fusionのホストモード(共有モードを除く)では、クライアント上でサーバーの更新を受信するたびに状態(ネットワークプロパティ)がリセットされ、リモートのサーバーティックからローカルのティックまでの再シミュレーションが行われることです。
また、Fusionのすべてのモードにおいて、すべてのFixedUpdateNetwork()
コールバックが実行された直後に、そのティックの状態がキャプチャされます。これによって、FixedUpdateNetwork()
が、あらゆるシミュレーションコードを記述する適切な場所になります。そのため、FixedUpdateNetwork()
と「シミュレーション」は本質的に同義とみなせます。
FixedUpdate()
は、FixedUpdateNetwork()
とは完全に別物なので、シミュレーションでの使用は通常避けてください。
ホストモードのネットコード
ホストモード(専用サーバー/ホスト/シングル)では、すべてのネットコードをFixedUpdateNetwork()
内でシミュレーションすることが非常に重要です。アセットストアからインポートしたコードや、自身が過去に記述したコードを再利用する場合、その実行周期は通常Update()
やFixedUpdate()
を使用しているため、細心の注意が必要です。
FixedUpdateNetwork()
は、UnityのFixedUpdate()
と非常に似ていて、固定間隔の実行周期で時間経過に合わせて進行します。大きな違いは、ホストモードのFixedUpdateNetwork()
では再シミュレーションが行われることです。
FixedUpdateNetwork()
外で記述したあらゆるシミュレーションコードは、再シミュレーションが行われないため、クライアントの同期が定期的にずれることになります。
FixedUpdateNetwork要件の例外
Update()
/FixedUpdate()
内で記述したシミュレーションコードが許容される場合もあります。
- オブジェクトに対して、一切のクライアントサイド予測を行わず(クライアント上では完全にリモートのタイムフレームで動作させて)、サーバーのみがオブジェクトの状態の変更を適用する場合
- クライアントで、オブジェクトの変更をスムーズに補間する必要がない場合
共有モードのネットコード
共有モードもホストモード(サーバー/ホスト/シングル)と同様に、固定周期のティックでシミュレーションが行われます。しかし以下のような理由から、開発者はFixedUpdateNetwork()
ではなくUpdate()
で(一般的なUnityと同じように)入力を適用できます。
- ユーザーの入力に対して、高速なレスポンスを返したい
- 既存アセットのコードが、
Update()
を使用している - 他のコンポーネント・システム・アセットなどが、
Update()
で変更が行われることを想定している
制御するオブジェクトが、ローカルRender
のタイムフレーム(2ティック間の線形補間)に置かれるため、オブジェクトのシミュレーションとレンダリングの両方が、ローカルプレイヤーの実際の時間で行われることになります。
ただし、Update()
で制御するオブジェクトの移動が、Fusionの固定周期のシミュレーションと完全に互換性があるわけではありません。Transform
の値は、Update()
の後ではなくFixedUpdateNetwork()
の後にキャプチャされるため、Update()
で行った変更は、(その後のUnityの更新で行われる)次のシミュレーションまでキャプチャされません。Update()
で行う任意のdeltaTime
ベースの移動やキャプチャ間のエイリアシングが原因で、移動がわずかに引っかかったりガタガタしたりする可能性があります。
共有モードのエイリアシング
共有モードでは、FixedUpdateNetwork()
内でネットコードのシミュレーションを行うことを厳密に要求されませんが、Update()
でのみシミュレーションを行った場合、潜在的にエイリアシングの誤差が生じます。以下の図は、その問題を示しています。ネットコードによる移動や状態の変更をUpdate()
で行っていても、FusionはFixedUpdateNetwork()
の後にその状態をキャプチャします。しかし、他のクライアント上では、値を変更した実際の時間は全く考慮されず、固定間隔の値として補間が行われるため、補間した結果が引っかかったりガタガタしたりする原因になります。
エイリアシングの対処戦略
エイリアシングが気になる場合、それに対処するオプションがいくつか存在します。
備考: エイリアシングが目立つかどうかは、シミュレーションされる値の性質や、関連するフレームレートやティックレートの値によります。
ティックレートとフレームレートが非常に高い場合、エイリアシングがプレイヤーに気づかれることはないでしょう。フレームレートがティックレートよりかなり高い場合も、ほとんど気になることはありません。
1) 何もしない
エイリアシングがユーザーに気づかれないほど十分に小さい場合、エイリアシングを一切無視して、Update()
でシミュレーションを行うことができます。
2) FixedUpdateNetwork()で移動し、状態権限者の値を補間する
共有モードをホストモードのように扱うオプションで、すべてのネットコードをシミュレーションに基づいてFixedUpdateNetwork()
内のみで実行します。そして、2つの直近のシミュレーション結果をRender()
内で線形補間します。欠点は、わずかに入力遅延が発生することです。
3) FixedUpdateNetwork()でもUpdate()でも移動する
もう一つのオプションは、FixedUpdateNetwork()
とUpdate()
とのハイブリッドでシミュレーションを行うことです。FixedUpdateNetwork()
では、オブジェクトを等間隔で正確なティックベースで移動し、その状態をキャプチャ/複製させることで、リモートのクライアント上でのスムーズな結果を保証します。さらにUpdate()
では、最新ティックからの経過時間を計算し、オブジェクトを正しい位置に移動させます。この場合、補間は不要です。
C#
using Fusion;
using UnityEngine;
/// <summary>
/// The objective of this basic controller code is to simulate
/// Object movement both in FixedUpdateNetwork() AND in Update().
/// It is important to move the Object in FixedUpdateNetwork()
/// to produce evenly spaced results, as these snapshots are used
/// to produce smooth interpolation on remote clients.
/// However, if it very common with Client Authority topologies
/// to apply inputs in Update(), as to apply player input
/// immediately to the rendered Object, rather than interpolating
/// between previous simulated states. So it becomes necessary
/// to simulate in both FUN() and Update() to satisfy both
/// Simulation and Interpolation requirements.
/// Simulation in Update() to produce smooth local movement,
// and Simulation in FUN() to capture even networked tick results.
/// </summary>
public class HybridSharedController : NetworkBehaviour, IBeforeUpdate
{
[SerializeField, Unit(Units.PerSecond)]
public float Speed = 5f;
float _lastSimulationTime;
Vector3 _currentInput;
public override void Spawned()
{
// Capture the starting time so our
// first simulation has a correct delta value.
_lastSimulationTime = Runner.SimulationTime;
}
public override void FixedUpdateNetwork()
{
// Only move the State Authority (there is no prediction in Shared Mode)
if (HasStateAuthority == false) return;
// Calculate the amount of time that has passed since the last
// FixedUpdateNetwork or Update - as this is our real delta
float timeSinceLastSim = Runner.SimulationTime - _lastSimulationTime;
_lastSimulationTime = Runner.SimulationTime;
transform.position += _currentInput * timeSinceLastSim;
}
// The BeforeUpdate() callback is the same as Update(),
// but it fires BEFORE all Fusion work is done.
// Update() would also work here, but input used in FUN()
// would always be from the previous Update().
void IBeforeUpdate.BeforeUpdate()
{
// Only move the State Authority (no prediction in Shared Mode)
if (HasStateAuthority == false) return;
// Collect input every Update(), and store it -
// as we will also use this input in FixedUpdateNetwork()
Vector3 input = default;
if (Input.GetKey(KeyCode.W)) { input += Vector3.forward * Speed; }
if (Input.GetKey(KeyCode.S)) { input += Vector3.back * Speed; }
if (Input.GetKey(KeyCode.A)) { input += Vector3.left * Speed; }
if (Input.GetKey(KeyCode.D)) { input += Vector3.right * Speed; }
_currentInput = input;
// Calculate the amount of time that has passed since last simulation
// (FixedUpdateNetwork or Update), as this is our real delta time
float realtime = Runner.LocalRenderTime + Runner.DeltaTime;
float timeSinceLastSim = realtime - _lastSimulationTime;
_lastSimulationTime = realtime;
transform.position += input * timeSinceLastSim;
}
}