見出し画像

【unity 】InputSystemを使ったキーコンフィグ(キーリバインド)の実装

InputSystemを使ったキーコンフィグを実装したのですが、めちゃくちゃ大変だったので備忘録がてらまとめ。

基本的にこれらのサイトの再現なのですが、そのまま流用しただけだと何故か上手くいかない点があったのでいくつくかアレンジしています。

※プログラミングを始めて一年強の独学素人なので、非効率的なやり方をしてるかもしれませんがご了承ください。

Unity 2020.3.31fを使用してます。

■ゴール

こんな感じのキーボードとゲームパッドのキーコンフィグを実装します。
Moveはキーボードでは2DVectorの各軸で、ゲームパッドでは左右のスティックかD-Padを選択する形にしています。

■導入、前準備

InputSystemの導入や、使い方については他の方がたくさんまとめられているのでそれらを参考にしてください。
自分もイマイチ理解できてない点が多々ありますので。

とりあえず自分はBehaviorをInvoke Unity Event、Player Inputコンポーネントを必要なgameobjectにつけ、インスペクター上からアクションの関数を指定するやり方でやってます。

PlayerInputの設定

ActionMapはプレイヤー用の"Player"、UI用の"'UI"、そしてキーコンフィグ時に必要となる何のキーバインドも設定していない"Blank"の3つを用意しました。

Action Map UI

またカーソル操作等のスクリプトは各々で準備してください。自分はSelectableが上手く扱えなかったので力技でやってます。

■コード

本筋に関係ない所もありますがまずはクラスの全容を貼ります。

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using TMPro;
using System.Collections;
using System.Collections.Generic;


public class RebindingInput : MonoBehaviour
{
    //PlayerInputコンポーネントがあるゲームオブジェクト
    [SerializeField]
    private PlayerInput _pInputPlayer;
    [SerializeField]
    private PlayerInput _pInputUI;

    List<InputAction> actionList;

    //リバインディングしたいInputAction項目。
    InputAction attackAction;
    InputAction jumpAction;
    InputAction skillAction;
    InputAction changeLAction;
    InputAction changeRAction;
    InputAction moveAction;

    InputAction submitAction;
    InputAction cancelAction;
    InputAction pauseAction;

    //リバインディングテキスト。アクションと同じ順番。Defaultは初期設定
    [SerializeField] List<TextMeshProUGUI> keyboardTexts;
    [SerializeField] List<string> keyboardDefaultTexts;

    [SerializeField] List<TextMeshProUGUI> gamepadTexts;
    [SerializeField] List<string> gamepadDefaultTexts;
    int textIndex = 0;



    private InputActionRebindingExtensions.RebindingOperation _rebindingOperation;

    //途中でゲームパッドを挿したとき用
    bool gamepadCompositeComplete = false;

    public void Start()
    {
        attackAction = _pInputPlayer.actions["Attack"];
        jumpAction = _pInputPlayer.actions["Jump"];
        skillAction = _pInputPlayer.actions["Skill"];
        changeLAction = _pInputPlayer.actions["SkillChangeL"];
        changeRAction = _pInputPlayer.actions["SkillChangeR"];
        moveAction = _pInputPlayer.actions["Move"];

        submitAction = _pInputUI.actions["Submit"];
        cancelAction = _pInputUI.actions["Cancel"];
        pauseAction = _pInputUI.actions["Pause"];

        actionList = new List<InputAction>();

        actionList.Add(attackAction);
        actionList.Add(jumpAction);
        actionList.Add(skillAction);
        actionList.Add(changeLAction);
        actionList.Add(changeRAction);
        actionList.Add(moveAction);
        actionList.Add(moveAction);
        actionList.Add(moveAction);
        actionList.Add(moveAction);
        actionList.Add(submitAction);
        actionList.Add(cancelAction);
        actionList.Add(pauseAction);
        Debug.Log(actionList.Count);

        if (Gamepad.current == null)
        {
            gamepadCompositeComplete = false;
        }

        //DefaultTextの保存
        keyboardDefaultTexts = new List<string>();
        gamepadDefaultTexts = new List<string>();
        foreach (var text in keyboardTexts)
        {
            keyboardDefaultTexts.Add(text.text);
        }

        foreach (var text in gamepadTexts)
        {
            gamepadDefaultTexts.Add(text.text);
        }


        //すでにリバインディングしたことがある場合はシーン読み込み時に変更。
        LoadBindData();

    }


