This document is about: FUSION 2
SWITCH TO

プレイヤー入力

概要

Fusionは、プレイヤーの入力を毎ティック収集し、そのデータの履歴をバッファに保存し、そして自動的にデータをサーバーへ送信します。

このメカニズムによって、主にクライアントサイド予測が可能になります。あるティックの入力は、そのティックのシミュレーション(FixedUpdateNetwork())で使用されます。ここで、クライアント側(HasInputAuthority == true)では予測を、サーバー側ではピア間で一貫性のある結果を作成します。履歴バッファは、クライアントの再シミュレーションで使用されます。

入力構造体の定義

入力構造体には、以下の制約があります。

  • INetworkInputを継承しなければならない
  • プリミティブ型と構造体のみを含む
  • 入力構造体とそれに含まれる構造体がトップレベルにある(構造体はクラスの入れ子にできない)
  • 真偽値はboolのかわりにNetworkBoolを使用する(1ビットで適切にシリアライズするため)

Fusionは構造体の型を賢くマッピングするので、異なるモードや異なるゲームのパートで、異なる入力構造体を使用することも可能です。入力を取得する時、Fusionは正しい型の有効な入力のみを返します。

C#

public struct MyInput : INetworkInput {
    public Vector3 aimDirection;
}

ボタン

NetworkButtons型は、INetworkInputにボタン押下を格納する便利なラッパーです。

入力構造体にボタンを追加するには、

  1. ボタン用の列挙型を作成する( 重要: 列挙型は明示的に定義し、値は0から始めなければなりません)
  2. INetworkInputNetworkButtons型の変数を追加する

C#

enum MyButtons {
    Forward = 0,
    Backward = 1,
    Left = 2,
    Right = 3,
}

public struct MyInput : INetworkInput {
    public NetworkButtons buttons;
    public Vector3 aimDirection;
}

NetworkButtonsのAPIから、値を直接読み書きすることができます。

  • void Set(int button, bool state):ボタン用の列挙型の値と、押下状態(押下中ならtrue)を渡します
  • bool IsSet(int button):ボタン用の列挙型の値を渡し、その押下状態を返します

NetworkButtons型はステートレスで、メタデータ(過去の押下状態など)を格納しません。後述するNetworkButtonsのメソッドを使用するには、各プレイヤーの過去の押下状態を追跡しておく必要があります。これは、過去の押下状態を持つネットワークプロパティを作成すると簡単です。

C#

public class PlayerInputConsumerExample : NetworkBehaviour {
    [Networked] public NetworkButtons ButtonsPrevious { get; set; }

    // Full snippet in the GetInput() section further down.
}
スニペット全体は、後述するGetInputの説明にあります。

これで、現在の押下状態と過去の押下状態を比較して、ボタンを押した瞬間や離した瞬間が計算できるようになります。

  • NetworkButtons GetPressed(NetworkButtons previous):全てのボタンについて、押した瞬間かどうかを返します
  • NetworkButtons GetReleased(NetworkButtons previous):全てのボタンについて、離した瞬間かどうかを返します
  • (NetworkButtons, NetworkButtons) GetPressedOrReleased(NetworkButtons previous):ボタンを押した瞬間かどうかと、ボタンを離した瞬間かどうかのタプルを返します

重要: 値の代入にはInput.GetKey()のみを使用してください。Input.GetKeyDown()またはInput.GetKeyUp()は、Fusionのティックと同期していないため、入力が見逃されることがあります。

入力のポーリング

Fusionで入力を収集するために、ローカルクライアントをポーリングして、入力構造体にデータを追加します。NetworkRunnerは、常に一つの入力構造体のみを追跡します。予期しない挙動を避けるため、入力のポーリングは一つの場所でのみ実装することを強く推奨します。

FusionのNetworkRunnerは、INetworkRunnerCallbacks.OnInput()を呼び出して、入力をポーリングします。OnInput()内で、INetworkInputを実装した構造体にデータを追加できます。データを追加した構造体は、NetworkInputSet()を呼び出すことで、Fusion側に渡されます。

