3 - 予測
概要
Fusion 103では、予測と、サーバーオーソリテーティブネットワークゲームでクライアントへスナッピーなフィードバックを提供する方法について説明します。
このセクションの最後には、プロジェクトでプレイヤーが予測キネマティックボールをスポーンできるようになります。
キネマティックオブジェクト
オブジェクトをスポーンできるようにするには、プレハブが必要です。
- Unity Editorで空のGameObjectを新規作成する
- 名称を
Ball
とする - 2に新規の
NetworkTransform
コンポーネントを追加する - Fusionから
NetworkObject
コンポーネントが不足している旨の警告が表示されるので、Add Network Object
を押す。 Interpolation Data Source
をPredicted
へ変更し、World Space
と設定するBall
にSphereの子を追加する- 全方向へ0.2までサイズダウンする
- 子を、親オブジェクトの
NetworkTransform
コンポーネントのInterpolationTarget
のドラッグする。これによりNetworkTransform
が、メインの(ネットワークステートへスナップする)ネットワークオブジェクト自体からスムーズな内挿ビジュアル(子オブジェクト)を区別できるようになる - 子Shpereからコライダーを削除する
- 代わりに親オブジェクトにSphereコライダーを新規作成し、半径を0.1にすることで子オブジェクトのビジュアル表現を完全にカバーできるようにする。
- ゲームオブジェクトにスクリプトを新規追加し、
Ball.cs
を名前を付ける Ball
オブジェクト全体をプロジェクトフォルダに不ドラッグし、プレハブを作成する- シーンを保存してネットワークオブジェクトをベイクし、シーンからプレハブインスタンスを削除する

