【Unity】InputSystemでシーンを跨いだバインディングを実現する（ローカルマルチ）

# 注意事項
この記事では、InputSystemについて導入および基礎的な理解が進んでいる前提で話が進みますので、ご了承ください。

基礎的な理解がまだできていないという方は、こちらの前提記事がおすすめです。

https://nekojara.city/unity-input-system-intro

https://nekojara.city/unity-input-system-local-multiplayer

# 動作環境
Unity 2022.3.10f1

# はじめに / 今回実装するもの
InputSystemを用いたローカルマルチの実装では、とあるプレイヤーオブジェクトに対して、一意のデバイスを紐づけることで実現します。

しかし、今回例に挙げる **「キャラクター選択画面」**（アウトゲームと呼ぶ）などを実装しようとすると、どうしてもアウトゲームとインゲームでシーンを分けなければいけなくなります。（無理やりひとつのシーンで実装することもできますが、管理が煩雑になりすぎます）

なので、**シーンを跨いでプレイヤーにバインディングをする**ということをやらなければなりません。

以上を実装する方法としては
**①キャラクター選択画面で「一意のデバイスが紐づいたプレイヤー」を生成**
②生成した各プレイヤーの入力により、キャラクターを選択する
**③インゲームに「選択したキャラクターとデバイスの情報」を渡して再生成**
となります。

**①**と**③**に関しては紹介しますが、②についてはビューの領域なので割愛します。

&#13;&#10;
【イメージ図】
```plantuml
partition "**アウトゲーム**" {
:接続するデバイス情報を取得;
:デバイス情報を紐づけたプレイヤーを生成;
note right
アウトゲームを動かすためのInputや
データのみを持っているプレイヤー
end note
split
:<color:red>Player1:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:red>コントローラ①
使用キャラ：<color:red>キャラクター①;
split again
:<color:blue>Player2:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:blue>キーボード①
使用キャラ：<color:blue>キャラクター②;
endsplit
:プレイヤーのデータをインゲームに送信;
}
partition "**インゲーム**" {
:インゲームがプレイヤーのデータをもとにキャラクターを生成;
note right
こっちがゲーム本編のプレイヤー
end note
}
```

&#13;&#10;
今回のサンプルを貼っておきます。

<iframe width="560" height="315" src="https://www.youtube.com/embed/S5gUXw-P_ew?si=LrDO0PyXZYvDIPUQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# ローカルマルチの実装方法
ローカルマルチ自体については、InputSystemの`PlayerInputManager`というコンポーネントを用いることで比較的簡単に実装することが可能です。

`PlayerInputManager`は、それぞれのプレイヤーを`PlayerInput`コンポーネントとして管理してくれるコンポーネントです。
`PlayerInput`コンポーネントがアタッチされたオブジェクトがシーン上に生成されると、**自動的に入室扱い**となるため、ひとまずコイツをシーン上に配置するだけで複数プレイヤーを制御することができます。

![スクリーンショット 2024-03-13 202210.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3744072/c000e118-c074-3bb7-c9fe-3943613739ae.png)

# 前提：Player Input Manager の設定
+ **Notification Behavior**
プレイヤー入室時等のイベントを、どの形式で通知するか設定する項目です。
詳細は後述します。

&#13;&#10;

+ **Join Behavior**
実行中にプレイヤーを生成する方法を設定する項目です。

  + `JoinPlayersWhenButtonIsPressed`
初期設定ではこの設定になっており、これは接続されているデバイスの**いずれかのキー（ボタン）が押されたとき**、`PlayerPrefab`に設定しているGameObjectがInstantiateされます。（すでにプレイヤーに割り当てられているデバイスからの入力は無視します。）
理由は後述しますが、非推奨です。

    &#13;&#10;
  + `JoinPlayersWhenJoinActionIsTriggered`
この設定では、**任意の`InputAction`が入力されたとき**、`PlayerPrefab`のGameObjectがInstantiateされます。上の設定より幾分か使いやすいですが、同様にこちらも非推奨です。

    &#13;&#10;
  + `JoinPlayersManually`
  この設定は、スクリプトから**手動**でプレイヤーをInstantiateさせることができます。

  多くの場合 **`JoinPlayersManually`** を選択し、プレイヤーを入室させることになります。
  
  :::note warn
  `JoinPlayersWhenButtonIsPressed`と`JoinPlayersWhenJoinActionIsTriggered`が非推奨の理由は、その自由度の低さにあります。
  Prefabは1つしかアタッチできないため、プレイヤーによって生成するPrefabを変えることはできません。
  
  また、この2つの設定は入力を行うことでしかプレイヤーを生成することができないため、今回のキャラクター選択画面など、シーンを跨いだ機能の実装には向いていません。
  :::

