【Unity】Input Systemでローカルマルチを実装する

こじゃらこじゃら

Input Systemでローカルマルチを実装する方法はないの?
複数のコントローラーを繋いで画面分割して遊べるゲームを目指してるの…

このはこのは

Player Input Managerを使えばこの辺が楽に出来るわ。

Input Systemでは、PCなどに複数のゲームパッドを繋いでゲームをプレイするローカルマルチプレイヤーに対応しています。

これは、Player InputおよびPlayer Input Managerコンポーネントを用いることで簡単に実現可能です。

次のような挙動が実現できます。

Player Input Managerでできること
  • 個別のコントローラーにプレイヤーを割り当てて操作可能にする
  • 何かキーが押されたら参加する
  • プレイヤーの入室・退室を検知する
  • プレイヤー毎のカメラを画面分割表示する

本記事では、このようなローカルマルチをInput Systemで実現する方法を解説していきます。また、後半では画面分割する方法を解説し、Cinemachine使用下での分割方法も紹介します。

動作環境
  • Unity 2022.1.21f1
  • Input System 1.5.1
  • Cinemachine 2.9.7

この作品はユニティちゃんライセンス条項の元に提供されています

スポンサーリンク

前提条件

事前にInput Systemパッケージがインストールされ、有効化されているものとします。

ここまでの手順が分からない方は、以下記事を参考にセットアップを済ませてください。

また、Input Systemからの入力はPlayer Inputコンポーネントを通じて取得するものとします。

Player Inputコンポーネントの基本的な使い方は以下記事で解説しています。

プレイヤー側の準備

次のようにプレイヤーオブジェクトにPlayer Inputコンポーネントがアタッチされており、Player Inputから入力を取得して操作できるようなPrefabをセットアップしておきます。

本記事ではPlayer Inputおよび操作スクリプトのセットアップ手順は割愛します。

操作可能なプレイヤーPrefabが欲しい場合、以下記事の内容に従ってプレイヤーを実装し、プレイヤーオブジェクトをPrefab化してください。

複数プレイヤーを管理する仕組み

Player Input Managerコンポーネントによって複数プレイヤーのオブジェクトやコントローラーを管理できます。

Player Input Managerは個々のプレイヤーをPlayer Inputコンポーネントとして管理します。

複数プレイヤーの管理イメージ

Player Inputがアタッチされたゲームオブジェクトがシーン上に配置されると、自動的にプレイヤーインデックスが割り振られ、入室扱いとなります。 [1]

オブジェクトが無効化されたり破棄されると、そのプレイヤーは退室扱いとなります。この時、インデックスは全体的に詰められます。 [2]

入退室のイメージ

各プレイヤーに割り当てるコントローラーのスキームは同一でも異なっていても構いません。

例えば、キーボード&マウス、ゲームパッド1、ゲームパッド2にそれぞれプレイヤーを割り当てることも可能です。

コントローラー割り当てのイメージ

Player Input Managerのセットアップ

まず、Player Input Managerコンポーネントを適当なゲームオブジェクトに追加します。

すると、次のような項目がインスペクターより設定可能になります。

各設定項目については後述します。

実行結果

Player Input Managerコンポーネントがシーンに存在している状態で、Player Inputコンポーネントがアタッチされているオブジェクト(プレイヤーなど)がシーンに配置されると、Player Inputにはユーザーインデックスが割り当てられます。

インデックスは、Player InputコンポーネントのインスペクターのDebug > Userから確認できます。

Player Input Managerの各種設定

ここからは、Player Input Managerコンポーネントの各設定項目について解説していきます。

入退室通知の設定

プレイヤーオブジェクトが(追加されるなどで)有効化されると「入室(参加)」イベントが通知されます。

プレイヤーオブジェクトが無効化されると「退室」イベントが通知されます。

参考:Class PlayerInputManager | Input System | 1.5.1

通知方法はNotification Behaviour項目から以下4種類を選択できます。

Send MessagesSendMessage経由で通知する。「入室」はOnPlayerJoined「退室」はすOnPlayerLeftイベントとして通知される。Player Inputコンポーネントがアタッチされているオブジェクトに受信用のスクリプトがアタッチされている必要がある。
Broadcast MessagesBoardcastMessage経由で通知する。Player Inputコンポーネントがアタッチされているオブジェクトまたは子オブジェクトに受信用のスクリプトがアタッチされている必要がある。
Invoke Unity EventUnityEvent経由で通知する。「入室」はPlayerInputManager.playerJoinedEventプロパティ「退室」はPlayerInputManager.playerLeftEventプロパティ。
Invoke C Shard EventsC#標準のデリゲート経由で通知する。「入室」はPlayerInputManager.onPlayerJoinedプロパティ「退室」はPlayerInputManager.onPlayerLeftプロパティ。

