【Unity】Input SystemのBindingをインスペクターから指定可能にする

こじゃらこじゃら

次のようにInput ActionのBindingをインスペクターから直接指定する方法はないの?

このはこのは

カスタムクラスを作ったりエディタ拡張を作ることになるけど可能だわ。

Input SystemActionには、次のように複数のBindingが格納されます。

これらの各Bindingには、固有のGUIDが割当てられています。このGUIDをスクリプトで保持しておけば、Bindingへの参照を表現できます。

しかしながら、BindingのGUIDを毎回調べてインスペクターから指定するのは大変でしょう。

また、任意のスクリプトに対し、BindingのGUIDをインスペクターからGUIで選択して指定する手段はInput System 1.5.1現在では提供されていません。

そのため、エディタ拡張(Property Drawer)で設定用のUIを自作する必要が出てきます。

本記事では、このようなBinding参照を実現する自作クラスおよびエディタ拡張を実装して対応する方法を紹介します。

動作環境
  • Unity 2022.2.16f1
  • Input System 1.5.1

スポンサーリンク

前提条件

事前にInput Systemパッケージがインストールされ、使用可能になっているものとします。

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

また、本記事ではInput Actionを使用して入力を取得することを前提とします。

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

本記事では、Input Action Asset内にあるBindingをスクリプトから参照可能にするものとして解説を進めます。

また、Bindingの参照およびエディタ拡張の実装は、Input Systemパッケージ公式のサンプル「Rebinding UI」のスクリプトRebindActionUI.csの内部実装を参考にしています。

参考:Class RebindActionUI | Input System | 1.0.2

クラスのソースコード

まず最初に、インスペクターからBindingへの参照指定を可能にし、スクリプトから楽に扱えるようにしたクラスの全体を示します。

クラス本体およびそのエディタ拡張(Property Drawer)が1つのファイルとなっています。

InputBindingReference.cs
using System;
using UnityEngine;
using UnityEngine.InputSystem;

#if UNITY_EDITOR
using UnityEditor;
#endif

[Serializable]
public class InputBindingReference
{
    // Actionへの参照
    [SerializeField] private InputActionReference _actionRef;

    // BindingのGUID
    [SerializeField] private string _id;

    #region プロパティ

    // InputActionAsset
    public InputActionAsset Asset => _actionRef != null ? _actionRef.asset : null;

    // Action
    public InputAction Action => _actionRef != null ? _actionRef.action : null;

    // BindingのGUID
    public string ID => _id;

    // Bindingのインデックス
    public int Index
    {
        get
        {
            if (Action == null) return -1;

            Guid.TryParse(_id, out var guid);
            return Action.bindings.IndexOf(x => x.id == guid);
        }
    }

    // Binding
    public InputBinding Binding
    {
        get
        {
            var index = Index;
            return index >= 0 ? Action.bindings[Index] : default;
        }
    }

    #endregion

    // 文字列
    public override string ToString()
    {
        if (_actionRef == null) return string.Empty;

        var result = _actionRef.ToString();

        if (!string.IsNullOrEmpty(Binding.effectivePath))
            result = $"{result}/{Binding.effectivePath}";

        if (!string.IsNullOrEmpty(Binding.groups))
            result = $"{result} [{Binding.groups}]";

        return result;
    }

    // 暗黙の型変換
    public static implicit operator InputBinding(InputBindingReference bindingRef)
    {
        return bindingRef.Binding;
    }
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(InputBindingReference))]
public class InputBindingReferenceEditor : PropertyDrawer
{
    private readonly GUIStyle _boldLabel = new GUIStyle("MiniBoldLabel");
    private readonly GUIContent _actionLabel = new GUIContent("Action");

    // UIの高さを取得する
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var lineCount = 2;
        
        // プロパティを取得
        var actionRefProp = property.FindPropertyRelative("_actionRef");

        // InputActionAssetを取得
        var actionRef = actionRefProp.objectReferenceValue as InputActionReference;
        if (actionRef != null) lineCount++;
        
