【Unity】Input Systemで独自デバイスを実装する

こじゃらこじゃら

Input Systemで独自のコントローラーを作って使いたいの。例えば仮想コントローラーなどを作りたいの。

このはこのは

可能だわ。InputDevice継承クラスを実装してInput Systemにデバイス登録すれば使えるようになるわ。

Input Systemでカスタムデバイスを実装する方法の解説記事です。

カスタムデバイスはInputDevice継承クラスを実装し、これをInput System側に登録すると使えるようになります。

[InputControlLayout(displayName = "My Device", stateType = typeof(MyDeviceState))]
public class MyDevice : InputDevice
{
    // カスタムデバイスの実装
    // デバイスの登録・解除処理など
}
独自デバイスが認識されている様子

独自デバイスを実装することで、例えば次のようなことが実現できます。

実現できること
  • 独自のハードウェアをコントローラーとして認識させる
  • Input Systemから認識されない、または予期せぬ形で認識されたHIDのレイアウトを強制的に上書きする
  • 仮想デバイスを作成する

参考:Devices | Input System | 1.7.0

参考:Class VirtualMouseInput | Input System | 1.7.0

Input System側で提供されているKeyboardMouseGamepadなどのクラスも全てInputDeviceクラスを継承して実装されています。

参考:Class InputDevice | Input System | 1.7.0

本記事では、Input Systemでカスタムデバイスを実装する方法を解説していきます。また、Input Actionから実際に設定したり、スクリプトから入力値を取得する部分にも触れます。

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

スポンサーリンク

前提条件

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

ここまでのセットアップ手順は以下記事にて解説しています。

また、本記事を読み進めるにあたり、Input Systemのデバイスの仕組みについて把握しておくと理解がスムーズです。

デバイスの仕組みの解説は以下記事をご覧ください。

カスタムデバイスの実装

カスタムデバイスは以下の流れで実装できます。

実装の流れ
  • 入力状態の生データを定義する構造体の実装
  • InputDevice継承クラスの実装
  • デバイスのレイアウトの登録
  • Control(入力ソース)の実装
  • デバイスの登録・解除処理の実装

順番に解説していきます。

入力状態の生データを定義する構造体の実装

ボタンやアナログ入力などのデバイスから得られる入力は、構造体に入力状態として保持されます。

これは、IInputStateTypeInfoインタフェースを実装した構造体として定義します。

using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;

// ボタンを1つだけ持つカスタムデバイスの入力状態
[StructLayout(LayoutKind.Explicit)]
public struct OneButtonDeviceState : IInputStateTypeInfo
{
    // フォーマット識別子
    public FourCC format => new FourCC('1', 'B', 'T', 'N');

    // ボタン
    [FieldOffset(0)]
    [InputControl(name = "button", layout = "Button", bit = 0, displayName = "One Button")]
    public byte button;
}

例では、ボタンをただ1つ持つカスタムデバイスとして解説を進めます。

IInputStateTypeInfoインタフェースは、次のようにFour CCを返すformatプロパティを持ちます。

public interface IInputStateTypeInfo
{
    FourCC format { get; }
}

Four CCは、デバイスを識別するための4文字で表現される値です。例では、「1」「B」「T」「N」という値を返しています。

// フォーマット識別子
public FourCC format => new FourCC('1', 'B', 'T', 'N');

構造体の各種フィールドは、次のように明示的にオフセットを指定する形でメモリ上にレイアウトしています。

[StructLayout(LayoutKind.Explicit)]
public struct OneButtonDeviceState : IInputStateTypeInfo

レイアウトに特にこだわりがなければ、以下のように自動的に連続的な配置にすることも可能です。

[StructLayout(LayoutKind.Sequential)]
public struct OneButtonDeviceState : IInputStateTypeInfo

InputDevice継承クラスの実装

カスタムデバイスクラスはInputDevice継承クラスとして実装します。

// ボタンを1つだけ持つカスタムデバイス
[InputControlLayout(displayName = "One Button Device", stateType = typeof(OneButtonDeviceState))]
public class OneButtonDevice : InputDevice
{
    // TODO : デバイスの実装
}

参考:Class InputDevice | Input System | 1.7.0

デバイス名やどの構造体を入力状態データとして扱うかなどの情報は、InputControlLayout属性で指定します。

それぞれdisplayNamestateTypeに設定すれば良いです。 [1]

