【Unity】Input Systemのデバイス管理の仕組み

こじゃらこじゃら

Input Systemで扱うゲームパッドキーボードなどはどのように管理されるの?できれば内部の仕組みも教えてほしいの。

このはこのは

これはデバイスとして管理されるわ。詳しく解説していくね。

Input Systemでは、マウスキーボードゲームパッドなどの物理コントローラーはデバイスとして扱われます。

Input Systemでサポートするデバイスには、次のようなものが存在します。

サポートされるデバイス(一部)
  • キーボード
  • ゲームパッド
  • ジョイスティック
  • マウス
  • ペン
  • タッチスクリーン
  • センサー(ジャイロや温度など)

このような物理コントローラー以外にも、仮想カーソルなどもデバイスとして扱うことが可能です。

上記で物足りない場合、デバイスを自作することも可能です。これにより、既存のデバイスだけでは対応できない物理コントローラーや好みの仮想コントローラーなどをInput System側から扱えるようになります。

コントローラーからの入力は、通常アンマネージメモリを介して受け渡しされます。 [1]

本記事では、Input Systemにおけるデバイス管理の仕組みや内部設計について解説していきます。ゲームパッドなどのコントローラーが内部的にどのように管理されているか、その基本的な部分を理解するところを目標とします。

動作環境
  • Unity 2023.2.0f1
  • Input System 1.7.0

スポンサーリンク

前提条件

本記事のサンプルを動かすためには、Input Systemパッケージがインストールされ、有効化されているものとします。

また、デバイスの仕組みを理解するにあたり、Input Systemの基本を押さえておくと理解がスムーズです。

この辺は、以下記事で解説しております。

Input Systemにおけるデバイスとは

キーボードやゲームパッドなど、コンピュータやゲーム機などに何らかの形で接続し、アプリケーションに対して操作可能なものを表します。 [2]

また、仮想カーソルなど、物理的には実体は無いがアプリケーション内部で仮想的に存在するコントローラーもデバイスとして扱います。

デバイスの例

参考:Devices | Input System | 1.7.0

通常、デバイスには1つ以上のボタンやスティックなどの入力ソース(Control)を持ちます。

デバイスと子Controlの構成例

デバイスもControlの一種として扱われ、これはボタンなどの子のControlを持つ最上位のControlとして表現されます。したがって、Input SystemではデバイスやボタンなどはControlの階層構造として管理されます。

参考:Controls | Input System | 1.7.0

Input Systemでは、認識できるデバイスが決まっており、認識可能なデバイスが接続されると、内部的にデバイスインスタンスが作成され、接続中デバイス一覧に追加されます。

接続中デバイスの管理イメージ

アプリケーション側からは、接続中デバイス配下のControl(入力ソース)にアクセスして入力値を取得できます。

メモ

キーボードやマウスなどは複数接続されても単一のデバイス(Keyboard、Mouse)として認識されますが、ゲームパッドの場合は個別のデバイスとして認識されます。

このような挙動は、ゲームパッドを複数人に割り振ってプレイしたい場合に有効です。

デバイスの登録

ここまでInput Systemのデバイスの基本的な仕組みについて解説しました。実際に物理デバイスを接続してInput System側からデバイスとして認識させるためには、予めデバイス情報をInput System側に登録しておく必要があります。

キーボードやマウス、ゲームパッドなどのプリセットのデバイスは予めInput System側に登録されているため、アプリケーション側から登録処理を行う必要はありません。

しかし、デバイスを自作する場合はアプリケーション起動時などのタイミングで明示的に登録する必要があります。

InputSystem.RegisterLayout<MyDevice>();

参考:Devices | Input System | 1.7.0

登録されたデバイスは、Input ActionなどからControlを設定する際に選択肢に表示されるようになります。 [3]

登録されたデバイス(Layout)

デバイスの指定

デバイスとその子の入力ソース(Control)は、Control Pathと呼ばれる文字列のパスで指定します。

これは、デバイスをルートとしたスラッシュ区切りのパスです。

Control Pathの例

Control Pathは数あるControlの条件をフィルタリングするために使用され、複数のControlがヒットする場合があります。

Control Pathの仕様などより詳細な解説は以下記事をご覧ください。

メモ

Input Actionなどに指定するBindingはControl Pathとして指定したデバイス配下のControlを表します。このパス(条件)に一致したControlから入力を受け取るという流れで特定ボタンやスティックなどの入力値を取得することができます。

複数のControlがある場合は、どのControlから入力があってもそれを受け取ります。

マルチプレイヤーにおけるデバイスの選択

ローカルマルチプレイなど、複数のゲームパッドをプレイヤー毎に割り当てたい場合、Player InputとPlayer Input Managerコンポーネントを用いて実現できます。

