【Unity】Input Systemの入力値の型をチェックする

こじゃらこじゃら

Input Systemで入力値を読むとき、違う型が来ると次のようにエラーが出てしまうの…

このはこのは

Input Actionから値を読む前に型チェックすれば良いわ。この辺を拡張メソッド化すれば楽に使えるようになるわ。

Input SystemのActionから入力値を読み込む時は、次のようなコードを書くことが多いでしょう。

private void OnPerformed(InputAction.CallbackContext context)
{
    // 入力値を取得
    var inputValue = context.ReadValue<float>();
    
    // TODO : 入力値を使って何か処理する
}

実際にAction側から受け取れる値は、取得先のControl(ボタンやスティックなど)によって型が異なる場合があります。

例えば、上記のfloat型の入力値として取得するコードに対し、次のようにスティックなどの2軸入力(Vector2型)をActionから受け取れるようにすると実行時エラーとなります。

InvalidOperationException: Cannot read value of type 'float' from control '/XboxOneGampadMacOSWireless/leftStick' bound to action 'Action Prop[/XboxOneGampadMacOSWireless/leftTrigger,/XboxOneGampadMacOSWireless/leftStick]' (control is a 'StickControl' with value type 'Vector2')

これは、ReadValueメソッド側で入力値の型と受取り側の要求する型が一致しない場合InvalidOperationException例外をスローするためです。

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

このようなエラーを回避するためのアプローチの一つとして、入力値を取得する前に型チェックが出来れば防ぐことが可能です。

特に、Input Action側で複数の型のBindingを指定し、2通り以上の型で入力値が渡ってくるケースなどで活躍できるでしょう。

本記事では、Input Actionの入力値を事前チェックしてエラーを防ぐ方法を解説します。また、拡張メソッドを自作して使いやすくする例も紹介します。

動作環境
  • Unity 2023.1.1f1
  • Input System 1.6.3

スポンサーリンク

前提条件

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

この手順は以下記事で解説しています。

また、本記事ではInput Action経由で入力を取得するものとします。

Input Actionの基本的な使い方は以下記事で解説しています。

コールバック経由で型をチェックする場合

コールバック経由で入力値を取得する場合、引数から得られるInputAction.CallbackContext構造体ReadValueメソッドを用いて値を取得します。

private void OnPerformed(InputAction.CallbackContext context)
{
    // 入力値を取得
    var inputValue = context.ReadValue<float>();
    
    // TODO : 入力値を使って何か処理する
}

このInputAction.CallbackContext構造体には、渡ってきた入力(Control)の型valueTypeプロパティから取得できます。

public readonly Type valueType { get; }

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

これを次のようなコードで取得可能かどうかをチェックします。

// 型チェック
if (!typeof(float).IsAssignableFrom(context.valueType))
    return;

Type.IsAssignableFromメソッドは、引数に指定された型が呼び元の型に代入可能かどうかを判定します。

型同士の比較をせずにType.IsAssignableFromメソッドを使う理由は、入力値の型が要求している型の派生型でも取得可能にするためです。

例えば、構造体Aを継承した構造体Bがある場合、構造体Bの入力値が来た時に構造体Aにキャストして取得可能になります。

サンプルスクリプト

以下、指定されたActionの入力値がfloat型かどうかチェックし、float型なら値を取得してログ出力する例です。

CallbackExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class CallbackExample : MonoBehaviour
{
    // 受け取る入力
    [SerializeField] private InputActionProperty _actionProp;

    private void Awake()
    {
        _actionProp.action.performed += OnPerformed;
    }

    private void OnDestroy()
    {
        _actionProp.action.performed -= OnPerformed;
        _actionProp.action.Dispose();
    }

    private void OnEnable()
    {
        _actionProp.action.Enable();
    }

    private void OnDisable()
    {
        _actionProp.action.Disable();
    }

    private void OnPerformed(InputAction.CallbackContext context)
    {
        // 型チェック
        if (!typeof(float).IsAssignableFrom(context.valueType))
            return;

        // 入力値を取得
        var inputValue = context.ReadValue<float>();

        // 入力値をログ出力
        print($"inputValue: {inputValue}");
    }
}

上記をCallbackExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターより受け取る入力を設定します。

例ではゲームパッドの左トリガー(float型)と左スティック(Vector2型)を設定するものとします。

実行結果

float型の入力が来た時はログ出力するが、それ以外の型(例では左スティックのVector2型)の入力が来た時は無視するようになりました。

左スティックが操作されてもエラーメッセージが表示されません。

スクリプトの説明

型チェックして入力値を受け取る処理は以下部分です。