参考:Class InputControlLayoutAttribute | Input System | 1.7.0

デバイスを登録する

実装したカスタムデバイスをInput System側から認識させるためには、デバイスをレイアウトとして登録する必要があります。

これはInputSystem.RegisterLayoutメソッドで行います。

// デバイスのレイアウトを登録
InputSystem.RegisterLayout<OneButtonDevice>();

参考:Class InputSystem | Input System | 1.7.0

実際の処理の実行タイミングは、UnityエディタならUnity起動時ビルドではアプリ起動時が相応しいでしょう。

// ボタンを1つだけ持つカスタムデバイス
[InputControlLayout(displayName = "One Button Device", stateType = typeof(OneButtonDeviceState))]
#if UNITY_EDITOR
// Unityエディタで初期化処理を呼び出すのに必要
[UnityEditor.InitializeOnLoad]
#endif
public class OneButtonDevice : InputDevice
{
    // 初期化
    static OneButtonDevice()
    {
        // デバイスのレイアウトを登録
        InputSystem.RegisterLayout<OneButtonDevice>();
    }
}

メモ

Unityエディタ上でカスタムデバイスを登録すると、Input ActionのBindingの候補に登録したデバイスが表示されるようになります。

入力イベントの通知

ボタンの押下状態などのControlの入力状態が変化した場合、そのままではInput System側は入力を検知できません。

そのため、変化した瞬間または定期的にInput System側に入力状態の更新を通知する必要があります。

// マウスの左ボタンが押されている場合はボタンを押下状態にする
var state = new OneButtonDeviceState
{
    button = // TODO : 実際の入力値を反映
};

// 入力状態をキューに追加
InputSystem.QueueStateEvent(this, state);

入力の通知はInputSystem.QueueStateEventメソッドで行います。これは次の3つの引数を持つstaticメソッドです。

public static void QueueStateEvent<TState>(InputDevice device, TState state, double time = -1)
    where TState : struct, IInputStateTypeInfo

第1引数デバイスのインスタンス第2引数入力状態の構造体データ第3引数イベント発生時刻(省略可能)を指定します。 [2]

参考:Class InputSystem | Input System | 1.7.0

入力の通知は低レイヤーのドライバからコールバックなどの通知として受け取った時Input Systemが更新されるタイミングなどいずれの方法でも可能です。

Input Systemが更新されるタイミングで通知したい場合は、デバイスクラスにIInputUpdateCallbackReceiverインタフェースを実装すればよいです。これでOnUpdateメソッドを通じて更新タイミングをフックできます。

参考:Interface IInputUpdateCallbackReceiver | Input System | 1.7.0

例では、次のようにInput Systemが更新される度に、マウス左ボタン入力をバイパスするような形で通知することとします。

// IInputUpdateCallbackReceiverを実装すると、
// Input Systemの更新タイミングをOnUpdateで受け取れるようになる
public class OneButtonDevice : InputDevice, IInputUpdateCallbackReceiver
{
    ・・・(中略)・・・

    public void OnUpdate()
    {
        var mouse = Mouse.current;
        if (mouse == null) return;
        
        // マウスの左ボタンが押されている場合はボタンを押下状態にする
        var state = new OneButtonDeviceState
        {
            button = mouse.leftButton.isPressed ? (byte)1 : (byte)0
        };

        // 入力状態をキューに追加
        InputSystem.QueueStateEvent(this, state);
    }
}

Control(入力ソース)の初期化

Input Action経由でしか入力を受け取らない場合は必須ではありませんが、カスタムデバイスクラスに直でアクセスして入力を取得するためのプロパティを実装します。

ボタン入力などは、Controlを通じて取得できます。このようなControlはInputControl継承クラスとして表現されます。

参考:Class InputControl | Input System | 1.7.0

例えば、ボタンを表すControlはButtonControl型プロパティとして定義すればよいです。

using UnityEngine.InputSystem.Controls;

・・・(中略)・・・

// ボタン
public ButtonControl button { get; private set; }

参考:Class ButtonControl | Input System | 1.7.0

そして、独自定義したControlは、デバイスが接続されて初期化されるタイミングなどで外部からアクセスできるようにします。

初期化タイミングはFinishSetupメソッドとして実装すればよいです。

public class OneButtonDevice : InputDevice
{
    // ボタン
    public ButtonControl button { get; private set; }