重要:

  1. 複数の場所で入力をポーリングすると、最後に渡された入力構造体で上書きされます
  2. 入力は、ローカルでのみポーリングされます(全てのモード)

SimulationBehaviour / NetworkBehaviour

SimulationBehaviourまたはNetworkBehaviourOnInput()を使用するには、INetworkRunnerCallbacksインターフェースを実装して、NetworkRunner.AddCallbacks()を実行してNetworkRunnerにコールバックを登録します。

C#

public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {

  public void OnEnable(){
    if(Runner != null){
       Runner.AddCallbacks( this );
    }
  }

  public void OnInput(NetworkRunner runner, NetworkInput input) {
    var myInput = new MyInput();

    myInput.Buttons.Set(MyButtons.Forward, Input.GetKey(KeyCode.W));
    myInput.Buttons.Set(MyButtons.Backward, Input.GetKey(KeyCode.S));
    myInput.Buttons.Set(MyButtons.Left, Input.GetKey(KeyCode.A));
    myInput.Buttons.Set(MyButtons.Right, Input.GetKey(KeyCode.D));
    myInput.Buttons.Set(MyButtons.Jump, Input.GetKey(KeyCode.Space));

    input.Set(myInput);
  }

  public void OnDisable(){
    if(Runner != null){
        Runner.RemoveCallbacks( this );
    }
  }
}

MonoBehaviour / ピュアC#

通常のC#スクリプトまたはMonoBehaviourで入力をポーリングするには、以下のステップに従います。

  1. INetworkRunnerCallbacksOnInput()を実装する
  2. AddCallbacks()を実行して、NetworkRunnerにスクリプトを登録する

C#


public class InputProvider : Monobehaviour, INetworkRunnerCallbacks {

  public void OnEnable(){
    var myNetworkRunner = FindObjectOfType<NetworkRunner>();
    myNetworkRunner.AddCallbacks( this );
  }

  public void OnInput(NetworkRunner runner, NetworkInput input) {
    // Same as in the snippet for SimulationBehaviour and NetworkBehaviour.
  }

  public void OnDisable(){
    var myNetworkRunner = FindObjectOfType<NetworkRunner>();
    myNetworkRunner.RemoveCallbacks( this );
  }
}

Unity New Input System

Unityの新しいInput Systemを使用するには、作成したInput Actionから入力を収集する必要があります。

Input Actionを作成して希望するボタンを定義した後、C#のコードでインスタンスを作成します。OnInput()で消費するために、入力をローカルでキャッシュすると、PlayerInputのイベントを使用することも可能です。

Input ManagerのかわりにInput Systemからボタンの押下状態を収集することが目的なので、Input Systemをセットアップする部分以外の実装は基本的に同じです。

C#

public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {

  // creating a instance of the Input Action created
  private PlayerActionMap _playerActionMap = new PlayerActionMap();

  public void OnEnable(){
    if(Runner != null){
        // enabling the input map
        _playerActionMap.Player.Enable();

        Runner.AddCallbacks(this);
    }
  }

  public void OnInput(NetworkRunner runner, NetworkInput input)
  {
    var myInput = new MyInput();
    var playerActions = _playerActionMap.Player;

    myInput.buttons.Set(MyButtons.Jump, playerActions.Jump.IsPressed());

    input.Set(myInput);
  }

  public void OnDisable(){
    if(Runner != null){
        // disabling the input map
        _playerActionMap.Player.Disable();

        Runner.RemoveCallbacks( this );
    }
  }
}

低ティックレートでの入力ポーリング

低ティックレートで入力を収集するためには、UnityのUpdateを使用する必要があります。そこで蓄積した入力を、構造体に記録し、後で消費できるようにします。

OnInput内のinput.Set()で、構造体から入力を取得し、適切にFusionに渡します。その後、次のティックの入力を蓄積するために、構造体をリセットします。

C#