    private void Update()
    {
        //やってはみたけど途中からの接続じゃキーバインドできなかった
        if(Gamepad.current != null && !gamepadCompositeComplete && (_pInputPlayer.currentActionMap.name =="Player" ) && (_pInputUI.currentActionMap.name == "UI"))
        {
            Debug.Log("ゲームパッドLoading");
            LoadBindData();
            gamepadCompositeComplete = true;
        }
        else if(Gamepad.current == null && gamepadCompositeComplete)
        {
            Debug.Log("ゲームパッドdisconect");
            GamePadUnComposite();
            gamepadCompositeComplete = false;
        }

        
    }



    public void StartRebinding(int index)
    {
        //Submitアクションの入力によってキーボードかゲームパッドか判別
        var s = _pInputUI.actions["Submit"].activeControl.path;

        //ボタンの誤作動を防ぐため、何も無いアクションマップに切り替え
        _pInputPlayer.SwitchCurrentActionMap("Blank");
        _pInputUI.SwitchCurrentActionMap("Blank");

        if (s.Contains("Keyboard"))
        {
            Debug.Log("keyboard");
            keyboardTexts[index].color = new Color32(176, 16, 48, 255);
            //Fireボタンのリバインディング開始
            _rebindingOperation = actionList[index].PerformInteractiveRebinding()
                .WithTargetBinding(actionList[index].GetBindingIndexForControl(actionList[index].controls[0]))  //contolsのindexは変数を渡すとエラーになりやすかったので直
                .WithControlsExcluding("Mouse") //.OnMatchWaitForAnother(0.1f)があると決定キー入力を受け付けない時がある
                .OnComplete(operation => RebindComplete(index, 0))
                .Start();
        }
        else
        {
            Debug.Log("GamePad");
            gamepadTexts[index].color = new Color32(176, 16, 48, 255);
            //Fireボタンのリバインディング開始
            _rebindingOperation = actionList[index].PerformInteractiveRebinding()
                .WithTargetBinding(actionList[index].GetBindingIndexForControl(actionList[index].controls[1]))
                .WithControlsExcluding("Mouse")
                .OnComplete(operation => RebindComplete(index, 1))
                .Start();
        }

    }

    public void RebindComplete(int index, int control)
    {
        //アクションのコントロール(バインディングしたコントロール)のインデックスを取得
            

        if(control == 0)
        {
            int bindingIndex = actionList[index].GetBindingIndexForControl(actionList[index].controls[control]);
            //Keyboardの時
            //バインディングしたキーの名称を取得する
            keyboardTexts[index].text = InputControlPath.ToHumanReadableString(
                actionList[index].bindings[bindingIndex].effectivePath,
                InputControlPath.HumanReadableStringOptions.OmitDevice);

            keyboardTexts[index].color = new Color32(255, 255, 255, 255);
        }
        else if(control == 1)
        {
            //GamePad
            int bindingIndex = actionList[index].GetBindingIndexForControl(actionList[index].controls[control]);
            gamepadTexts[index].text = InputControlPath.ToHumanReadableString(
                actionList[index].bindings[bindingIndex].effectivePath,
                InputControlPath.HumanReadableStringOptions.OmitDevice);

            gamepadTexts[index].color = new Color32(255, 255, 255, 255);
        }
        else
        {
            //GamePadのMove用
            //GetBindingIndexForControl(actionList[index].controls[control])はエラーの温床マジで
            //結局アクションコントロールのindexもindexと同じなのでそのまま利用
            gamepadTexts[index].text = InputControlPath.ToHumanReadableString(
                actionList[index].bindings[index].effectivePath,
                InputControlPath.HumanReadableStringOptions.OmitDevice);

            gamepadTexts[index].color = new Color32(255, 255, 255, 255);
        }

        _rebindingOperation.Dispose();


        //リバインディング時は空のアクションマップだったので通常のアクションマップに切り替え
        _pInputPlayer.SwitchCurrentActionMap("Player");
        _pInputUI.SwitchCurrentActionMap("UI");
        //リバインディングしたキーを保存(シーン開始時に読み込むため)
        PlayerPrefs.SetString("rebindSample", _pInputPlayer.actions.SaveBindingOverridesAsJson());
        PlayerPrefs.SetString("rebindUI", _pInputUI.actions.SaveBindingOverridesAsJson());
    }