    ・・・(中略)・・・

    // セットアップ完了時に呼び出される
    protected override void FinishSetup()
    {
        base.FinishSetup();

        // ボタンを取得
        button = GetChildControl<ButtonControl>("button");
    }
}

参考:Class InputControl | Input System | 1.7.0

ここまでの解説で、カスタムデバイス側の実装は一通り出来たことになります。

デバイスを追加・削除する

実装したカスタムデバイスは、Input Action側やスクリプトから扱えるようになりますが、そのままでは未接続のままなので入力を取得できません。

実際にデバイスを機能させるためには、Input System側にデバイスを追加する必要があります。

OneButtonDevice oneButtonDevice;

・・・(中略)・・・

// デバイスの登録
oneButtonDevice = InputSystem.AddDevice<OneButtonDevice>();

参考:Class InputSystem | Input System | 1.7.0

また、一度追加したデバイスは使わなくなった時点などで削除する必要があります。

// デバイスの登録解除
InputSystem.RemoveDevice(_oneButtonDevice);

注意

削除を忘れると、永遠にデバイスが接続された状態になります。この状態で追加すると、重複してデバイスが登録されて増えていってしまいます。

カスタムデバイスの実装例

ここまでの解説を踏まえたカスタムデバイスの実装例です。

OneButtonDeviceState.cs
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;

// ボタンを1つだけ持つカスタムデバイスの入力状態
[StructLayout(LayoutKind.Explicit)]
public struct OneButtonDeviceState : IInputStateTypeInfo
{
    // フォーマット識別子
    public FourCC format => new FourCC('1', 'B', 'T', 'N');

    // ボタン
    [FieldOffset(0)] [InputControl(name = "button", layout = "Button", bit = 0, displayName = "One Button")]
    public byte button;
}

// ボタンを1つだけ持つカスタムデバイス
[InputControlLayout(displayName = "One Button Device", stateType = typeof(OneButtonDeviceState))]
#if UNITY_EDITOR
// Unityエディタで初期化処理を呼び出すのに必要
[UnityEditor.InitializeOnLoad]
#endif
public class OneButtonDevice : InputDevice, IInputUpdateCallbackReceiver
{
    // ボタン
    public ButtonControl button { get; private set; }

    // 初期化
    static OneButtonDevice()
    {
        // デバイスのレイアウトを登録
        InputSystem.RegisterLayout<OneButtonDevice>();
    }

    // セットアップ完了時に呼び出される
    protected override void FinishSetup()
    {
        base.FinishSetup();

        // ボタンを取得
        button = GetChildControl<ButtonControl>("button");
    }

    public void OnUpdate()
    {
        var mouse = Mouse.current;
        if (mouse == null) return;
        
        // マウスの左ボタンが押されている場合はボタンを押下状態にする
        var state = new OneButtonDeviceState
        {
            button = mouse.leftButton.isPressed ? (byte)1 : (byte)0
        };

        // 入力状態をキューに追加
        InputSystem.QueueStateEvent(this, state);
    }
}

カスタムデバイスから入力を受け取るスクリプトは以下のようになります。

GetCustomDeviceButtonExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class GetCustomDeviceButtonExample : MonoBehaviour
{
    // 接続したカスタムデバイス
    private OneButtonDevice _oneButtonDevice;

    private void Awake()
    {
        // デバイスの追加
        _oneButtonDevice = InputSystem.AddDevice<OneButtonDevice>();
    }

    private void OnDestroy()
    {
        // デバイスの削除
        InputSystem.RemoveDevice(_oneButtonDevice);
    }

    private void Update()
    {
        // ボタンの入力状態を取得して表示
        if (_oneButtonDevice.button.wasPressedThisFrame)
        {
            // ボタンが押された瞬間
            print("Pressed");
        }
        else if (_oneButtonDevice.button.wasReleasedThisFrame)
        {
            // ボタンが離された瞬間
            print("Released");
        }
    }
}

上記2つのスクリプトをUnityプロジェクトに保存し、GetCustomDeviceButtonExampleの方を適当なゲームオブジェクトにアタッチすると機能するようになります。

実行結果

マウスの左ボタンをクリックすると、ボタンが反応するようになりました。

スクリプトの説明

カスタムデバイスのスクリプトは前述の通りのため割愛します。