&#13;&#10;

+ **Joining Enabled By Default**
初期状態でプレイヤーの入室を有効化するかを設定する項目です。
チェックを外すとプレイヤーが入室できなくなります。なぜかgetonlyのためスクリプトから値を変更することができません。（たぶん値を変える方法があるはずだけど…）
どちらにせよ、前述した**Join Behavior**の`JoinPlayersManually`を選択している場合は、この設定は無視されるため**関係のない項目**です。

&#13;&#10;

+ **Limit Number Of Players**
入室可能なプレイヤーの上限数を設定する項目です。
チェックを付けることで有効となり、上限数を入力できるフィールドが出てきます。
…ですが、こちらもなぜかgetonlyなのに加え、上の項目と同じく`JoinPlayersManually`を選択している場合はこの設定も無視されるため、**関係のない項目**です。

&#13;&#10;

+ **Split Screen**
画面分割について設定する項目です。
今回は使用しないため割愛します。

# 本題①：プレイヤーを手動で生成する
### PlayerInput.Instantiate()を使う

<details><summary>イメージ図の現在地（展開して表示）</summary>

```plantuml
partition "**アウトゲーム**" {
#lightgreen:**接続するデバイス情報を取得**;
:デバイス情報を紐づけたプレイヤーを生成;
note right
アウトゲームを動かすためのInputや
データのみを持っているプレイヤー
end note
split
:<color:red>Player1:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:red>コントローラ①
使用キャラ：<color:red>キャラクター①;
split again
:<color:blue>Player2:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:blue>キーボード①
使用キャラ：<color:blue>キャラクター②;
endsplit
:プレイヤーのデータをインゲームに送信;
}
partition "**インゲーム**" {
:インゲームがプレイヤーのデータをもとにキャラクターを生成;
note right
こっちがゲーム本編のプレイヤー
end note
}
```
</details>

&#13;&#10;
前述した`PlayerInputManager`の**Join Behavior**で`JoinPlayersManually`を設定したため、プレイヤーを手動で生成するスクリプトを作ります。

基本的には`PlayerInput`コンポーネントが付いたオブジェクトをInstantiateするのですが、デバイス情報を紐づけなけらばならないため、**`PlayerInput.Instantiate()`** という、普段使うものとは違うInstantiate関数を使います。

この関数は`MonoBehaviour`のInstantiateをラップした`PlayerInput`クラスの関数で、通常の引数とは違う引数を渡すことで、**デバイス情報を紐づけたPrefabを生成する**ことができます。

`PlayerInput.Instantiate()`の引数は以下の通りです。（※オーバーロードあり）

| 型 | 引数名 | 説明 |
|:-|:-|:-|
| **GameObject** | prefab | 生成するプレハブ |
| **int** | playerIndex | 生成したプレイヤーが待つ固有の番号 |
| string | controlScheme | 紐づけているデバイスのスキーム。<br>自動で設定されるので今回は指定しない |
| int | splitScreenIndex | 画面分割のインデックス。今回は指定しない |
| **InputDevice** | pairWithDevice | 紐づけるデバイスの情報 |

### InputDevice（デバイス情報）の取得
さて、上記の引数に必要な`InputDevice`型ですが、こちらは`InputAction`（入力）のコールバックである`InputAction.CallbackContext`構造体から取得できます。

以下はプレイヤーに紐づけるデバイス情報を取得するコードのサンプルです。

```cs:InputDeviceの取得方法
[SerializeField] private InputAction playerJoinInputAction = default;

private void Awake()
{
    // InputActionを有効化（これがないと動かない）
    playerJoinInputAction.Enable();
    // InputAction発火時のコールバックを設定
    playerJoinInputAction.performed += OnJoin;
}

private void OnJoin(InputAction.CallbackContext context)
{
    // InputAction.CallbackContext構造体のcontrol.deviceプロパティからInputDeviceを取得
    InputDevice device = context.control.device;
}
```

これで、`InputAction`を発火させた（入力した）デバイス情報を取得することができました。

上記のサンプルでは`InputAction`型の変数をシリアライズ化し、インスペクタから受け付ける入力を設定していますが、ここはコールバックを受け取れればどんな方法でも構いません。（`InputActionReference`型や`InputActionAsset`型など）