この場合、デフォルトでは1つのデバイスに対して1プレイヤー(ユーザー)を割り当てる挙動になります。 [4]

複数のゲームパッドは通常、それぞれ独立したデバイスとして認識されます。そのため、プレイヤーにゲームパッドを割り当てる場合は、未割り当てのゲームパッド(デバイス)を何らかの方法で指定すれば良いことになります。 [5]

1人プレイゲームなど、プレイヤーへのデバイス割り当てを行わない場合は、すべてのデバイスが入力対象となります。

デバイスの接続・切断

デバイスから実際に入力を受け取るためには、デバイスが接続されている必要があります。接続されることによって、初めてInput System内部でデバイスに相当するオブジェクトが生成されて管理されます。

デバイス接続は、例えばゲームパッドが物理的に接続された際に行うようにします。

// デバイスを接続する
InputSystem.AddDevice<Gamepad>();

逆に、ゲームパッドが物理的に切断された場合は、デバイス切断を行う必要があります。

// 接続されたデバイス
Gamepad device;

・・・(中略)・・・

// デバイスを切断する
InputSystem.RemoveDevice(device);

通常、ゲームパッドやキーボードなどInput System側が予め提供しているデバイスは、このような接続・切断処理を既に行うようになっているため、開発者側が意識する必要はありません。

Tips

現在接続されているデバイスはInput Debuggerから確認できます。

トップメニューのWindow > Analysis > Input Debuggerの順に選択し、ツリーのDevicesを展開すると接続中デバイスがリアルタイムに確認できます。

更にデバイスをダブルクリックすると、より詳細な情報を閲覧できます。

入力値のやり取り

ゲームパッドなどのデバイスの各Controlの入力値は、通常はアンマネージドメモリなどを介して受渡しされます。 [6]

参考:Architecture | Input System | 1.7.0

例えば、ボタン入力は1ビットの値、トリガーなどの連続値は4バイトのfloat型、スティックなどの倒し具合は16バイトのVector2として表現できます。

入力状態のメモリレイアウトの例

このメモリレイアウトはデバイス側が自由に設計できます。

ボタンやスティックなどの入力値が変化し、上記メモリの状態が更新されるたびに、Input System側にイベントとしてメッセージを送信する必要があります。 [7]

var state = new MyDeviceState();
InputSystem.QueueStateEvent(this, state);

参考:Devices | Input System | 1.7.0

送信されるメッセージは、デバイスなどを識別するための4文字(FourCC)入力状態のデータのペアで送られます。

メッセージ送信の例

参考:Struct FourCC | Input System | 1.7.0

Tips

デバイスから送信されるメッセージは、デバイスの詳細ウィンドウから確認できます。

更にメッセージをダブルクリックすると、入力状態の生データが閲覧できます。

Input Action経由で入力を受け取る場合

デバイス側からメッセージを送るタイミングは、入力状態が変化した瞬間一定周期で送るなど、いずれの方法でも入力値を正常に処理できる設計になっています。

例えばInput Actionがボタンを押した瞬間を判定する場合、イベントを受け取った際に入力値が閾値Press Point未満から以上に変化した瞬間にコールバック(performed)として通知します。

スティックなどの連続値をコールバックで受け取る際も、デバイスからメッセージを受け取った時に値が変化したら処理するような挙動になります。

これによって、デバイス側が不要なメッセージを送ってもアプリケーション側のコールバックには無駄な通知が行かないようになっています。

ただし、ActionのAction TypeがPath Throughの場合に限り、デバイス側から送信されたメッセージの入力値をそのままperformedコールバックとして通知します。

参考:Interactions | Input System | 1.7.0

プログラム上での扱い

ここまで解説したデバイスはInputDeviceクラスとして表現されます。

public class InputDevice : InputControl

参考:Class InputDevice | Input System | 1.7.0

ゲームパッド、キーボードなどの各種デバイスはこの派生クラスとして実装されています。

public class Keyboard : InputDevice, ITextInputReceiver
public class Gamepad : InputDevice, IDualMotorRumble, IHaptics

参考:Class Keyboard | Input System | 1.7.0

参考:Class Gamepad | Input System | 1.7.0

また、デバイスの子Controlとして存在するボタンスティックInputControl派生クラスとして表現されます。

public class ButtonControl : AxisControl
public class Vector2Control : InputControl<Vector2>
public class StickControl : Vector2Control

参考:Class ButtonControl | Input System | 1.7.0

参考:Class StickControl | Input System | 1.7.0

InputDeviceクラス自体もInputControlの派生クラスです。

Input Control自体は複数の子のInputControlインスタンスを持つ事が可能なため、デバイスはInputControlインスタンスのツリー構造として表現されます。