        return EditorGUIUtility.singleLineHeight * lineCount + EditorGUIUtility.standardVerticalSpacing * (lineCount - 1);
    }

    // Bindingの選択UIを描画する
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        
        position.height = EditorGUIUtility.singleLineHeight;

        // ラベル表示
        EditorGUI.LabelField(position, label, _boldLabel);
        position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;

        // プロパティを取得
        var actionRefProp = property.FindPropertyRelative("_actionRef");
        var idProp = property.FindPropertyRelative("_id");

        // インデントして表示
        using (new EditorGUI.IndentLevelScope())
        {
            // InputActionAsset項目を表示
            EditorGUI.PropertyField(position, actionRefProp, _actionLabel);
            position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;

            // InputActionAssetを取得
            var actionRef = actionRefProp.objectReferenceValue as InputActionReference;
            if (actionRef == null) return;

            // BindingのGUIDを取得
            Guid.TryParse(idProp.stringValue, out var id);

            // InputActionAssetとBindingのGUIDからInputActionを取得
            var action = actionRef.action;
            if (action == null) return;

            // Bindingのインデックスを取得
            var bindings = action.bindings;
            var bindingIndex = bindings.IndexOf(x => x.id == id);

            // Bindingの一覧をドロップダウンで表示
            var displayOptions = InputBinding.DisplayStringOptions.DontUseShortDisplayNames |
                                 InputBinding.DisplayStringOptions.IgnoreBindingOverrides |
                                 InputBinding.DisplayStringOptions.DontOmitDevice;

            var bindingNames = new string[action.bindings.Count];
            for (var i = 0; i < bindingNames.Length; i++)
            {
                var binding = bindings[i];

                // Bindingの表示名を決定
                var displayString = binding.isComposite
                    ? binding.name
                    : action.GetBindingDisplayString(i, displayOptions);

                // Composite Bindingの一部の場合
                if (binding.isPartOfComposite)
                    displayString = $"{ObjectNames.NicifyVariableName(binding.name)}: {displayString}";

                // スラッシュ区切りを解除してフラットな表示にする
                displayString = displayString.Replace('/', '\\');

                bindingNames[i] = displayString;
            }

            // 選択中のインデックスを取得
            var newBindingIndex = EditorGUI.Popup(position, "Binding", bindingIndex, bindingNames);

            // Bindingのインデックスが変更されたら、BindingのGUIDを更新
            if (newBindingIndex != bindingIndex)
            {
                idProp.stringValue = bindings[newBindingIndex].id.ToString();
            }
        }
        
        EditorGUI.EndProperty();
    }
}

#endif

上記をInputBindingReference.csという名前でUnityプロジェクトに保存すると使用可能になります。

クラスの使い方

InputBindingReference型のフィールドをシリアライズ可能なフィールド([SerializeField]属性指定、publicなど)で定義すると機能するようになります。

以下、InputBindingReference型フィールドを定義し、その内容をログ出力する例です。

UseExample.cs
using System.Text;
using UnityEngine;

public class UseExample : MonoBehaviour
{
    // Bindingへの参照(インスペクタから設定)
    [SerializeField] private InputBindingReference _bindingRef;

    // 初回のみ実行
    private void Start()
    {
        var sb = new StringBuilder();

        // Bindingの内容をログ出力
        sb.AppendLine($"InputBindingReference: {_bindingRef}");
        sb.AppendLine($"  - Asset: {_bindingRef.Asset}");
        sb.AppendLine($"  - Action: {_bindingRef.Action}");
        sb.AppendLine($"  - ID: {_bindingRef.ID}");
        sb.AppendLine($"  - Index: {_bindingRef.Index}");
        sb.AppendLine($"  - Binding: {_bindingRef.Binding}");

        Debug.Log(sb);

        // Bindingの属するActionの入力を受け取れるかテスト
        if (_bindingRef?.Action != null)
        {
            // performedコールバックが呼ばれたらログ出力
            _bindingRef.Action.performed += ctx => Debug.Log("performed!");

            // ActionはEnableしないと入力を受け取れない
            _bindingRef.Action.Enable();
        }
    }
}

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