受取り側のスクリプトでは、初期化と終了のタイミングでデバイスの追加と削除を行なっています。

// 接続したカスタムデバイス
private OneButtonDevice _oneButtonDevice;

private void Awake()
{
    // デバイスの追加
    _oneButtonDevice = InputSystem.AddDevice<OneButtonDevice>();
}

private void OnDestroy()
{
    // デバイスの削除
    InputSystem.RemoveDevice(_oneButtonDevice);
}

これにより、次のコードで入力を取得できるようになります。

private void Update()
{
    // ボタンの入力状態を取得して表示
    if (_oneButtonDevice.button.wasPressedThisFrame)
    {
        // ボタンが押された瞬間
        print("Pressed");
    }
    else if (_oneButtonDevice.button.wasReleasedThisFrame)
    {
        // ボタンが離された瞬間
        print("Released");
    }
}

カスタムデバイスのbuttonプロパティ(自作したボタンのControl)のプロパティを参照してボタンの押した瞬間と離した瞬間にログ出力しています。

Input Actionで使用する

カスタムデバイスを登録すると、Input Action側から入力を取得できるようになります。

Input Actionから入力を受け取る方法はいくつか存在しますが、本記事ではスクリプト中から動的生成して取得する例を示します。

実装例

以下、Action経由で前述のカスタムデバイスの入力を受け取る例です。

InputActionExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class InputActionExample : MonoBehaviour
{
    // 接続したカスタムデバイス
    private OneButtonDevice _oneButtonDevice;
    
    // カスタムデバイスからの入力を受け取るAction
    [SerializeField] private InputAction _action;

    private void Awake()
    {
        // デバイスの追加
        _oneButtonDevice = InputSystem.AddDevice<OneButtonDevice>();
        
        // Actionの入力状態が変化した時のコールバックを登録
        _action.performed += _ => print("Performed");
    }

    private void OnDestroy()
    {
        // デバイスの削除
        InputSystem.RemoveDevice(_oneButtonDevice);
        
        // Actionの破棄
        _action.Dispose();
    }
    
    private void OnEnable() => _action.Enable();
    private void OnDisable() => _action.Disable();
}

上記をInputActionExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりカスタムデバイスのActionを設定してください。

実行結果

Action経由でも入力を受け取れるようになりました。

注意

本記事の実装例のように、既存のデバイスのControlの入力をそのままカスタムデバイス入力として反映すると、0と1の値が交互に通知され、毎フレーム押した瞬間と離した瞬間として処理されてしまうことがあります。

低レイヤーから入力値を取得する場合、取得タイミングなどに注意しながら実装するとよいでしょう。

スクリプトの説明

Action経由で入力を受け取るため、次の行でInputActionフィールドを定義しています。

// カスタムデバイスからの入力を受け取るAction
[SerializeField] private InputAction _action;

入力があった時のコールバックは、以下のようにperformedコールバックを購読する形で受け取るようにしています。

private void Awake()
{
    // デバイスの追加
    _oneButtonDevice = InputSystem.AddDevice<OneButtonDevice>();
    
    // Actionの入力状態が変化した時のコールバックを登録
    _action.performed += _ => print("Performed");
}

定義したActionはそのままでは有効にならないため、コンポーネントが有効になるタイミングなどでEnableメソッドで有効化する必要があります。

private void OnEnable() => _action.Enable();
private void OnDisable() => _action.Disable();

また、スクリプト側から生成したInput Actionは、最後にDisposeメソッドで破棄しています。

private void OnDestroy()
{
    // デバイスの削除
    InputSystem.RemoveDevice(_oneButtonDevice);
    
    // Actionの破棄
    _action.Dispose();
}

様々なControlの追加例

ここまでの例では、ただ一つのボタンを持つカスタムデバイスの実装例を紹介しました。

複数のボタンやスティックなどの2軸入力など、あらゆる型のデバイスを実装することが可能です。

カスタムゲームパッドの実装例

以下は、独自のカスタムゲームパッドを実装した例です。

MyCustomGamepadState.cs
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;

// カスタムゲームパッドの入力状態
[StructLayout(LayoutKind.Explicit)]
public struct MyCustomGamepadState : IInputStateTypeInfo
{
    public FourCC format => new FourCC('M', 'Y', 'C', 'G');