Controlの階層構造の例

プログラムでの使用例

ここまで解説したデバイスの仕組みについて理解するためのサンプルスクリプトを幾つか示します。

接続中デバイスへのアクセス

現在Input Systemが接続中とみなしているデバイス一覧をスクリプトから取得する例です。ゲームパッドなどが接続・切断されたら、そのデバイスをログ出力する処理も含まれています。

ConnectedDevicesExample.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.InputSystem;

public class ConnectedDevicesExample : MonoBehaviour
{
    // 初期化
    private void Awake()
    {
        // デバイスの変更を監視
        InputSystem.onDeviceChange += OnDeviceChange;
        
        // 最初に接続されているデバイス一覧をログ出力
        PrintAllDevices();
    }
    
    // 終了処理
    private void OnDestroy()
    {
        // デバイス変更の監視解除
        InputSystem.onDeviceChange -= OnDeviceChange;
    }

    // 全てのデバイスをログ出力
    private void PrintAllDevices()
    {
        // デバイス一覧を取得
        var devices = InputSystem.devices;

        // 現在のデバイス一覧をログ出力
        var sb = new StringBuilder();
        sb.AppendLine("現在接続されているデバイス一覧");
        
        for (var i = 0; i < devices.Count; i++)
        {
            sb.AppendLine($" - {devices[i]}");
        }
        
        print(sb);
    }

    // デバイスの変更を検知した時の処理
    private void OnDeviceChange(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            case InputDeviceChange.Added:
                print($"デバイス {device} が接続されました。");
                break;
            
            case InputDeviceChange.Disconnected:
                print($"デバイス {device} が切断されました。");
                break;
            
            case InputDeviceChange.Reconnected:
                print($"デバイス {device} が再接続されました。");
                break;
            
            default:
                // 接続や切断以外の変更は無視
                return;
        }
        
        // 接続や切断があった場合は、全てのデバイスを再びログ出力
        PrintAllDevices();
    }
}

上記をConnectedDevicesExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチすると機能するようになります。

実行結果

ゲームを実行すると、現在接続されているデバイス一覧が出力されます。

また、コントローラーを切断・接続するとその旨のメッセージを出力し、最新状態のデバイス一覧を出力します。

スクリプトの説明

Input Systemが現在接続されているとみなすデバイス一覧はInputSystem.devicesプロパティから取得できます。

// デバイス一覧を取得
var devices = InputSystem.devices;

参考:Class InputSystem | Input System | 1.7.0

デバイスの接続や切断のイベント、InputSystem.onDeviceChangeプロパティから発火します。

// デバイスの変更を監視
InputSystem.onDeviceChange += OnDeviceChange;

参考:Class InputSystem | Input System | 1.7.0

接続や切断の判定は、第2引数から得られるため、以下処理で判定しています。

switch (change)
{
    case InputDeviceChange.Added:
        print($"デバイス {device} が接続されました。");
        break;
    
    case InputDeviceChange.Disconnected:
        print($"デバイス {device} が切断されました。");
        break;
    
    case InputDeviceChange.Reconnected:
        print($"デバイス {device} が再接続されました。");
        break;
    
    default:
        // 接続や切断以外の変更は無視
        return;
}

Control PathからデバイスやControlを取得する

デバイスにはボタンやスティックなどの複数の入力ソースとなる子のControlを持つことを解説しました。

これらはControl Pathを用いて検索できます。また、得られたControlから子Controlを取得したり、ルートとなるデバイスを取得することも可能です。

以下、指定されたControl PathのControlの階層構造とルートとなるデバイスをログ出力する例です。

EnumControlsExample.cs
using System.Text;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;

public class EnumControlsExample : MonoBehaviour
{
    // 対象のControl(Control Path)
    [InputControl, SerializeField] private string _controlPath;
    
    // 初期化
    private void Awake()
    {
        // Control PathからControlを取得
        var control = InputSystem.FindControl(_controlPath);
        if (control == null)
        {
            Debug.LogError($"指定されたControl Path「{_controlPath}」のControlが見つかりませんでした。");
            return;
        }
        
        // ログ出力
        var sb = new StringBuilder();
        
        sb.AppendLine($"Control Path: {_controlPath}");
        sb.AppendLine($"Device: {control.device}");
        sb.AppendLine($"Control: {control}");
        
        // 子のControlを再帰的にログ出力
        PrintChildrenRecursive(sb, control, 1);
        
        print(sb);
    }
    