    //Move用Start。キーボードは2DVectorの各方向。ゲームパッドはスティックのD-Pad選択
    public void StartRebinding(int index,string vector ,string compositePart)
    {
        var s = _pInputUI.actions["Submit"].activeControl.path;

        _pInputPlayer.SwitchCurrentActionMap("Blank");
        _pInputUI.SwitchCurrentActionMap("Blank");

        if (s.Contains("Keyboard"))
        {
            var action = actionList[index];
            //vectorは2DVector、compositePartは各方向名
            int targetIndex = Get2DVectorCompositeBindingIndex(action, vector, compositePart);  //これもtargetIndexを取得しているが2DVectorは[0]が2DVectorで上下左右が[1]~[4]なので直でもいいかも

            Debug.Log("Movekeyboard");
            Debug.Log(targetIndex);
            keyboardTexts[index].color = new Color32(176, 16, 48, 255);
            //キーボードの各方向に対してリバインディング開始
            _rebindingOperation = actionList[index].PerformInteractiveRebinding()
                .WithTargetBinding(targetIndex)
                .WithControlsExcluding("Mouse")
                .OnComplete(operation => RebindComplete(index, vector, compositePart))  //キーボード用のRebindComplete
                .Start();
        }
        else
        {
            if(index == 7 || index == 8)
            {
                _pInputPlayer.SwitchCurrentActionMap("Player");
                _pInputUI.SwitchCurrentActionMap("UI");
                return;
            }
            Debug.Log("GamePad");
            Debug.Log(actionList[index]);
            gamepadTexts[index].color = new Color32(176, 16, 48, 255);
            //Moveアクションのリバインディング開始
            _rebindingOperation = actionList[index].PerformInteractiveRebinding()
                .WithTargetBinding(index)   //actionList[index].GetBindingIndexForControl(actionList[index].controls[index]))では拾えない。やはりエラーの温床
                .WithControlsExcluding("Mouse")
                .OnComplete(operation => RebindComplete(index, index))
                .Start();
        }

    }

    //MoveKeyBoard用
    public void RebindComplete(int index, string vector, string compositePart)
    {
        //アクションのコントロール(バインディングしたコントロール)のインデックスを取得
        var action = actionList[index];
        int targetIndex = Get2DVectorCompositeBindingIndex(action, vector, compositePart);
        //バインディングしたキーの名称を取得する
        keyboardTexts[index].text = InputControlPath.ToHumanReadableString(
            actionList[index].bindings[targetIndex].effectivePath,
            InputControlPath.HumanReadableStringOptions.OmitDevice);    

        keyboardTexts[index].color = new Color32(255, 255, 255, 255);

        _rebindingOperation.Dispose();


        //リバインディング時は空のアクションマップだったので通常のアクションマップに切り替え
        _pInputPlayer.SwitchCurrentActionMap("Player");
        _pInputUI.SwitchCurrentActionMap("UI");
        //リバインディングしたキーを保存(シーン開始時に読み込むため)
        PlayerPrefs.SetString("rebindSample", _pInputPlayer.actions.SaveBindingOverridesAsJson());
    }