実行結果

次のようにインスペクターからInput Action AssetのBindingを指定できるようになります。

上記設定をした後にゲームを実行すると、コンソールにBindingへの参照情報がログ出力されます。

また、Bindingのキー(例ではスペースキー)を入力する度に文字列「performed!」がログ出力されます。

試しにDebugモードでインスペクターを表示してみると、BindingのGUIDが文字列としてシリアライズされていることが確認できます。

クラスの説明

上記で紹介したBindingへの参照を表すInputBindingReferenceクラスでは、内部的に次のフィールドを保持しています。

フィールド名説明
InputActionReference_actionRefInputActionAsset(ScriptableObject)のActionへの参照
string_id参照先BindingのGUID文字列

InputActionReferenceはInput System側が提供しているクラスで、特定のActionへの参照を表すクラスです。

参考:Class InputActionReference | Input System | 1.5.1

また、関連情報へアクセスするための以下プロパティを公開しています。

プロパティ名説明
InputActionAssetAssetBindingが属する大元のInput Action Assetインスタンス
InputActionActionBindingが属するActionインスタンス
stringIDBindingのGUID文字列
intIndexAction中の自身のBindingのインデックス
InputBindingBindingInputBindingインスタンス

クラスの内部実装の説明

Input Action Assetの特定のBindingへの参照は、以下情報により決定できます。

  • InputActionAsset(ScriptableObject)への参照(UnityEngine.ObjectのGUID)
  • Bindingへの参照(GUID文字列)

ただし、本記事で紹介したクラスInputBindingReferenceでは、Action関連情報へのアクセスをしやすくするために、Actionの参照(GUID文字列)も追加で持たせるようにしています。

InputActionAssetとActionへの参照はInputActionReferenceで表現できるため、InputBindingReferenceクラスではInputActionReferenceインスタンスとBindingのGUIDを保持しておけば良いことになります。

各フィールドのGUIDと参照関係

これらのフィールドの内容に基づいて、各プロパティも機能するように実装しています。

GUIDからBindingを取得する処理は、次のインデックスを返すプロパティ内で行っています。

// Bindingのインデックス
public int Index
{
    get
    {
        if (Action == null) return -1;

        Guid.TryParse(_id, out var guid);
        return Action.bindings.IndexOf(x => x.id == guid);
    }
}

IndexOfメソッドを使ってGUIDが一致する要素を検索し、ヒットした要素のインデックスを返しています。

参考:Struct ReadOnlyArray<TValue> | Input System | 1.5.1

エディタ拡張部分の説明

エディタ拡張部分はInputBindingReferenceEditorクラスにより実装しています。InputBindingReferenceEditorクラスはPropertyDrawer継承クラスです。

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(InputBindingReference))]
public class InputBindingReferenceEditor : PropertyDrawer

Input Action AssetおよびActionへの参照は、InputActionReferenceクラスのPropertyDrawer側が実装してくれているため、次のようにプロパティを表示するだけで済みます。

// InputActionAsset項目を表示
EditorGUI.PropertyField(position, actionRefProp, _actionLabel);

指定されたActionのBindingを一覧表示するために、項目名の文字列配列を作成します。

// Bindingの一覧をドロップダウンで表示
var displayOptions = InputBinding.DisplayStringOptions.DontUseShortDisplayNames |
                     InputBinding.DisplayStringOptions.IgnoreBindingOverrides |
                     InputBinding.DisplayStringOptions.DontOmitDevice;

