このページは編集中です。更新が保留になっている可能性があります。

Fusion 103 - 予測

概要

Fusion 103では、予測と、サーバーオーソリテーティブネットワークゲームでクライアントへスナッピーなフィードバックを提供する方法について説明します。

このセクションの最後には、プロジェクトでプレイヤーが予測キネマティックボールをスポーンできるようになります。

C本トピックの詳細についてはこちらを参照してください。

トップに戻る

キネマティックオブジェクト

オブジェクトをスポーンできるようにするには、プレハブが必要です。

  1. Unity Editorで空のGameObjectを新規作成する
  2. 名称をBallとする
  3. 2に新規のNetworkTransformコンポーネントを追加する
  4. FusionからNetworkObjectコンポーネントが不足している旨の警告が表示されるので、Add Network Objectを押す。
  5. Interpolation Data SourcePredictedへ変更し、World Spaceと設定する
  6. BallにSphereの子を追加する
  7. 全方向へ0.2までサイズダウンする
  8. 子を、親オブジェクトのNetworkTransformコンポーネントのInterpolationTargetのドラッグする。これによりNetworkTransformが、メインの(ネットワークステートへスナップする)ネットワークオブジェクト自体からスムーズな内挿ビジュアル(子オブジェクト)を区別できるようになる
  9. 子Shpereからコライダーを削除する
  10. 代わりに親オブジェクトにSphereコライダーを新規作成し、半径を0.1にすることで子オブジェクトのビジュアル表現を完全にカバーできるようにする。
  11. ゲームオブジェクトにスクリプトを新規追加し、Ball.csを名前を付ける
  12. Ballオブジェクト全体をプロジェクトフォルダに不ドラッグし、プレハブを作成する
  13. シーンを保存してネットワークオブジェクトをベイクし、シーンからプレハブインスタンスを削除する

Ball Prefab
Ball Prefab

トップに戻る

予測動作

全てのピアで同時に、Ballのインスタンスが同様の挙動をとることが目的です。

ここでいう「同時」とは、「同じシミュレーションティックで」ということで、現実世界の時間と同様ではありません。以下の手順で実現します。

  1. サーバーが、特定の、等しく間隔の開いたティックでシミュレーションを実行し、各ティックでFixedUpdateNetwork()を呼び出します。ティックを次に進められるのは、常にサーバーのみです。Unityの通常のローカル物理シミュレーションのFixedUpdate()と全く同様です。各シミュレーションティックの後に、サーバーはネットワークステートの変更を前回のティックと比較して計算し、圧縮し、ブロードキャストします。
  2. これらのスナップショットを、定期的なインターバールでクライアントは受信しますが、いつもサーバーから一歩遅れています。スナップショットを受け取ったら、クライアントは内部のステートをそのスナップショットのティックに設定し直しますが、それからまたすぐに、独自のシミュレーションを実行して、受信したスナップショットとクライアントの最新ティックの間の全てのティックを再シミュレーションします。
  3. 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. クライアントの最新ティックは常にサーバーよりも進んでいます。

これは多くの意味をもちます。

  1. クライアントはフレームごとにFixedUpdateNetwork()を多数実行し、同じティックでも最新スナップショットを受信するので数回シミュレーションします。これは、FusionがFixedUpdateNetwork()を呼び出す前に適切なティックにリセットするので、ネットワークステートに有効です。ただし、ネットワークステート以外にはtrueではないので、FixedUpdateNetwork()でのローカルステートの使用に関してはよく注意してください。
  2. 各ピアは、あらゆるオブジェクトの予測した未来のステートを既知の前回の位置、速度、加速、その他の決定性プロパティに応じてシミュレーションすることができます。予測ができないものは、他のプレイヤーのインプットです。この場合は予測はうまくいかないことになります。
  3. ローカル入力は即時フィードバックのためにクライアントに即座に適用されますが、それらはオーソリテーティブではありません。入力のティックローカル適用が単なる予測であることを最終的に定義するのは、依然としてサーバによって生成されるスナップショットです。

このことを念頭に置いて、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つの手順を実行します。

  1. Input Structure(インプット構造)にデータを追加する
  2. UnityのInputからデータを集める
  3. プレイヤーの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フィールドにドラッグします。

ドキュメントのトップへ戻る