public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {

  // Local variable to store the input polled.
  MyInput myInput = new MyInput();

  public void OnEnable() {
    if(Runner != null) {
        Runner.AddCallbacks( this );
    }
  }

  public void Update()
  {
    if (Input.GetMouseButtonDown(0)) {
      myInput.Buttons.Set(MyButtons.Attack, true);
    }

    if (Input.GetKeyDown(KeyCode.Space)) {
      myInput.Buttons.Set(MyButtons.Jump, true);
    }
  }

  public void OnInput(NetworkRunner runner, NetworkInput input) {

    input.Set(myInput);

    // Reset the input struct to start with a clean slate
    // when polling for the next tick
    myInput = default;
  }
}

UIからの入力ポーリング

UIからの入力ポーリングは、上記(低ティックレートでの入力ポーリング)と同じロジックです。UIから呼ばれるメソッドでNetworkButtonを設定し、OnInput内で入力を渡した後にリセットします。

入力の取得

入力はシミュレーションで取得し、ネットワーク上の状態を、現在の状態から入力を反映した新しい状態へ更新します。Fusionは、入力構造体をネットワーク経由で同期して、入力権限を持つクライアントと状態権限を持つホストのシミュレーション中で、入力を使用できるようにします。

入力のポーリングとは逆に、入力の取得は、必要に応じて様々な場所で行われます。

注意: プレイヤー入力は、入力権限か状態権限を持つクライアントでのみ取得可能です。ホストモード(サーバーモード)では、プレイヤー自身のクライアントと、ホスト(サーバー)です。共有モードでは、全く同一のクライアントになります。

あるクライアントの入力を、他のクライアントが取得することはできません。入力による変更を他のクライアントに複製するためには、その変更をネットワークプロパティに保存する必要があります。

GetInput()

入力構造体を取得するには、NetworkBehaviour(例えば、プレイヤーの移動を制御するコンポーネント)のFixedUpdateNetwork()GetInput(out T input)を呼び出して、オブジェクトに対する入力権限を持っているかどうかを問い合わせます。GetInput()は、OnInput()で以前に渡された入力構造体と同じものを返します。

GetInput()の呼び出しでfalseが返されるのは、以下の場合です。

  • クライアントが、状態権限も入力権限も持っていない
  • 要求した入力の型が、シミュレーションに存在しない

各モード固有の情報:

  • ホストモード(サーバーモード):あるティックの入力は、プレイヤーとホスト(サーバー)のシミュレーションでのみ有効です。入力は、プレイヤー間では共有されません
  • 共有モード:OnInput()GetInput()を使用するのはグッドプラクティスになりますが、サーバー主導ではないため、ローカルのプレイヤー入力は、ローカル(プレイヤー自身)のシミュレーションでのみアクセスできます。入力は、プレイヤー間では共有されません

C#

using Fusion;
using UnityEngine;

public class PlayerInputConsumerExample : NetworkBehaviour {

  [Networked] public NetworkButtons ButtonsPrevious { get; set; }

  public override void FixedUpdateNetwork() {

    if (GetInput<MyInput>(out var input) == false) return;

    // compute pressed/released state
    var pressed = input.Buttons.GetPressed(ButtonsPrevious);
    var released = input.Buttons.GetReleased(ButtonsPrevious);

    // store latest input as 'previous' state we had
    ButtonsPrevious = input.Buttons;

    // movement (check for down)
    var vector = default(Vector3);

    if (input.Buttons.IsSet(MyButtons.Forward)) { vector.z += 1; }
    if (input.Buttons.IsSet(MyButtons.Backward)) { vector.z -= 1; }

    if (input.Buttons.IsSet(MyButtons.Left)) { vector.x  -= 1; }
    if (input.Buttons.IsSet(MyButtons.Right)) { vector.x += 1; }

    DoMove(vector);

    // jump (check for pressed)
    if (pressed.IsSet(MyButtons.Jump)) {
      DoJump();
    }
  }

  void DoMove(Vector3 vector) {
    // dummy method with no logic in it
  }

  void DoJump() {
    // dummy method with no logic in it
  }
}

Runner.TryGetInputForPlayer()