参考:Class PlayerInputManager | Input System | 1.5.1

サンプルスクリプト

以下、UnityEvent経由で通知を受け取るスクリプトの例です。

ReceiveNotificationExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class ReceiveNotificationExample : MonoBehaviour
{
    // プレイヤー入室時に受け取る通知
    public void OnPlayerJoined(PlayerInput playerInput)
    {
        print($"プレイヤー#{playerInput.user.index}が入室!");
    }

    // プレイヤー退室時に受け取る通知
    public void OnPlayerLeft(PlayerInput playerInput)
    {
        print($"プレイヤー#{playerInput.user.index}が退室!");
    }
}

上記をReceiveNotificationExample.csという名前で保存し、適当なゲームオブジェクトにアタッチします。

そして、Player Input ManagerのNotification BehaviourUnity Eventに設定し、Events配下のそれぞれにメソッドを指定してください。

実行結果

プレイヤーが追加されると、「入室」通知のログが出力されます。プレイヤーを削除すると、「退室」通知のログが出力されます。

このとき、該当するプレイヤーのインデックスも一緒に表示しています。

スクリプトの説明

各プレイヤーオブジェクトのユーザー情報は、PlayerInput.userプロパティから取得できます。

print($"プレイヤー#{playerInput.user.index}が入室!");

これは、InputUser型の構造体で、インデックス以外にもユニークIDデバイス情報なども格納しています。

参考:Struct InputUser | Input System | 1.5.1

プレイヤー入室方法の設定

Player Input Managerには、特定の条件を満たしたタイミングでプレイヤーを追加する機能を有しています。これは、プレイヤーのPrefabをInstantiateすることで実現します。

入室に関する設定は、Joining以下の項目から行います。

参考:Class PlayerInputManager | Input System | 1.5.1

Join Behaviour項目には、プレイヤー(Prefab)を追加させる条件を指定します。

次の3種類の設定が可能です。

Join Players When Button Is Pressedプレイヤーが割り当てられていないコントローラーのボタンが押された時に入室する。
Join Players When Join Action Is Triggered指定されたActionの入力があった時に入室する。
Join Players Manuallyスクリプトから手動で入室させる設定。

参考:Enum PlayerJoinBehavior | Input System | 1.5.1

Player Prefab項目には、追加するプレイヤーのPrefabを指定します。Player Input Manager側から新しいプレイヤーを入室させる際には、このPrefabがシーン上にInstantiateされます。

Joining Enabled By Default項目には、初期状態でプレイヤーの入室を有効化するかを設定します。チェックが入っていると有効になります。

注意

この項目が無効になっていると、Join Behaviourで指定された入室条件を満たしていてもPrefabが追加されないのでご注意ください。

Limit Number of Players項目にチェックを入れると、入室可能なプレイヤー数の上限を設定できるようになります。

上限は、チェックを入れると現れるMax Player Count項目から設定できます。

以下、Prefabを設定するまでの例です。

例では、Join BehaviourにJoin Players When Button Is Pressedを指定し、未割り当ての任意ボタンが押されたら入室する設定としています。

実行結果

入室の条件を満たすと、プレイヤーがシーンに配置されるようになりました。

プレイヤー毎に異なるコントローラーから排他的に操作できるようになっていれば成功です。

各プレイヤーのPlayer Inputをインスペクターから見ると、別々のコントローラーが割り当てられていることが確認できます。

注意

もし複数のコントローラーから1プレイヤーが制御可能になっている場合、Input Action AssetのControl Schemeの設定に問題がある可能性があります。

例えば、該当するActionのControl Schemeに複数のLayoutが設定されている場合などに起きます。

この場合、SchemeをGamepadのみなど単一のLayoutにすると良いです。

画面分割設定

Player Input Managerでは、プレイヤー毎のカメラ分割機能もサポートしています。

カメラ分割を有効にするには、Split-Screen > Enable Split-Screen項目にチェックを入れます。

すると、詳細設定項目が出現します。

設定内容の詳細については、後述する画面分割のセットアップ手順にて説明します。

画面分割する

Player Input Managerの画面分割機能を有効化すると、各プレイヤー毎の視点のカメラで画面分割することができます。

この手順について解説していきます。

カメラの配置