**インスペクタから`InputAction`を設定**
![スクリーンショット 2024-04-01 140922.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3744072/684d4a65-ab35-de33-bc38-7190e7fde701.png)

:::note
**【備考】**
本実装ではあまりおすすめしませんが、`InputAction`型はコンストラクタなどを利用することによって、スクリプト内でバインディングすることも可能です。

```cs:InputActionにスクリプト内でバインディングする
private InputAction playerJoinInputAction = default;

private void Awake()
{
    playerJoinInputAction = new InputAction(binding: "<Gamepad>/buttonEast");
    playerJoinInputAction.AddBinding("<Keyboard>/space");
}
```
:::

### 実際にプレイヤーを生成する

<details><summary>イメージ図の現在地（展開して表示）</summary>

```plantuml
partition "**アウトゲーム**" {
:接続するデバイス情報を取得;
#lightgreen:**デバイス情報を紐づけたプレイヤーを生成**;
note right
アウトゲームを動かすためのInputや
データのみを持っているプレイヤー
end note
split
:<color:red>Player1:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:red>コントローラ①
使用キャラ：<color:red>キャラクター①;
split again
:<color:blue>Player2:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:blue>キーボード①
使用キャラ：<color:blue>キャラクター②;
endsplit
:プレイヤーのデータをインゲームに送信;
}
partition "**インゲーム**" {
:インゲームがプレイヤーのデータをもとにキャラクターを生成;
note right
こっちがゲーム本編のプレイヤー
end note
}
```
</details>

&#13;&#10;
それでは、実際にプレイヤーを生成してみましょう。

```cs:プレイヤーを手動で生成するコード
using UnityEngine;
using UnityEngine.InputSystem;

/// <summary>
/// プレイヤーの入退室の管理クラス（アウトゲーム）
/// </summary>
public class PlayerJoinManager : MonoBehaviour
{
    // プレイヤーがゲームにJoinするためのInputAction
    [SerializeField] private InputAction playerJoinInputAction = default;
    // PlayerInputがアタッチされているプレイヤーオブジェクト
    [SerializeField] private PlayerInput playerPrefab = default;
    // 最大参加人数
    [SerializeField] private int maxPlayerCount = default;

    // Join済みのデバイス情報
    private InputDevice[] joinedDevices = default;
    // 現在のプレイヤー数
    private int currentPlayerCount = 0;


    private void Awake()
    {
        // 最大参加可能数で配列を初期化
        joinedDevices = new InputDevice[maxPlayerCount];

        // InputActionを有効化し、コールバックを設定
        playerJoinInputAction.Enable();
        playerJoinInputAction.performed += OnJoin;
    }

    private void OnDestroy()
    {
        playerJoinInputAction.Dispose();
    }

    /// <summary>
    /// デバイスによってJoin要求が発火したときに呼ばれる処理
    /// </summary>
    private void OnJoin(InputAction.CallbackContext context)
    {
        // プレイヤー数が最大数に達していたら、処理を終了
        if (currentPlayerCount >= maxPlayerCount)
        {
            return;
        }

        // Join要求元のデバイスが既に参加済みのとき、処理を終了
        foreach (var device in joinedDevices)
        {
            if (context.control.device == device)
            {
                return;
            }
        }

        // PlayerInputを所持した仮想のプレイヤーをインスタンス化
        // ※Join要求元のデバイス情報を紐づけてインスタンスを生成する
        PlayerInput.Instantiate(
            prefab: playerPrefab.gameObject,
            playerIndex: currentPlayerCount,
            pairWithDevice: context.control.device
            );

        // Joinしたデバイス情報を保存
        joinedDevices[currentPlayerCount] = context.control.device;

        currentPlayerCount++;
    }
}
```

以上が、デバイス情報を紐づけたプレイヤーを生成する最小限のコードです。

スクリプト内で`joinedDevices`という配列を宣言していますが、このような方法で入室済みデバイスの情報を保持しておかないと、**同じデバイスから何度も入室する**ということが可能になってしまうためこのような実装にしています。

また、`PlayerInput.Instantiate()`の引数である`playerIndex`は0から順番に振っています。
この引数は指定しなくとも勝手に割り振ってくれるのですが、**今後の拡張性**を考えると手動で明示的に振ってあげた方がよいと思います。（今回は割愛していますが、仕様によっては退室処理などで使うかも）

::: note

### Tips: プレイヤーの入室イベント

ここでようやく、説明を飛ばした`PlayerInputManager`の**Notification Behavior**が出てきます。
`PlayerInputManager`には**プレイヤーが入室したときに発行されるイベント**というものが存在します。それらのイベントを、どのような形式で発行するかを設定することができます。

