Network Simulation Loop
はじめに
Fusionでは、ネットワークモードに関わらず、離散的なティックベースのシミュレーションを一貫したタイムステップで実行します。これを処理する全体的なプロセスを「Networked Simulation Loop」と呼びます。すべてのロジックは、NetworkBehaviour
またはSimulationBehaviour
のサブクラスに記述されており、通常のUnity GameObjectがこのシミュレーションループの中心となっています。
Fusionはワールドの状態のスナップショットを前進させるだけでなく、ローカルプレイヤーの入力や物理ベースのオブジェクトに基づいて将来の状態を予測することもできます(物理のクライアントサイド予測をオンにするかどうかは任意です)。
現在の既知のデータやカスタムコードに基づいて、任意のオブジェクトのクライアントサイド予測ロジックを書くことができます(例:弾道の外挿)。
このドキュメントでは、Fusionのシミュレーションループの仕組みに関する最も重要な概念を詳しく説明します。
ティック
ネットワークに接続されたマシンの間には、自然に発生するタイミングのずれがあります。Fusionでは、シミュレーションを直接時間で動かすのではなく、ティック と呼ばれる離散的な抽象時間単位を使って処理します。ティックは、特定のクライアントやホストで経過する実際の現実世界の時間とは切り離されています。ハードウェアクロックの代わりにティックを使用することで、ネットワークセッション内のすべてのクライアントが「時間」の概念について共通の参照フレームを共有することができます。これは、ネットワークに接続された複数のマシンで、未来や過去の出来事を正確に推論するために重要なことです。
各ティックの間のタイムステップは、NetworkProjectConfig
のSimulation > Tick Rate
で定義されます。タイクレートはヘルツ(Hz)で定義されるので、タイムステップが60の場合、1/60秒の差分に相当します。つまり、前回のティックと今回のティックの間に実際にどれだけの時間が経過したかに関わらず、システムは1/60秒のタイムステップを使ってゲームの状態を進行させることになります。コード上では、タイムステップは、NetworkRunner.DeltaTime
プロパティでアクセスできます。
重要: シミュレーションループで使用されるティックレートは、レンダリングレート(Frame Per Seconds)とは異なります。
各ティックには、その時点での世界の「真実」を明示的に示す、状態スナップショットがあります。次のティックに向けて新しいスナップショットを生成するために、FusionはすべてのSimulationBehaviour
とNetworkBehaviour
でFixedUpdateNetwork()
を呼び出し、担当するゲームオブジェクトの全体的な状態を一部更新します。Fusionではこれを、GameObject
に対してState Authority
を持つことと呼んでいます。
どのオブジェクトに対しても、State Authority
を持つマシンは1台だけです。
- クライアント/サーバー型の設定では、これは常にサーバーです。
- 各クライアントが自分のオブジェクトを所有するクライアントオーソリティの設定では、各クライアントは通常、自分が作成したオブジェクトに対する権限を保持します。
入力処理
Fusionでは、入力を使ってクライアント側の予測を行います。クライアントは、ローカルの(古い)情報とローカルプレイヤーの入力に基づいて、次のサーバーの状態を予測します。実際の状態を受信すると、クライアントはこの新しい事実を基にして、新しい未来の状態を再シミュレーションします。
予測を可能にするために、入力処理は2つのステップに分かれています。
- 入力ポーリング:Fusionは、特定のオブジェクトに対する入力権限を持つクライアントのローカルハードウェアから入力を収集します。
- 入力の消費:入力はローカルシミュレーションによって適用され、権威あるシミュレーションに含まれるようにサーバーと共有されます。
入力の定義、ポーリング、消費についての詳細は、マニュアルのNetwork Input
のページをご覧ください。
予測
予測とは、クライアントが現在のローカル情報に基づいて、将来のサーバーの状態を「予測」することです。サーバーから受信した真のゲーム状態のスナップショットに基づくローカル情報は、ネットワーク機器間の固有のレイテンシーのため、常に古いものとなります。クライアントは予測を利用して、ローカルのゲーム状態をサーバーのゲーム状態に追従させようとします。これは、ネットワークの待ち時間がプレイヤーの体験に与える影響を軽減するために必要です。ネットワークレイテンシーは、サーバーからクライアントへの往復の時間と常に等しくなります。これは、離散的なティック(刻み)で考えることができます。
以下のようなシナリオを想像してみてください。
- 現在のサーバーのティック:100
- ティックレート 1/60秒
- クライアント1の待ち時間は4ティック(サーバからクライアントへの2ティック+クライアントからサーバへの2ティック)に等しい
- クライアント2のレイテンシーは6ティック(サーバーからクライアントへの3ティック+クライアントからサーバーへの3ティック)に等しい
クライアントはそれぞれのレイテンシーを認識していますが、クライアントがそれに追いつくために何ティックを予測する必要があるかを通知するのはサーバーです。予測することで、クライアントはサーバーが必要とする時間に合わせて入力を送信することができます。クライアントが入力権限を持っているオブジェクトは、ローカルの入力(プレイヤーキャラクターなど)を使って予測されるため、即座に反応しているように見えます。オブジェクトの状態を明示的にコントロールすることはできませんが、状態を動かす入力をコントロールすることができるため、入力権限と呼ばれています。
例えば、クライアント1がプレイヤーキャラクターを動かすとき、サーバーから受け取った最後の有効なティックに基づいて、将来の状態を予測します。この予測に基づいて、クライアント1は、サーバーの2ティック先の未来(すなわち102ティック目)に何をする予定かをサーバーに伝えます。これにより、入力がサーバーに届き、102ティック目の有効な状態のシミュレーション中に消費されるのに十分な時間が確保されます。クライアント1のプレイヤーの動きを妨げるものがない限り、クライアント1の予測されたTick102とサーバーの検証されたTick102は、同じ状態になります。
クライアント1は、遅延を補うために将来の十分なティックを予測するように、サーバーは、常にサーバーの状態の少なくとも2ティック先を予測するように指示します。一方、クライアント2は、遅延が大きいため、追いつくためには3ティック先まで予測する必要があります。
図からわかるように、クライアント1とクライアント2は、サーバーに対する相対的なレイテンシーのために、まったく同じ状態ではありません。クライアントは、他のクライアントからの情報がまだ届いていないため、自分たちの「真実」が誤っているという事実を認識しています。ここで、レプリケーションとリコンシリエーションの出番となります。
レプリケーション
Fusionはコンパクトなメモリバッファで状態の進行を管理し、シミュレートします。Fusionはサーバーから状態のスナップショットを受け取ると、そのスナップショットをメモリバッファに展開します。バッファに保存されている現在の状態は、予測された状態に基づいているので、それをスナップショットの状態に置き換えると、クライアントはサーバーから受け取ったスナップショットに関連したティックに事実上スナップバックすることになります。Fusionは、受信した状態とクライアントのローカルな予測状態との間の各ティックごとに、シミュレーションループを1回実行します。これにより、スナップショットを受信する前の状態に戻り、より正確な予測状態になります。
リコンシリエーション
クライアントが、より最近の、つまりより正確な世界の状態を示す新しいスナップショットを受け取ると、クライアントは自分のローカルの状態とスナップショットの状態を調整します。この調整は、クライアントが受信した最後のサーバーのスナップショットを複製し、サーバーのスナップショットに関連付けられたティックから現在のティックまでのローカルゲームの状態を再シミュレーションすることによって行われます。
最終的には、サーバーは常にしっかりと調整された状態にあり、クライアントは予期せぬ出来事が起こったときに、徐々に自己調整を行うようになります。この手順はFusionでは_Re-simulation_ と呼ばれ、クライアントの状態とサーバーの状態を調整するために、検証済みの新しい状態に基づいて古い入力の影響を再シミュレーションします。
これは、クライアント1と2がサーバー上で衝突した場合に特に重要です。サーバーは、任意のティックにおけるオブジェクトの最終的な状態を独占的に決定するため、位置はサーバーによって解決されます。これが、サーバーが「State Authority」を持っていると言われる理由です。
例
サーバーは、古いデータの定常的な流れをクライアントに送ります。検証済みのティック100をクライアント1が受信すると、クライアント1はティック100の検証済みの状態をメモリバッファに複製し、それまで予測していたティック100を上書きします。
- 予期せぬことが起きていなければ、ローカルで予測されたティック100はすでに正しい状態になっており、クライアント1とサーバーは完全に同期していることになりますが、クライアント1がサーバーよりも2ティック進んでいることになります。
- 検証済みのティックがクライアント1の予測と一致しなかった場合は、検証済み ティック 100が修正された状態として使用されます。
どちらのシナリオでも、クライアントは100、101、102の順に入力を再適用して、新しいティック103に到達します。
クライアント1の視点で時間を進めると、修正されたティック101を受信し、102と103の入力を適用し、修正された状態に到達するのはティック104になります。
クライアント2は、シミュレーションループの中で全く同じ手順を踏みます。唯一の違いは、サーバーが検証したティック1を受け取るのが、より高いレイテンシーのために若干遅れることですが、これはさらに先を予測することで補います。
FixedUpdateNetwork()
現在のティックステートを次のティックステートに進めるために、Fusionはシーン内のすべてのSimulationBehaviour
とNetworkBehaviour
コンポーネントに対してFixedUpdateNetwork()
を呼び出します。FixedUpdateNetwork
はレプリケーションと予測の両方のプロセスで呼び出されるので、ローカルの状態はネットワークの状態から_派生したものだけでなければなりません。FixedUpdateNetwork
からローカルの状態にプログレッシブデルタアップデートを適用すると、多くの変更が適用されることになります。
スナップショット内挿
通常、各クライアントは1つまたは少数のGameObject
に対してのみ入力権限を持ち、残りのGameObject
はスナップショットの更新によってその状態をクライアントに伝えます。予測された状態とは異なり、スナップショットは、サーバーから送信された時間と、クライアントが受信して複製する時間との間に若干の遅延があるため、常にサーバーより遅れています。
Fusionでは、このような リモートで制御されるGameObjects
を__プロキシ__ と呼んでいます。
プロキシの現在の状態(ビジュアル)にスナップショットをそのまま適用すると、クライアントのレンダリング速度ではなく、シミュレーションのティックレートでアニメーションやアップデートが行われているように見えます。これは、いくつかの理由から避けなければなりません。
- 帯域を節約するために シミュレーションは、レンダリングよりもはるかに低い周波数で実行することが望ましい場合があります。例えば、ネットワークシミュレーションの周波数が30Hz、レンダリングの周波数が120Hzといった具合です。
- ネットワークパケットは一定の速度では届きません。ランダム性の高いイベントにゲームのレンダリング周波数を合わせると、動きがぎこちなくなります。
スムーズな状態遷移を実現しながら、ネットワークのパケット周波数とレンダリングのリフレッシュレートを切り離すために、Fusionは2つのスナップショット間で「レンダリング状態」を補間します。理想的には、最新の2つのスナップショットに基づいて補間を行います。しかし、スナップショットは一定の速度で到着するとは限りません。そのため、わずかなマージン(バッファ)でこれを回避しない限り、一定速度の補間が一定でないネットワークスナップショットに追いついてしまう危険性があります。
Fusionの強みのひとつは、現在のネットワークの状態に応じて、必要なオフセットとバッファリングを適応させる内挿アルゴリズムです。これにより、スムーズな映像を提供しながら、クライアントのレイテンシを抑えることができます。
NetworkTransform
のようなFusionのコアコンポーネントは、内挿を使用してそのビジュアルコンポーネントを変換していますが、Fusionの組み込み済みインターポレータにアクセスして使用することも可能です。カスタムネットワークプロパティでも同様です。カスタム(ネットワーク)プロパティ用にインターポレータを取得するには、GetInterpolator<T>(nameof(MyProperty))
を呼び出します。するとタイプT
のMyProperty
用Interpolator
が返されます。現在のレンダリング時間で内挿された値はInterpolator.Value
にあります。
稀なケースですが、GetInterpolator
のタイプレスなオーバーロードを使用する必要がある場合もあります。この代替メソッドは関連ティックへのプロパティのメモリバッファへ直接のアクセスを提供します。このデータの適切なタイプへの変換方法と内挿方法を理解している必要があります。
最後に、GetInterpolationData
を呼び出すだけでコア内挿へのアクセスを行うことができますが、これを行うとNetworkBehaviour
全体に生ポインタが提供されインデックス化とでコード化の両方がアプリケーションの手に渡ります。