    // 子のControlを再帰的にログ出力
    private void PrintChildrenRecursive(StringBuilder sb, InputControl control, int depth)
    {
        // 子のControlを取得
        var children = control.children;
        
        // 子のControlをログ出力
        foreach (var child in children)
        {
            sb.AppendLine($"{new string(' ', depth * 2)}- {child}");
            
            // 子のControlが持つ子のControlを再帰的にログ出力
            PrintChildrenRecursive(sb, child, depth + 1);
        }
    }
}

上記をEnumControlsExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりControl Pathを指定すると機能するようになります。

例では、ゲームパッドの左スティックを表す「<Gamepad>/leftStick」としました。

実行結果

指定されたControl、ルートとなるデバイス子のControl(階層)が出力されます。

試しに、デバイスを表す「<Gamepad>」のみを指定すると、ゲームパッド配下の全てのControlが階層で出力されます。

スクリプトの説明

指定されたControl PathからControlを取得するには、InputSystem.FindControlメソッドを使用します。ただし、これは接続中のデバイス配下のControlに限ります。

参考:Class InputSystem | Input System | 1.7.0

以下、実際に取得する部分のコードです。

// Control PathからControlを取得
var control = InputSystem.FindControl(_controlPath);
if (control == null)
{
    Debug.LogError($"指定されたControl Path「{_controlPath}」のControlが見つかりませんでした。");
    return;
}

そして、以下コードでControlやルートデバイス、子のControlなどをログ出力しています。

// ログ出力
var sb = new StringBuilder();

sb.AppendLine($"Control Path: {_controlPath}");
sb.AppendLine($"Device: {control.device}");
sb.AppendLine($"Control: {control}");

// 子のControlを再帰的にログ出力
PrintChildrenRecursive(sb, control, 1);

print(sb);
// 子のControlを再帰的にログ出力
private void PrintChildrenRecursive(StringBuilder sb, InputControl control, int depth)
{
    // 子のControlを取得
    var children = control.children;
    
    // 子のControlをログ出力
    foreach (var child in children)
    {
        sb.AppendLine($"{new string(' ', depth * 2)}- {child}");
        
        // 子のControlが持つ子のControlを再帰的にログ出力
        PrintChildrenRecursive(sb, child, depth + 1);
    }
}

受け取った入力のデバイスを取得する

Input Action経由で入力を取得する際も、入力情報からControlやそのルートデバイスを取得できます。

以下、コールバック経由で入力が来た時(トリガー)された時にControlとそのデバイスをログ出力する例です。

GetDeviceFromCallbackExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class GetDeviceFromCallbackExample : MonoBehaviour
{
    // 入力を受け取る対象のAction
    [SerializeField] private InputActionProperty _action;
    
    private void Awake() => _action.action.performed += OnPerformed;
    private void OnDestroy() => _action.action.performed -= OnPerformed;
    private void OnEnable() => _action.action.Enable();
    private void OnDisable() => _action.action.Disable();

    private void OnPerformed(InputAction.CallbackContext context)
    {
        // CallbackContextからControlを取得
        var control = context.control;
        
        // Controlからデバイスを取得
        var device = control.device;
        
        // ログ出力
        print($"Control Path: {control.path}, Device: {device}, Control: {control}");
    }
}

上記をGetDeviceFromCallbackExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、Actionの設定を行うと機能します。

例では、ゲームパッド配下の全てのControlを対象とする「<Gamepad>/*」を指定してみることにします。

Control Pathのワイルドカード指定について詳しく知りたい方は、以下記事をご覧ください。

実行結果

ゲームパッドの入力があると、その入力のデバイスやControl情報がログ出力されます。

スクリプトの説明

Actionのコールバックの受け取り側では、以下のようにログ出力しています。

private void OnPerformed(InputAction.CallbackContext context)
{
    // CallbackContextからControlを取得
    var control = context.control;
    
    // Controlからデバイスを取得
    var device = control.device;
    
    // ログ出力
    print($"Control Path: {control.path}, Device: {device}, Control: {control}");
}

コールバック引数のInputAction.CallbackContext構造体のcontrolプロパティから現在のControl(InputControl型インスタンス)が得られます。そして、ここから自ずとdeviceプロパティによりルートのデバイスが得られます。

参考:Struct InputAction.CallbackContext | Input System | 1.7.0

さいごに

Input Systemでは、キーボードやマウス、ゲームパッド、仮想カーソルなどをデバイスとして扱います。

Input SystemのActionなど何らかの入力を受け取るには、その対象のデバイスがInput System側から接続されている必要があり、通常はこの接続・切断処理は無意識のうちに行われています。

本記事では割愛しましたが、デバイスの仕組みを理解することは、カスタムデバイスを実装する際にも大いに役立ちます。カスタムデバイスの実装方法は別記事で解説する予定です。

関連記事

参考サイト

スポンサーリンク