    [FieldOffset(0)]
    [InputControl(name = "button1", layout = "Button", bit = 0, displayName = "Button1")]
    [InputControl(name = "button2", layout = "Button", bit = 1, displayName = "Button2")]
    [InputControl(name = "button3", layout = "Button", bit = 2, displayName = "Button3")]
    [InputControl(name = "button4", layout = "Button", bit = 3, displayName = "Button4")]
    public byte buttons;

    [FieldOffset(1)] [InputControl(name = "dpad", layout = "Dpad", displayName = "Dpad")]
    public byte dpad;

    [FieldOffset(2)] [InputControl(name = "leftStick", layout = "Stick", displayName = "Left Stick")]
    public Vector2 leftStick;

    [FieldOffset(10)] [InputControl(name = "rightStick", layout = "Stick", displayName = "Right Stick")]
    public Vector2 rightStick;
}

// カスタムゲームパッド
[InputControlLayout(displayName = "My Custom Gamepad", stateType = typeof(MyCustomGamepadState))]
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoad]
#endif
public class MyCustomGamepad : InputDevice, IInputUpdateCallbackReceiver
{
    public ButtonControl button1 { get; private set; }
    public ButtonControl button2 { get; private set; }
    public ButtonControl button3 { get; private set; }
    public ButtonControl button4 { get; private set; }
    public DpadControl dpad { get; private set; }
    public StickControl leftStick { get; private set; }
    public StickControl rightStick { get; private set; }

    static MyCustomGamepad()
    {
        InputSystem.RegisterLayout<MyCustomGamepad>();
    }

    protected override void FinishSetup()
    {
        base.FinishSetup();

        button1 = GetChildControl<ButtonControl>("button1");
        button2 = GetChildControl<ButtonControl>("button2");
        button3 = GetChildControl<ButtonControl>("button3");
        button4 = GetChildControl<ButtonControl>("button4");
        dpad = GetChildControl<DpadControl>("dpad");
        leftStick = GetChildControl<StickControl>("leftStick");
        rightStick = GetChildControl<StickControl>("rightStick");
    }

    public void OnUpdate()
    {
        var gamepad = Gamepad.current;
        if (gamepad == null)
            return;
        
        var state = default(MyCustomGamepadState);

        if (gamepad.buttonSouth.isPressed)
            state.buttons |= 1 << 0;
        if (gamepad.buttonWest.isPressed)
            state.buttons |= 1 << 1;
        if (gamepad.buttonNorth.isPressed)
            state.buttons |= 1 << 2;
        if (gamepad.buttonEast.isPressed)
            state.buttons |= 1 << 3;

        if (gamepad.dpad.up.isPressed)
            state.dpad |= 1 << 0;
        if (gamepad.dpad.down.isPressed)
            state.dpad |= 1 << 1;
        if (gamepad.dpad.left.isPressed)
            state.dpad |= 1 << 2;
        if (gamepad.dpad.right.isPressed)
            state.dpad |= 1 << 3;

        state.leftStick = gamepad.leftStick.ReadValue();
        state.rightStick = gamepad.rightStick.ReadValue();

        InputSystem.QueueStateEvent(this, state);
    }
}

実際の使用例は以下のようになります。

CustomGamepadExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class CustomGamepadExample : MonoBehaviour
{
    // 接続したカスタムデバイス
    private MyCustomGamepad _myDevice;

    private void Awake()
    {
        // デバイスの追加
        _myDevice = InputSystem.AddDevice<MyCustomGamepad>();
    }

    private void OnDestroy()
    {
        // デバイスの削除
        InputSystem.RemoveDevice(_myDevice);
    }

    private void Update()
    {
        // 左スティック
        var leftStick = _myDevice.leftStick.ReadValue();
        print($"LeftStick: {leftStick}");
        
        // 右スティック
        var rightStick = _myDevice.rightStick.ReadValue();
        print($"RightStick: {rightStick}");
    }
}

この例では左右スティックのみを取得していますが、当然ながら他のボタン入力も同様に取得可能です。

さいごに

Input Systemの既存デバイスは全てInputDevice継承クラスとして管理されます。カスタムデバイスもInputDevice継承クラスとして実装してInput System側に登録することで使用可能になります。

Input System側では期待通りに認識されないHIDをデバイスとして実装したり、仮想デバイスを自由に追加できるようになるため、うまく活用すれば様々な応用が期待できるでしょう。

関連記事

参考サイト

スポンサーリンク