    public void DefaultReBind()
    {
        _pInputPlayer.actions.RemoveAllBindingOverrides();
        _pInputUI.actions.RemoveAllBindingOverrides();
        PlayerPrefs.SetString("rebindSample", _pInputPlayer.actions.SaveBindingOverridesAsJson());
        PlayerPrefs.SetString("rebindUI", _pInputUI.actions.SaveBindingOverridesAsJson());

        LoadBindData(); //ロードにTextリセットがあるため
    }

    void GamePadUnComposite()
    {
        Debug.Log("Padが抜けました");
        foreach(var text in gamepadTexts)
        {
            text.color = new Color32(160, 160, 160, 255);
        }
    }

    //なくても良かった
    int Get2DVectorCompositeBindingIndex(InputAction inputAction, string actionName, string bindingName)
    {
        var tmpBindingSyntax = inputAction.ChangeCompositeBinding(actionName);
        Debug.Log(tmpBindingSyntax.bindingIndex);
        tmpBindingSyntax = tmpBindingSyntax.NextPartBinding(bindingName);

        Debug.Log(tmpBindingSyntax);
        return tmpBindingSyntax.bindingIndex;
    }

    private void LoadBindData()
    {
        //最初にDefaultTextをいれる
        foreach (var text in keyboardDefaultTexts)
        {
            int index = keyboardDefaultTexts.IndexOf(text);
            keyboardTexts[index].text = text;
        }
        foreach (var text in gamepadDefaultTexts)
        {
            int index = gamepadDefaultTexts.IndexOf(text);
            gamepadTexts[index].text = text;
        }

        if(Gamepad.current != null)
        {
            foreach (var text in gamepadTexts)
            {
                text.color = new Color32(255, 255, 255, 255);
            }

        }

        string rebinds = PlayerPrefs.GetString("rebindSample");
        string rebindUI = PlayerPrefs.GetString("rebindUI");

        if (!string.IsNullOrEmpty(rebinds) || !string.IsNullOrEmpty("rebindUI"))
        {
            _pInputPlayer.actions.LoadBindingOverridesFromJson(rebinds);
            _pInputUI.actions.LoadBindingOverridesFromJson(rebindUI);

            Debug.Log("RebindDataあり");
            //リバインディング状態をロード  
            foreach (var action in actionList)
            {

                //int bindingIndex = action.GetBindingIndexForControl(action.controls[0]);  2DVector等Composite入力があるとうまくいかない
                keyboardTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                action.bindings[0].effectivePath,
                InputControlPath.HumanReadableStringOptions.OmitDevice);


                //keuboard用ロード
                switch (textIndex)
                {
                    case 5:
                        keyboardTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                        action.bindings[3].effectivePath,   //左
                        InputControlPath.HumanReadableStringOptions.OmitDevice);
                        break;
                    case 6:
                        keyboardTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                        action.bindings[4].effectivePath,   //右
                        InputControlPath.HumanReadableStringOptions.OmitDevice);
                        break;
                    case 7:
                        keyboardTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                        action.bindings[1].effectivePath,   //上
                        InputControlPath.HumanReadableStringOptions.OmitDevice);
                        break;
                    case 8:
                        keyboardTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                        action.bindings[2].effectivePath,   //下
                        InputControlPath.HumanReadableStringOptions.OmitDevice);
                        break;

                }

                if (Gamepad.current != null)
                {
                    Debug.Log("Padあり");
                    gamepadCompositeComplete = true;
                    //bindingIndex = action.GetBindingIndexForControl(action.controls[1]);
                    gamepadTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                    action.bindings[1].effectivePath,
                    InputControlPath.HumanReadableStringOptions.OmitDevice);

                    //Gamepad用ロード
                    switch (textIndex)
                    {
                        case 5:
                            Debug.Log(textIndex);
                            gamepadTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                            action.bindings[5].effectivePath,   //LStick
                            InputControlPath.HumanReadableStringOptions.OmitDevice);
                            break;
                        case 6:
                            Debug.Log(textIndex);
                            gamepadTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                            action.bindings[6].effectivePath,   //D-Pad
                            InputControlPath.HumanReadableStringOptions.OmitDevice);
                            break;
                        case 7:
                            Debug.Log(textIndex);
                            gamepadTexts[textIndex].text = "―";
                            //gamepadTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                            //action.bindings[5].effectivePath,
                            //InputControlPath.HumanReadableStringOptions.OmitDevice);
                            break;
                        case 8:
                            Debug.Log(textIndex);
                            //gamepadTexts[textIndex].text = InputControlPath.ToHumanReadableString(
                            //action.bindings[6].effectivePath,
                            //InputControlPath.HumanReadableStringOptions.OmitDevice);
                            gamepadTexts[textIndex].text = "―";
                            break;

                    }

                }
                textIndex++;
            }
            textIndex = 0;
        }


    }

}