まず、プレイヤーを写すカメラオブジェクトを配置します。

プレイヤーをPrefabとしている場合、Prefabの子オブジェクトとして配置すれば良いでしょう。

この場合、例えばルートのプレイヤーオブジェクト直下にカメラオブジェクトとCharacter Controllerオブジェクトが存在する形になります。

Player Input側の設定

Player InputコンポーネントのCamera項目に、前述のプレイヤー用カメラを指定します。

最終的に以下のようになっていれば良いです。

画面分割方法の設定

画面分割の仕方は、Player Input ManagerコンポーネントのSplie-Screen以下の項目から設定します。

Maintain Aspect Ratio項目は、画面分割する際にカメラのアスペクト比を維持する設定です。

Set Fixed Number項目は、分割表示する画面数を固定化する設定です。チェックを入れると、その下に出現するNumber of Screens項目から画面数を指定できます。

例えば4を指定すると、最初から画面を4分割する前提でカメラ画面の表示領域が計算されます。

Screen Rectangle項目には、画面全体の表示領域をビューポート座標として指定します。

初期値は画面全体を表す(0, 0) 〜 (1, 1)です。

実行結果

プレイヤーが入室すると、数に応じて適切に画面分割されているのを確認できます。

Cinemachine使用下で画面分割する

ここまで解説した方法では、Cinemachineを使わない純粋なUnityカメラを用いて画面分割を行っていました。

Cinemachineを使用した環境下でも画面分割は可能です。

ただし、次のような制約があります。

  • プレイヤー数分のレイヤーが必要
    • 例:P0、P1、P2、P3
  • UnityカメラのCulling Maskにプレイヤー用のレイヤーが排他的に設定されている
    • 例:P0が設定され、P1、P2、P3は未設定など
  • Cinemachineカメラ(バーチャルカメラ)のレイヤー設定
    • 例:P0

これは、Cinemachineで複数のUnityカメラを扱う方法に準拠した制約です。Cinemachineで複数カメラを機能させる仕組み、および基本手順を知りたい方は、以下記事をご覧ください。

以上を踏まえ、Cinemachine環境下でも機能させる手順を解説していきます。

Cinemachineカメラの設定

まず、プレイヤーPrefab配下のカメラにCinemachine Brainコンポーネントを追加します。

次に、プレイヤー用のCinemachineカメラ(バーチャルカメラ)を配置します。

ヒエラルキー左上の+アイコン > Cinemachine > Virtual Cameraより配置できます。

必要に応じて、カメラの追従設定を行います。

例では、FollowとLook At項目にプレイヤーオブジェクトを指定し、BodyにTransposerを指定し、Binding ModeにSimple Follow With World Upを指定して追従させることとします。

プレイヤー専用レイヤーの定義

各プレイヤー毎のレイヤーを追加します。

例では4人対戦ゲームを想定し、P0、P1、P2、P3の4レイヤーを追加することとします。

プレイヤーインデックスに応じたレイヤーを設定するスクリプトの実装

プレイヤー入室時に割り当てられるプレイヤーインデックスに応じたレイヤーを設定するスクリプトを実装します。

スクリプトの実装例は以下のようになります。

PlayerCameraLayerUpdater.cs
using System;
using Cinemachine;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerCameraLayerUpdater : MonoBehaviour
{
    [SerializeField] private PlayerInput _playerInput;
    [SerializeField] private CinemachineVirtualCamera _cinemachineCamera;

    // プレイヤーインデックスとレイヤーの対応
    [Serializable]
    public struct PlayerLayer
    {
        public int index;
        public int layer;
    }

    [SerializeField] private PlayerLayer[] _playerLayers;

    private int _currentIndex = -1;

    // 初期化
    private void Awake()
    {
        if (_playerInput == null) return;

        // レイヤー更新
        OnIndexUpdated(_playerInput.user.index);
    }

    // 有効化
    private void OnEnable()
    {
        if (PlayerInputManager.instance == null) return;

        // プレイヤーが退室した時のイベントを監視する
        PlayerInputManager.instance.onPlayerLeft += OnPlayerLeft;
    }

    // 無効化
    private void OnDisable()
    {
        if (PlayerInputManager.instance == null) return;

        PlayerInputManager.instance.onPlayerLeft -= OnPlayerLeft;
    }

    // プレイヤーが退室した時に呼ばれる
    private void OnPlayerLeft(PlayerInput playerInput)
    {
        // 他プレイヤーが退室した時はインデックスがずれる可能性があるので
        // レイヤーを更新する
        if (playerInput.user.index >= _playerInput.user.index)
            return;

        // この時、まだインデックスは前のままなので-1する必要がある
        OnIndexUpdated(_playerInput.user.index - 1);
    }

    // プレイヤーインデックスが更新された時に呼ばれる
    private void OnIndexUpdated(int index)
    {
        if (_currentIndex == index) return;

        // インデックスに応じたレイヤー情報取得
        var layerIndex = Array.FindIndex(_playerLayers, x => x.index == index);
        if (layerIndex < 0) return;

        // プレイヤー用のカメラ取得
        var playerCamera = _playerInput.camera;
        if (playerCamera == null) return;

        // カメラのCullingMaskを変更
        // 自身のレイヤーは表示、他プレイヤーのレイヤーは非表示にする
        for (var i = 0; i < _playerLayers.Length; i++)
        {
            var layer = 1 << _playerLayers[i].layer;

            if (i == index)
                playerCamera.cullingMask |= layer;
            else
                playerCamera.cullingMask &= ~layer;
        }

        // Cinemachineカメラのレイヤー変更
        _cinemachineCamera.gameObject.layer = _playerLayers[layerIndex].layer;

        _currentIndex = index;
    }
}