var bindingNames = new string[action.bindings.Count];
for (var i = 0; i < bindingNames.Length; i++)
{
    var binding = bindings[i];

    // Bindingの表示名を決定
    var displayString = binding.isComposite
        ? binding.name
        : action.GetBindingDisplayString(i, displayOptions);

    // Composite Bindingの一部の場合
    if (binding.isPartOfComposite)
        displayString = $"{ObjectNames.NicifyVariableName(binding.name)}: {displayString}";

    // スラッシュ区切りを解除してフラットな表示にする
    displayString = displayString.Replace('/', '\\');

    bindingNames[i] = displayString;
}

最終的にbindingNames変数に各Bindingに対応した表示名が格納されます。配列のインデックスはBindingのインデックスに対応しています。

そして、以下でbindingNames変数の内容をドロップダウンリストで表示しています。

// 選択中のインデックスを取得
var newBindingIndex = EditorGUI.Popup(position, "Binding", bindingIndex, bindingNames);

選択された項目に変更があったら、以下処理でBindingのGUIDを文字列に変換してInputBindingReferenceインスタンスの_idフィールドに反映します。

// Bindingのインデックスが変更されたら、BindingのGUIDを更新
if (newBindingIndex != bindingIndex)
{
    idProp.stringValue = bindings[newBindingIndex].id.ToString();
}

キーコンフィグでの活用例

本記事で紹介したInputBindingReferenceクラスは、例えばキーコンフィグの実装などで役立つかもしれません。

キーコンフィグでは、指定されたActionに対してリバインド(Bindingの上書き)を行いますが、この時どのBindingかをインデックスで指定する必要があります。

InputAction action;
InputActionRebindingExtensions.RebindingOperation rebindOperation;

・・・(中略)・・・

// Bindingのインデックスを取得
int bindingIndex = 何らかの方法でBindingのインデックスを決定

// インタラクティブなリバインド開始
rebindOperation = action.PerformInteractiveRebinding(bindingIndex)
    // コールバックの設定など
    .Start();

本記事のクラスを使うと、インスペクターから指定したBindingのインデックスを簡単に取得できるようになります。

InputAction action;
InputActionRebindingExtensions.RebindingOperation rebindOperation;

// Bindingの参照情報
InputBindingReference bindingRef;

・・・(中略)・・・

// Bindingのインデックスを取得
int bindingIndex = bindingRef.Index;

// インタラクティブなリバインド開始
rebindOperation = action.PerformInteractiveRebinding(bindingIndex)
    // コールバックの設定など
    .Start();

キーコンフィグでどのBindingを使用するかの指定は、他にもスキームなどで条件を絞ったりして決定することもできます。

方法の一つとして検討していただければ幸いです。

Input Systemパッケージのサンプル「Rebinding UI」にあるスクリプトRebindActionUI.csでも同様の実装例がありますが、本記事で紹介したInputBindingReferenceクラスはどのスクリプトでも簡単に使いまわせる利点があります。

使用上の注意事項

当記事で紹介したクラスはBindingの指定を楽にする一方、注意すべき点も存在します。

Input Action AssetのBindingを削除した時

InputBindingReferenceクラスから参照しているBindingが削除されると、存在しないBindingのGUIDを保持することになり、Missingとなります。

インスペクター上では何も選択されない状態になります。

例えばBindingを作り直したりした時などは、GUIDが変わるためインスペクターからもう一度指定し直す必要があります。

Input Action AssetのBinding内容を変更した時

クラスからは内部的にInputActionAsset(ScriptableObject)をインスタンス化し、この中のBinding情報にアクセスするようになっています。

そのため、Input Action Asset側のBindingの内容(Control Pathなど)を変更した場合は、再度リフレッシュするまで古い表示のままになっています。

この場合、ドメインリロードを有効化した状態でプレイ [1] するか、Unityを再起動すれば改善されます。

特に、起動高速化のためにドメインリロードが無効化されている場合は注意が必要です。

さいごに

本記事で紹介したInputBindingReferenceクラスは、Bindingへの参照情報の管理およびインスペクターからのBinding設定を簡単に行える利点があります。

また、スクリプトでフィールドを定義するだけで機能するので扱いも楽になるでしょう。

関連記事

参考サイト

スポンサーリンク