この記事を読んでいる人は上の参考記事を読んでいるだろうと思いますので、リバインドについての詳細な流れは割愛します(というか自分もなんとなくでしかわかっていない)

基本的には動画のように各項目で決定するとそのindexを引数としてStartRebinding()を呼ぶという形になってます。
そしてその引数に応じてActionとTextのListから該当するものを呼び出し、変更するという流れです。

Moveに関してはBindingIndexが他と異なるので特別なStart〜を用意しています。

■変更点の解説

参考資料からの大きな変更点の解説をしていきます。

●変更点①InputActionReference非使用

インスペクター上からのInputActionReference型のアクションの指定をやめています。

どういう訳かInputActionReferenceを使うと上手くリバインド出来ず、ログ上では変わっているのに、実際のキー入力は元のままという現象に遭遇し、なんとか導き出した苦肉の策なのでInputActionReferenceで済めばそっちでも良いと思います。

attackにpathを表示するようにしたら同じアクションに二つのpathが!(kは反応なし)

今思えばこれもverUpで直ったのかも

●変更点②.OnMatchWaitForAnother()非使用

リバインドを開始する際に、決定キーを押してからリバインドする為のキー入力を受け付けるまでのディレイ時間を設定するこの〜関数ですが、使用をやめています。

最初は普通に使っていたのですが、テストプレイ中にキーのリバインド後に続けてもう一度リバインドしようと決定キー(ENTER)を押しても、決定に設定したアクション関数が呼ばれない(2回目の入力からは反応する)という不具合に遭遇しました。(しかも出たり出なかったりまちまち)

色々原因を探ってみてもわからず、この関数を削るとすんなり動いたのでおそらくこいつが何かしらコードとバッティングしてたのかなと思います。
ポーズ中でのコンフィグなのでもしかしたらtimescaleが関係したのかも?

●変更点③.WithCancelingThrough()非使用

公式の説明を見てもイマイチ用途が分からなかった〜ですが、設定したキーを押すとフリーズするので止めました。使用法が間違ってる可能性はあります。

●変更点④BindingIndex取得方法の変更

キーコンフィグの実装におけるエラーの7割を占めたであろうこいつの取得方法を都度変えてます。
詳細は以下にまとめますが、結局(問題ないのであれば)直接指定したほうが早いと思いました。

■注意点の解説

自分が躓いた点を解説していきます。

●BindingIndexについて

今回キーコンフィグを実装した約二週間で一番躓いたであろうBindingIndexの取得についての詳細です。
キーバインドを探しにいくGetBindingIndex()、GetBindingIndexForControl()等の関数なんですが、これが結構落とし穴の多い面倒なシステムでした。