private void OnPerformed(InputAction.CallbackContext context)
{
    // 型チェック
    if (!typeof(float).IsAssignableFrom(context.valueType))
        return;

    // 入力値を取得
    var inputValue = context.ReadValue<float>();

    // 入力値をログ出力
    print($"inputValue: {inputValue}");
}

例ではfloat型として入力値を受け取りたいため、typeof(float)に対してIsAssignableFromメソッドを呼び出し引数contextのvalueTypeプロパティを指定しています。

このコードにより、context.valueTypeがfloat型変数に代入可能ならtrue、それ以外ならfalseが得られるため分岐処理しています。

float型変数に代入可能なら、context.ReadValue<float>メソッドでfloat型として入力値を安全に取得できます。

ポーリング方式で型をチェックする場合

コールバックではなく、InputActionインスタンスから直接ReadValueメソッドで入力値を取得する場合でも型チェックができます。

ただし、コールバック経由で取得する場合と比べて若干面倒で、まず現在アクティブなInputControlを調べる必要があります。

InputAction action;

・・・(中略)・・・

// 現在アクティブなControlを取得
InputControl control = action.activeControl;

// Controlが無かったら何もしない
if (control == null) return;

入力が何もない場合など、アクティブなControlが存在しない場合もあるため、nullチェックも必要です。

参考:Class InputAction | Input System | 1.6.1

Controlが取得出来たら、valueTypeプロパティに入力値の型が格納されているため、前述の例と同じ要領で型チェックが行えます。

// 型チェック
if (!typeof(float).IsAssignableFrom(control.valueType))
    return;

// 入力値の取得
var inputValue = action.ReadValue<float>();

サンプルスクリプト

ポーリング方式で入力値を受け取る際に型チェックを行う例です。

PollingExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class PollingExample : MonoBehaviour
{
    // 受け取る入力
    [SerializeField] private InputActionProperty _actionProp;

    private void OnDestroy()
    {
        _actionProp.action.Dispose();
    }

    private void OnEnable()
    {
        _actionProp.action.Enable();
    }

    private void OnDisable()
    {
        _actionProp.action.Disable();
    }

    private void Update()
    {
        var action = _actionProp.action;

        // アクティブなControlを取得
        var control = action?.activeControl;
        if (control == null) return;

        // 型チェック
        if (!typeof(float).IsAssignableFrom(control.valueType))
            return;

        // 入力値を取得
        var inputValue = action.ReadValue<float>();

        // 入力値をログ出力
        print($"inputValue: {inputValue}");
    }
}

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

例ではゲームパッドのSouthボタンと左スティックを割り当てることとしました。

実行結果

ボタン(float型のControl)を押すと、その間入力値がログ出力され続けます。

こちらもfloat型以外のControl(例では左スティック)が入力されてもログ出力されません。

スクリプトの説明

アクティブなControl取得から型チェック、入力値取得までの一連の流れは、以下Update内で行っています。

private void Update()
{
    var action = _actionProp.action;

    // アクティブなControlを取得
    var control = action?.activeControl;
    if (control == null) return;

    // 型チェック
    if (!typeof(float).IsAssignableFrom(control.valueType))
        return;

    // 入力値を取得
    var inputValue = action.ReadValue<float>();

    // 入力値をログ出力
    print($"inputValue: {inputValue}");
}

1つ目の例と大きく変わった部分はInputActionからアクティブなControlを取得する部分になります。

拡張メソッドを自作して使いやすくする

ここまで入力値の取得前に型チェックを行う方法を解説してきました。しかしながら、このような型チェックのコードを毎回書くのは大変かもしれません。

これは拡張メソッドとして使いまわせるようにすると楽です。

コールバックで使うInputAction.CallbackContext構造体、ポーリングなどで使うInputActionクラスそれぞれに対して型チェック付きの拡張メソッドを実装する方法を紹介します。

拡張メソッドの実装例

以下、拡張メソッドを実装したクラスの完成形です。

ReadValueExtensions.cs
using UnityEngine.InputSystem;

public static class ReadValueExtensions
{
    /// <summary>
    /// InputAction.CallbackContextから入力値を安全に取得する。
    /// コールバックなどで使う。
    /// </summary>
    /// <returns>型が一致したときは入力値、それ以外はデフォルト値を返す。</returns>
    public static T ReadValueSafe<T>(this InputAction.CallbackContext context)
        where T : struct
    {
        // 型チェック
        if (!typeof(T).IsAssignableFrom(context.valueType))
            return default;

        // 入力値を返す
        return context.ReadValue<T>();
    }