上記をPlayerCameraLayerUpdater.csとしてUnityプロジェクトに保存します。

スクリプトの適用

前述のスクリプトをプレイヤーPrefabにアタッチし、インスペクターよりPlayer InputCinemachineカメラを設定します。

そして、各プレイヤーインデックスとレイヤーの対応テーブルを定義します。

例では、レイヤーP0、P1、P2、P3のインデックスがそれぞれ7、8、9、10であるため、上記の設定にしています。

Player Input Manager側のイベント通知設定

上記サンプルスクリプトでは、C#イベント経由で退室通知を受け取るため、Player Input ManagerコンポーネントのNotification Behaviour項目にはInvoke C Shard Eventsを指定してください。

実行結果

ここまでの手順を成功させると、動画のようにCinemachineが適用された状態でも独立してカメラが制御されることが確認できます。

この時、プレイヤーのUnityカメラのCulling Maskには、自身のレイヤーが設定され、他プレイヤーのレイヤーが未設定になります。

また、Cinemachineカメラのレイヤーには自身に対応するレイヤーが設定されます。

スクリプトの解説

自身のプレイヤーインデックスが更新される時、次の処理でレイヤー情報とカメラの取得を行います。

// インデックスに応じたレイヤー情報取得
var layerIndex = Array.FindIndex(_playerLayers, x => x.index == index);
if (layerIndex < 0) return;

// プレイヤー用のカメラ取得
var playerCamera = _playerInput.camera;
if (playerCamera == null) return;

次に、自身のカメラのCulling Maskを更新します。この時、他プレイヤーのレイヤーは除外する必要があります。

// カメラのCullingMaskを変更
// 自身のレイヤーは表示、他プレイヤーのレイヤーは非表示にする
for (var i = 0; i < _playerLayers.Length; i++)
{
    var layer = 1 << _playerLayers[i].layer;

    if (i == index)
        playerCamera.cullingMask |= layer;
    else
        playerCamera.cullingMask &= ~layer;
}

そして、Cinemachineカメラのレイヤーを自身のものに設定します。

// Cinemachineカメラのレイヤー変更
_cinemachineCamera.gameObject.layer = _playerLayers[layerIndex].layer;

ここまでの処理は、自身が入室した時に行うほか、他プレイヤーが退室した時もインデックスがずれる可能性があるため行います。

// 初期化
private void Awake()
{
    if (_playerInput == null) return;

    // レイヤー更新
    OnIndexUpdated(_playerInput.user.index);
}
// プレイヤーが退室した時に呼ばれる
private void OnPlayerLeft(PlayerInput playerInput)
{
    // 他プレイヤーが退室した時はインデックスがずれる可能性があるので
    // レイヤーを更新する
    if (playerInput.user.index >= _playerInput.user.index)
        return;

    // この時、まだインデックスは前のままなので-1する必要がある
    OnIndexUpdated(_playerInput.user.index - 1);
}

さいごに

Input Systemでローカルマルチを実装したい場合、Player Input Managerコンポーネントを用いると比較的楽に実装できます。

ただし、プレイヤーはPlayer Inputコンポーネント経由で操作する必要があるという制約もあります。

また、画面分割もサポートしていることも特徴です。Cinemachine環境下でも実現可能です。

関連記事

参考サイト

スポンサーリンク