+ **Notification Behavior**
    + `SendMessages`
`PlayerInput`コンポーネントがアタッチされているオブジェクトの、"OnPlayerJoined"という名前のメソッドを実行する。パフォーマンスがよくないので**非推奨**。

    &#13;&#10;
    
    + `BroadcastMessages`
`PlayerInput`コンポーネントがアタッチされているオブジェクトと、そのすべての子オブジェクトの"OnPlayerJoined"という名前のメソッドを実行する。こちらもパフォーマンスがよくないので**非推奨**。

    &#13;&#10;

    + **`InvokeUnityEvents`**
`PlayerInputManager`の`onPlayerJoined`プロパティに、インスペクタから実行するメソッドを登録します。

    &#13;&#10;

    + **`InvokeCSharpEvents`**
`PlayerInputManager`の`onPlayerJoined`プロパティに、任意のスクリプトから実行するメソッドを登録します。

基本的には`InvokeCSharpEvents`がスクリプト内で完結できて便利なので、これ一択かなと思います。

```cs:プレイヤー入室イベント
private PlayerInputManager playerInputManager = default;

private void Awake()
{
    playerInputManager = GetComponent<PlayerInputManager>();

    // プレイヤー入室時のコールバックを設定
    playerInputManager.onPlayerJoined += OnPlayerJoined;
    // プレイヤー退室時のコールバックを設定
    playerInputManager.onPlayerLeft += OnPlayerLeft;
}

private void OnPlayerJoined(PlayerInput playerInput)
{
    // プレイヤーが入室したときの処理
}

private void OnPlayerLeft(PlayerInput playerInput)
{
    // プレイヤーが退室したときの処理
}
```
このプレイヤー入室時のイベントの最大の強みは、**ビューとロジックを分割して管理できること**だと思います。
例えば「プレイヤー生成時にUIを表示する」として、最も簡単な方法はプレイヤー生成時（[`PlayerJoinManager`](#実際にプレイヤーを生成する)）にその処理を記述することです。

<details><summary>簡単な方法：PlayerJoinManagerに処理を追記する（展開して表示）</summary>

```cs
~

private void OnJoin(InputAction.CallbackContext context)
{
    if (currentPlayerCount >= maxPlayerCount)
    {
        return;
    }

    foreach (var device in joinedDevices)
    {
        if (context.control.device == device)
        {
            return;
        }
    }

    PlayerInput.Instantiate(
        prefab: playerPrefab.gameObject,
        playerIndex: currentPlayerCount,
        pairWithDevice: context.control.device
        );
        

    // 例えば（変数宣言は割愛）-------------------------------
    Instantiate(UIObject, canvas.transform);  // UIを表示する
    audioSource.PlayOneShot(joinSE);          // SEを再生する
    // ------------------------------------------------------


    joinedDevices[currentPlayerCount] = context.control.device;

    currentPlayerCount++;
}
```
</details>

しかし見てわかる通り、**「プレイヤーを生成する」というロジックの処理の中に、「UIを表示する」というビューの処理が入ってしまっている**ため、非常に読みにくく管理しにくいコードになってしまいます。（ピンと来ない方は、今後処理や表示したいUIなどが膨大に増えることを想像してみてください）

このような「プレイヤーを生成する」処理とは関係ないが、そのタイミングで実行したい内容は、この`onPlayerJoined`プロパティのコールバックを利用するとよいと思います。

:::

# 本題②：遷移先シーンにプレイヤー情報を渡す

さて、少し話が膨らみましたが、ここまでで「一意のデバイスが紐づいたプレイヤー」を生成することはできました。あとはこの情報をシーンを跨いで渡すことができればOKです。

### プレイヤーの情報をまとめる

<details><summary>イメージ図の現在地（展開して表示）</summary>

```plantuml
partition "**アウトゲーム**" {
:接続するデバイス情報を取得;
:デバイス情報を紐づけたプレイヤーを生成;
note right
アウトゲームを動かすためのInputや
データのみを持っているプレイヤー
end note
split
:<color:red>Player1:</color> キャラクターを選ぶ;
#lightgreen:**【プレイヤーのデータを作成】**
**デバイス：<color:red>コントローラ①**
**使用キャラ：<color:red>キャラクター①**;
split again
:<color:blue>Player2:</color> キャラクターを選ぶ;
#lightgreen:**【プレイヤーのデータを作成】**
**デバイス：<color:blue>キーボード①**
**使用キャラ：<color:blue>キャラクター②**;
endsplit
:プレイヤーのデータをインゲームに送信;
}
partition "**インゲーム**" {
:インゲームがプレイヤーのデータをもとにキャラクターを生成;
note right
こっちがゲーム本編のプレイヤー
end note
}
```
</details>

&#13;&#10;
ここで言うプレイヤーの情報とは、今回の例では「デバイス情報」と「選んだキャラの情報」のことを指します。
キャラクター選択機能については説明を割愛しますが、最終的に以下のようなデータクラスを作成することをおすすめします。

```cs:プレイヤーのデータクラス
// 例えばこんなenumがあったとして
public enum CharacterType
{
    Character1,
    Character2,
    Character3,
}

// こういうデータクラスを作る
public class PlayerInfo
{
    public InputDevice PairWithDevice { get; private set; } = default;
    public CharacterType SelectedCharacter { get; private set; } = default;

    public PlayerInfo(InputDevice pairWithDevice, CharacterType selectedCharacter)
    {
        PairWithDevice = pairWithDevice;
        SelectedCharacter = selectedCharacter;
    }
}
```
### 遷移先シーンにプレイヤー情報を渡す

<details><summary>イメージ図の現在地（展開して表示）</summary>

```plantuml
partition "**アウトゲーム**" {
:接続するデバイス情報を取得;
:デバイス情報を紐づけたプレイヤーを生成;
note right
アウトゲームを動かすためのInputや
データのみを持っているプレイヤー
end note
split
:<color:red>Player1:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:red>コントローラ①
使用キャラ：<color:red>キャラクター①;
split again
:<color:blue>Player2:</color> キャラクターを選ぶ;
:【プレイヤーのデータを作成】
デバイス：<color:blue>キーボード①
使用キャラ：<color:blue>キャラクター②;
endsplit
#lightgreen:**プレイヤーのデータをインゲームに送信**;
}
partition "**インゲーム**" {
:インゲームがプレイヤーのデータをもとにキャラクターを生成;
note right
こっちがゲーム本編のプレイヤー
end note
}
```
</details>

&#13;&#10;
別シーンに情報を渡す方法として、今回は[こちらの記事](https://qiita.com/ozaki_shinya/items/0834a635d32585839c23 "Unityでシーン遷移時にシンプルかつ安全にパラメーターを渡す方法 - Qiita")を参考にします。

https://qiita.com/ozaki_shinya/items/0834a635d32585839c23

```cs:シーン遷移し、データを渡す
// どこかで各プレイヤーの情報を配列にしておく
private PlayerInfo[] playerInfos = default;

// 例えば「GameStart」のボタンが押されたら、シーンを遷移
private async Task GameStart()
{
    // シーン遷移し、遷移先のコンポーネントを取得する
    var target = await SceneLoader.Load<InGameHogeScript>("InGameScene");
    // 遷移先のコンポーネントのプロパティにセット
    target.SetInformation(playerInfos);
}
```

これにて、「アウトゲームで生成したプレイヤーが使用するキャラクターを選び、そのデータをインゲームに持ってくる」という機能を実装することができました。
あとは改めてインゲームでキャラクターを生成すればOKです。

```cs:インゲームでキャラクターを生成する
public class InGameHogeScript : MonoBehaviour
{
    [SerializeField] private GameObject character1 = default;
    [SerializeField] private GameObject character2 = default;
    [SerializeField] private GameObject character3 = default;
    
    private PlayerInfo[] playerInfos = default;


    public void SetInformation(PlayerInfo[] playerInfos)
    {
        this.playerInfos = playerInfos;
        // 例えばここで生成
        CreateCharacter();
    }

    // 改めてインゲームでキャラクターを生成
    private void CreateCharacter()
    {
        for (int i = 0; i < playerInfos.Length; i++)
        {
            GameObject character = default;
        
            switch (playerInfos[i].SelectedCharacter)
            {
                case CharacterType.Character1:
                    character = character1;
                    break;

                case CharacterType.Character2:
                    character = character2;
                    break;

                case CharacterType.Character3:
                    character = character3;
                    break;
            }

            PlayerInput.Instantiate(
            prefab: character,
            playerIndex: i,
            pairWithDevice: playerInfos[i].PairWithDevice
            );
        }
    }
}
```

# おわりに
今回はInputSystemを使ったローカルマルチ、およびシーンを跨いだバインディングの実現について解説しました。
前提知識を要する内容が多く、少々抽象的で理解しがたかった部分もあるかもしれませんが、ローカルマルチプレイの制作のお役に立てれば幸いです。