NetworkRunner.TryGetInputForPlayer<T>(PlayerRef playerRef, out var input)によって、NetworkBehaviour以外で入力を取得することができます。INetworkInput型は、指定したプレイヤーの入力がFusionに渡されている必要があります。

注意: 制限はGetInput()と同じです。指定したプレイヤーの入力を取得できるのは、入力権限を持つクライアントとホスト(サーバー)のみです。

C#

var myNetworkRunner = FindObjectOfType<NetworkRunner>();

// Example for local player if script runs only on the client
if(myNetworkRunner.TryGetInputForPlayer<MyInput>(myNetworkRunner.LocalPlayer, out var input)){
    // do logic
}

注意点

完全なシミュレーションを保証するには、OnInput()では、入力を収集して入力構造体に追加することのみを行うのが重要です。入力に基づいて実行されるロジックは、GetInput()で完全に行うべきです。

弾を発射する例では、以下のように分けられます。

  • OnInput():プレイヤーの発射ボタンの値を保存する
  • GetInput():発射ボタンが押されているかどうかを調べ、押されているなら弾を発射する

複数のプレイヤーを持つピア

「カウチ」「画面分割」「ローカルマルチプレイヤー」と呼ばれるゲームでは、一つのピア(複数のコントローラーが接続されているゲーム機など)に、複数の人間のプレイヤーの入力が提供されます。そこでさらに、オンラインマルチプレイヤーゲームに参加することもあります。Fusionは、一つのピアの全てのプレイヤーを、単一のPlayerRefPlayerRefが識別するのは、ネットワーク上のピアであり、個々の人間のプレイヤーではありません)として扱い、プレイヤーの区別をしません。しかし開発者には、「プレイヤー」が何を指し、どのような入力を提供するかを決める余地はあります。

このユースケースの解決方法の一例として、各プレイヤーのINetworkStructを入れ子で含むINetworkInputを定義します。

C#

public struct PlayerInputs : INetworkStruct
{
  // All player specific inputs go here
  public Vector2 dir;
}

public struct CombinedPlayerInputs : INetworkInput
{
  // For this example we assume 4 players max on one peer
  public PlayerInputs PlayerA;
  public PlayerInputs PlayerB;
  public PlayerInputs PlayerC;
  public PlayerInputs PlayerD;

  // Example indexer for easier access to nested player structs
  public PlayerInputs this[int i]
  {
    get {
      switch (i) {
        case 0:  return PlayerA;
        case 1:  return PlayerB;
        case 2:  return PlayerC;
        case 3:  return PlayerD;
        default: return default;
      }
    }

    set {
      switch (i) {
        case 0:  PlayerA = value; return;
        case 1:  PlayerB = value; return;
        case 2:  PlayerC = value; return;
        case 3:  PlayerD = value; return;
        default: return;
      }
    }
  }
}

複数のプレイヤーの入力の収集:

C#

public class CouchCoopInput : MonoBehaviour, INetworkRunnerCallbacks
{
  public void OnInput(NetworkRunner runner, NetworkInput input)
  {
    // For this example each player (4 total) has one Joystick.
    var myInput = new CombinedPlayerInputs();
    myInput[0] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy1_X"), Input.GetAxis("Joy1_Y")) };
    myInput[1] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy2_X"), Input.GetAxis("Joy2_Y")) };
    myInput[2] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy3_X"), Input.GetAxis("Joy3_Y")) };
    myInput[3] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy4_X"), Input.GetAxis("Joy4_Y")) };
    input.Set(myInput);
  }

  // (removed unused INetworkRunnerCallbacks)
}

シミュレーションでの入力の取得:

C#

public class CouchCoopController : NetworkBehaviour
{
  // Player index 0-3, indicating which of the 4 players
  // on the associated peer controls this object.
  private int _playerIndex;

  public override void FixedUpdateNetwork()
  {
    if (GetInput<CombinedPlayerInputs>(out var input))
    {
      var dir = input[_playerIndex].dir;
      // Convert joystick direction into player heading
      float heading = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
      transform.rotation = Quaternion.Euler(0f, heading - 90, 0f);
    }
  }
}
Back to top