    /// <summary>
    /// InputActionから入力値を安全に取得する。
    /// ポーリングで値を取得する場合などで使う。
    /// </summary>
    /// <returns>型が一致したときは入力値、それ以外はデフォルト値を返す。</returns>
    public static T ReadValueSafe<T>(this InputAction action)
        where T : struct
    {
        // アクティブなControl取得
        var control = action?.activeControl;
        if (control == null) return default;

        // 型チェック
        if (!typeof(T).IsAssignableFrom(control.valueType))
            return default;

        // 入力値を返す
        return action.ReadValue<T>();
    }

    /// <summary>
    /// InputAction.CallbackContextから入力値を取得する。
    /// 取得成否も知りたい場合に使う
    /// </summary>
    /// <returns>型が一致したときはtrue、それ以外はfalseを返す。</returns>
    public static bool TryReadValue<T>(this InputAction.CallbackContext context, out T value)
        where T : struct
    {
        // 型チェック
        if (!typeof(T).IsAssignableFrom(context.valueType))
        {
            value = default;
            return false;
        }

        // 入力値を返す
        value = context.ReadValue<T>();
        return true;
    }
    
    /// <summary>
    /// InputActionから入力値を取得する。
    /// 取得成否も知りたい場合に使う
    /// </summary>
    /// <returns>型が一致したときはtrue、それ以外はfalseを返す。</returns>
    public static bool TryReadValue<T>(this InputAction action, out T value)
        where T : struct
    {
        // アクティブなControl取得
        var control = action?.activeControl;
        if (control == null)
        {
            value = default;
            return false;
        }

        // 型チェック
        if (!typeof(T).IsAssignableFrom(control.valueType))
        {
            value = default;
            return false;
        }

        // 入力値を返す
        value = action.ReadValue<T>();
        return true;
    }
}

上記をReadValueExtensions.csなどとUnityプロジェクトに保存すると使用可能になります。

使用例

以下、コールバックとポーリング方式それぞれで前述の拡張メソッドを使用して入力値を取得する例です。

UseExtensionExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class UseExtensionExample : MonoBehaviour
{
    // 受け取る入力
    [SerializeField] private InputActionProperty _actionPropCallback;
    [SerializeField] private InputActionProperty _actionPropUpdate;

    private void Awake()
    {
        _actionPropCallback.action.performed += OnPerformed;
    }

    private void OnDestroy()
    {
        _actionPropCallback.action.performed -= OnPerformed;
        _actionPropCallback.action.Dispose();
        _actionPropUpdate.action.Dispose();
    }

    private void OnEnable()
    {
        _actionPropCallback.action.Enable();
        _actionPropUpdate.action.Enable();
    }

    private void OnDisable()
    {
        _actionPropCallback.action.Disable();
        _actionPropUpdate.action.Disable();
    }

    private void OnPerformed(InputAction.CallbackContext context)
    {
        // 入力値を取得
        // 失敗した場合は何もしない
        if (!context.TryReadValue<float>(out var inputValue))
            return;

        print($"Callback: {inputValue}");
    }

    private void Update()
    {
        // 入力値を取得
        // 型不一致の場合は入力0として処理する
        var inputValue = _actionPropUpdate.action.ReadValueSafe<float>();

        print($"Polling: {inputValue}");
    }
}

上記をUseExtensionExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりコールバック用、ポーリング用それぞれのActionを設定する動くようになります。

例では、コールバック用ActionにゲームパッドのWestボタンと左スティック、ポーリング用ActionにゲームパッドのEastボタンと左スティックを割り当てることとします。

実行結果

コールバックとポーリングそれぞれ型チェック付きで入力値が取得できていることが確認できます。

左スティックを操作してもエラーになりません。

スクリプトの説明

コールバック側では、入力値の取得に失敗した場合は処理を止めたいため、自作のTryReadValue拡張メソッドで成否チェックもしています。

private void OnPerformed(InputAction.CallbackContext context)
{
    // 入力値を取得
    // 失敗した場合は何もしない
    if (!context.TryReadValue<float>(out var inputValue))
        return;

    print($"Callback: {inputValue}");
}

一方、ポーリング方式での取得では、入力値の取得に失敗した場合は0として処理したいため、自作のReadValueSafe拡張メソッドで取得し、成否チェックは行いません。

private void Update()
{
    // 入力値を取得
    // 型不一致の場合は入力0として処理する
    var inputValue = _actionPropUpdate.action.ReadValueSafe<float>();

    print($"Polling: {inputValue}");
}

さいごに

Input SystemのActionから入力値を取得する時は、Input Actionの設定次第では期待する型と異なる型の入力が渡ってくる場合があります。

設定を間違えなければ起こり得ないですが、複数の型を許容するActionの場合、異なる入力値を取得するとエラーとなるため型チェックが出来たほうが望ましい場合があります。

型チェックのコードを毎回書くのが大変な場合、拡張メソッドを自作して使いやすくすると良いでしょう。

参考サイト

スポンサーリンク