・BindingIndexの注意点1 未接続のPathの扱い
アクションに設定したバインドの中で、今現在接続されていないはもの(ゲームパッド等)に関してはnullとして扱われる為、
例えばキーバインド0にゲームパッドの設定をし、キーバインド1にキーボードの設定をしていた場合、ゲームパッドを接続していないと、キーボードのインデックスは0に繰り上がるようです。
なのでキーボードだからとcontrols[1]の様に指定するとエラーが返ってくる場合があるので注意です。

自分のように、キーボードとゲームパッドの両方のキーコンフィグを扱う場合は、キーボードを[0]にして、ゲームパッドが繋がっているかどうかで処理を分けるのが無難かと思います。

 var s = _pInputUI.actions["Submit"].activeControl.path;

        if (s.Contains("Keyboard")){...

自分はこの記述で分けていますが、キーボードの入力を直接参照する方法がわからなかっただけです。

・BindingIndexの注意点2 InputControlに変数を渡さない
GetBindingForControl(InputControl)の引数にcontrols[index]の様に変数を渡すとたまにエラーを返すことがあります。
これはもう何故かはわかりません。自分のコードとバッティングしてるだかかもしれません。
controls[0]の様にindexの数字を直接打ち込む分には特に問題はなかったので自分はそうしてます。

・BindingIndexの注意点3 groupの指定

.WithBindingGroup("GamePad")     

参考記事の様に、"Gamepad"の所を"Keyboard"に変えてもうまくいきませんでした。
これも自分のやり方がだめな可能性があります。"Keyboard & Mouse"ならうまく行ったのかも?

・BindingIndexの注意点4 そもそも取得する必要がない?
複雑なキー設定でない限りActionMapに設定している順番がそのままBindingIndexになるので取得する必要がない気がします。(勿論綺麗にコードを纏めたい場合は必要だとは思いますが)

Action Map Player

自分のこのActionsの場合Keyboardが[0]でゲームパッドが[1]になります。
2DVectorの含まれるMoveだけ特殊で2DVectorという名前が0、Upから順に[1]〜[4]、Left Stickが[5]、D-Padが[6]となってます。

「BindingIndex取得が厄介だよぅ…」という方は試してみてはいかがでしょうか。

●その他細かな変更点、注意点

①参考記事のコードだとリバインド時のテキストは参照しても、デフォルトに戻した時のテキストは更新されないので手を加えています。

②個人的にも未知数なところなんですが、キーコンフィグ中にゲームパッドが非接続になったりすると、タイミングによってはなんか面倒な事が起きそうな気がして怖いです。
かといって対処法はわからないので、せめて再接続の処理はキーバインド中を避けてます。
しかし現状途中からゲームパッドを接続した場合、リバインドが反応しません(というよりもActionMap UIのキーが反応しない?)
この辺はむしろ誰か分かったら教えてほしいです。

③InputSystem ver1.3ではリバインド中にEキーを押すとフリーズするという不具合があるようです。自分も遭遇しました。
ver1.41では修正されてるようなので遭遇したらverUpをオススメします。

また、リバインド中に特定のキーが反応しなくなるという現象も起きてます。(自分の場合左列のTabやshift等)
これは特に調べてないですがver1.41でも出るので現状諦めてます。今後に期待。

④このやり方だと決定とキャンセルとポーズを同じキーに設定できてしまうので、決定キーと同じキーにリバインドできなくする等の詰み防止が必要になります。
自分は面倒だったのでタイトル画面に戻ればデフォルトにキーコンフィグ出来るようにしてます。

■あとがき

本来であれば自分のような独学の素人よりも相応しい人達が書くべき記事だと思いますが、なかったのでしょうがない。
今後現れる自分のような人のために書いてます。一助となれば幸いです。


もしコードの疑問点、改善点等あればコメントください。

この記事が気に入ったらサポートをしてみませんか?