予測動作
全てのピアで同時に、Ball
のインスタンスが同様の挙動をとることが目的です。
ここでいう「同時」とは、「同じシミュレーションティックで」ということで、現実世界の時間と同様ではありません。以下の手順で実現します。
- サーバーが、特定の、等しく間隔の開いたティックでシミュレーションを実行し、各ティックで
FixedUpdateNetwork()
を呼び出します。ティックを次に進められるのは、常にサーバーのみです。Unityの通常のローカル物理シミュレーションのFixedUpdate()
と全く同様です。各シミュレーションティックの後に、サーバーはネットワークステートの変更を前回のティックと比較して計算し、圧縮し、ブロードキャストします。 - これらのスナップショットを、定期的なインターバールでクライアントは受信しますが、いつもサーバーから一歩遅れています。スナップショットを受け取ったら、クライアントは内部のステートをそのスナップショットのティックに設定し直しますが、それからまたすぐに、独自のシミュレーションを実行して、受信したスナップショットとクライアントの最新ティックの間の全てのティックを再シミュレーションします。
- The clients current tick is always ahead of the server by a wide enough margin, that the input it collects from the user can be sent to the server before the server reaches the given tick, and needs the input to run its simulation. クライアントの最新ティックは常にサーバーよりも進んでいます。
これは多くの意味をもちます。
- クライアントはフレームごとに
FixedUpdateNetwork()
を多数実行し、同じティックでも最新スナップショットを受信するので数回シミュレーションします。これは、FusionがFixedUpdateNetwork()
を呼び出す前に適切なティックにリセットするので、ネットワークステートに有効です。ただし、ネットワークステート以外にはtrueではないので、FixedUpdateNetwork()
でのローカルステートの使用に関してはよく注意してください。 - 各ピアは、あらゆるオブジェクトの予測した未来のステートを既知の前回の位置、速度、加速、その他の決定性プロパティに応じてシミュレーションすることができます。予測ができないものは、他のプレイヤーのインプットです。この場合は予測はうまくいかないことになります。
- ローカル入力は即時フィードバックのためにクライアントに即座に適用されますが、それらはオーソリテーティブではありません。入力のティックローカル適用が単なる予測であることを最終的に定義するのは、依然としてサーバによって生成されるスナップショットです。
このことを念頭に置いて、Ballスクリプトを開き、ベースクラスをNetworkBehaviour
に変更してFusionシミュレーションループに含め、事前生成されたボイラープレートコードをFusionFixedUpdateNetwork()
のオーバーライドに置き換えます。
この単純な例では、「Ball」は一定の速度で前進方向に5秒間移動した後、自らデスポーンします。次のように、オブジェクトのトランスフォームに単純な直線の動きを追加してみましょう。
using Fusion;
public class Ball : NetworkBehaviour
{
public override void FixedUpdateNetwork()
{
transform.position += 5 * transform.forward * Runner.DeltaTime;
}
}
これは、時間ステップがTime.deltaTime
ではなく、ティック間の時間に対応するRunner.DeltaTime
であることを除いて、通常のネットワーク化されていないUnityオブジェクトを移動するために使用されるコードとほぼ同じです。Unityのtransform
のような一見ローカルなプロパティで、これがネットワーク全体で機能する秘訣は、もちろん前に追加されたNetworkTransform
コンポーネントです。NetworkTransform
は、トランスフォームプロパティがネットワーク状態の一部であることを保証する便利な方法です。
コードは、設定された時間が経過した後もオブジェクトをデスポーンする必要があるため、オブジェクトが無限に飛び、最終的にループしてプレーヤーの首に当たることはありません。Fusionは、TickTimer
という適切な名前のタイマー用の便利なヘルパー型を提供します。現在の残り時間を保存する代わりに、終了時間をティック単位で保存します。これは、タイマーが作成されるときに、すべてのティックで同期させる必要はなく、1回だけ同期させる必要があることを意味します。
ゲームのネットワーク状態にTickTimerを追加するには、BallにTickTimer
型のlife
という名前のプロパティを追加し、getterとsetterに空のスタブを提供し、[Networked]
属性でマークします。
[Networked] private TickTimer life { get; set; }
オブジェクトの生成前にタイマーを設定してください。Spawn()
はローカルインスタンスが作成された後にのみ呼び出されるので、ネットワーク状態の初期化のために使用するべきではありません。
代わりに、プレーヤーから呼び出すことができるInit()
メソッドを作成し、これを使用してlifeプロパティを5秒先に設定します。これは、TickTimer
自体の静的ヘルパーメソッドCreateFromSeconds()
を使用して行うのが最善です。
public void Init()
{
life = TickTimer.CreateFromSeconds(Runner, 5.0f);
}
最後に、FixedUpdateNetwork()
を更新してタイマーが切れたかどうか確認します。
切れていたら、ボールのデスポーンを行います。:
if(life.Expired(Runner))
Runner.Despawn(Object);
全体的に、Ball
クラスは以下の様な見た目になります。:
using Fusion;
public class Ball : NetworkBehaviour
{
[Networked] private TickTimer life { get; set; }
public void Init()
{
life = TickTimer.CreateFromSeconds(Runner, 5.0f);
}
public override void FixedUpdateNetwork()
{
if(life.Expired(Runner))
Runner.Despawn(Object);
else
transform.position += 5 * transform.forward * Runner.DeltaTime;
}
}
プレハブのスポーン
プレハブのスポーンはプレイヤーアバターのスポーンと同様の仕組みです。ただし、プレイヤーがスポーン去れる場所はネットワークイベント(プレイヤーのゲームセッション参加)によって引き起こされますが、ボールはユーザーのインプットに基づいてスポーンされます。
これを実現するには、インプットデータ構造は追加データで補強される必要があります。動作を同様のパターンに従い、3つの手順を実行します。
- Input Structure(インプット構造)にデータを追加する
- UnityのInputからデータを集める
- プレイヤーの
FixedUpdateNetwork()
実装のInput(インプット)を適用する
NetworkInputData
を開いて、buttons(ボタン)
と呼ばれるバイトフィールドを新しく追加し、1つめのマウスボタンにconstを定義します。
using Fusion;
using UnityEngine;
public struct NetworkInputData : INetworkInput
{
public const byte MOUSEBUTTON1 = 0x01;
public byte buttons;
public Vector3 direction;
}
BasicSpawner
を開いて、OnInput()
メソッドに移動し、プライマリマウスボタンのチェックを追加して、これがdownの場合、buttons
フィールドの最初のビットを設定します。素早いタップを見逃すことのないように、マウスボタンはUpdate()でサンプル化され、インプット構造で記録されたら、リセットされます。
private bool _mouseButton0;
private void Update()
{
_mouseButton0 = _mouseButton0 | Input.GetMouseButton(0);
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var data = new NetworkInputData();
if (Input.GetKey(KeyCode.W))
data.direction += Vector3.forward;
if (Input.GetKey(KeyCode.S))
data.direction += Vector3.back;
if (Input.GetKey(KeyCode.A))
data.direction += Vector3.left;
if (Input.GetKey(KeyCode.D))
data.direction += Vector3.right;
if (_mouseButton0)
data.buttons |= NetworkInputData.MOUSEBUTTON1;
_mouseButton0 = false;
input.Set(data);
}
Player
クラスを開いて、GetInput()
の中で、ボタンビットを取得し、最初のビットの準備が整ったらプレハブをスポーンします。プレハブは、Unityインスペクタによってアサインされる通常のUnity [SerializeField]
メンバーで提供されることがあります。別の方向でもスポーンできるようにするには、メンバー変数を追加して最新の動作方向を保存し、これをBallの前方向として使用します。
[SerializeField] private Ball _prefabBall;
private Vector3 _forward;
...
if (GetInput(out NetworkInputData data))
{
...
if (data.direction.sqrMagnitude > 0)
_forward = data.direction;
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
Runner.Spawn(_prefabBall,
transform.position+_forward, Quaternion.LookRotation(_forward),
Object.InputAuthority);
}
...
}
...
スポーンの頻度を制限するには、呼び出しをラップして各スポーンの間に期限切れになるネットワークタイマーでスポーンします。タイマーのリセットは、ボタンが押されたことが検知された場合のみにしてください。
[Networked] private TickTimer delay { get; set; }
...
if (delay.ExpiredOrNotRunning(Runner))
{
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
Runner.Spawn(_prefabBall,
transform.position+_forward, Quaternion.LookRotation(_forward),
Object.InputAuthority);
...
Ballは、同期される前にもう一度初期化する必要があるため、Spawn
への実際の呼び出しを修正する必要があります。具体的には、前に追加しておいたInit()
メソッド呼び出して、ティックタイマーが適切に設定されるようにします。
この目的のために、Fusionはコールバックが呼び出されて、プレハブのインスタンス化の後に呼び出すSpawn()
を提供することを許可しますが、それは同期される前です。
まとめると、クラスの見え方は次のようになります。
using Fusion;
using UnityEngine;
public class Player : NetworkBehaviour
{
[SerializeField] private Ball _prefabBall;
[Networked] private TickTimer delay { get; set; }
private NetworkCharacterControllerPrototype _cc;
private Vector3 _forward;
private void Awake()
{
_cc = GetComponent<NetworkCharacterControllerPrototype>();
_forward = transform.forward;
}
public override void FixedUpdateNetwork()
{
if (GetInput(out NetworkInputData data))
{
data.direction.Normalize();
_cc.Move(5*data.direction*Runner.DeltaTime);
if (data.direction.sqrMagnitude > 0)
_forward = data.direction;
if (delay.ExpiredOrNotRunning(Runner))
{
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
Runner.Spawn(_prefabBall,
transform.position+_forward, Quaternion.LookRotation(_forward),
Object.InputAuthority, (runner, o) =>
{
// Initialize the Ball before synchronizing it
o.GetComponent<Ball>().Init();
});
}
}
}
}
}
テスト前の最後の手順は、プレハブをPlayer
プレハブの_prefabBall
フィールドにアサインすることです。プロジェクトでPlayerPrefab
を選択肢て、Ball
プレハブを Prefab Ball
フィールドにドラッグします。