【Unity】高度なn度押しに対応した素敵なInputManagerを作る

:::note
今回のスクリプトです。
スターつけてくれると喜びます。
https://github.com/konbraphat51/Unity-GoodInputManager
:::

# この記事は
素敵な`InputManager`を書きました。
モチベーションとしては、 
* キー操作の対応を一か所にまとめたい
* エディタ上でキー操作を登録したい
* n度押しに簡単に対応したい
* 関数を呼ぶことでキーを押したことにしたい
 
というのがありました。

### キー操作の対応を一か所にまとめたい
各々のオブジェクトが好き勝手に`Input.GetKey()`をしていたら、どいつがどのキーを読んでいるのかややこしくなるので、一元管理したいですよね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/a6e7a65c-1aca-66ad-7cb4-d3dd4f1a1925.png)
できました。

### エディタ上でキー操作を登録したい
Unityの良いところは非プログラマーも ~~仕事を押し付けられる~~ 編集に参加できるところです。プログラマーだってノーコードで済む作業はノーコードで済ませたいでしょう。
![sasa_20230204_165049.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/65236f3f-a1c5-223a-089d-9d2dafe5a656.gif)
できました。

### n度押しに対応したい
一度押しで歩行、二度押しでダッシュ、のような仕様が欲しいですよね？しかしオブジェクトごとにその処理を書くのはだるい、、、
なんと、こちらの`InputManager`はｎ度押しを検知したら登録したメソッドを呼んでくれます！
![sasa_20230204_165037.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/4a0e6bba-3116-c00c-64ca-472caaf28707.gif)
こちらは「一度押しで歩行、二度押しでダッシュ」を実装した例です。
ここでミソなのが、 **ダッシュする直前に一瞬だけ歩いている** ところです。二度押しされるまで待って遅延させるより、一度押しがあった時点で歩行を開始し、二度押しを検知した時点で走行に切り替えています。天才じゃないですか？（もちろん、入力待ちができるようにもしています）

### 関数を呼ぶことでキーを押したことにしたい
例えばクリック検知とかで`EventTrigger`で呼ばれたものをキー入力扱いにしておきたいことってありますよね。
![sasa_20230204_172716.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/4542ba6c-f8f5-55ab-7054-17daee816a5a.gif)
できました。（これは球へのクリックを「w」入力に変換している例です）

# 実装！
コードが全部で700行ほどあって、丸ごとをここに載せても記事がかさばるだけですので、全コードは[こちらのgithubを参照](https://github.com/konbraphat51/Unity-GoodInputManager)していただくようお願いします。

下記のコード例は、オリジナルのクラスやメソッドをゴリゴリに使っているので使いまわしは難しいです。メソッドやクラスの中身については[githubを参照してください。](https://github.com/konbraphat51/Unity-GoodInputManager)
あくまでも、大筋の流れが参考になるかと思います。

### シングルトンにする
シングルトンにすることで、staticなメソッドの呼び出し（オブジェクトの参照などせずに`InputManager.Push()`を書けるように）しています。
そのシングルトンの処理については、[こちらのTeachさんの記事からお借りしました。](https://qiita.com/Teach/items/c146c7939db7acbd7eee)

### 関数を登録しておいて、キーが押されたらそれを発動する
`UnityEvent`と`UnityAction`型を上手に活用することで実現できます。

引数に`UnityAction`型を指定することで、メソッドをそのまんま渡すことができます。
`UnityEvent`を作っておいて、`UnityEvent.AddListener()`で登録。
`UnityEvent.Invoke()`で発動させることができる、って寸法です。

### エディタ上で編集可能にする
クラスの定義に`[System.Serializable]`をつけることで、クラスをエディタ上で編集できるようにできます。

publicメソッドを登録できるのは、変数の型に`UnityEvent`を指定したからです。

`enum`型を使うことで、プルダウンメニューを作れます。
```C#
[System.Serializable] public class KeyAction
{
    public enum InputType
    {
        GetKey,
        GetKeyDown,
        GetKeyUp
    }

    [Tooltip("Methods called when key pushed")]
    [SerializeField] private UnityEvent _actions;

    [Tooltip("Key name. Google 'Unity Input key name'")]
    [SerializeField] private string _keyName;

    [Tooltip("0:GetKey, 1:GetKeyDown, 2:GetKeyUp")]
    [SerializeField] private InputType _inputType = 0;
}
```

### 高度なn度押し対応
ｎ度押し自体の対応は、シンプルにキーが押されたら、そのキーに対応するタイマーを作って、タイマーが指定時間になったらアクションを開始するという感じです。

「走る直前に歩く」のような二度押しされるまで一度押しのアクションを実行するｎ度押し対応の仕組みはこのような感じです。

`dontWait`が`True`と登録されたｋ度押しのアクションを、ｋ度押しされた瞬間に発動するようにしています。
発動するにはリスト`runningActions`に入れることで発動します。
そして、ｋ＋１度目が押された瞬間に、`runningActions`から削除すれば解決、ってことですね。
```C#
/// <summary>
/// ｎ度押しのｎの部分が増えたか検知する
/// </summary>
private void CheckSeveralPushesIncreases()
{
    //対象のキー全てについて
    foreach (string keyName in severalPushesKeys)
    {
        if (GetKeyDown(keyName))
        {
            //対象のキーが押された！

            //カウンターを増やす
            //対象のタイマーを取ってくる（このタイマーは、ｎ度押し検知の時間計測のために使われる）
            KeyTimer keyTimer = GetTimer(keyName);
            keyTimer.pushedNum++;

            //タイマーをリセット
            keyTimer.timer = 0f;

            ////もし即発動の対象なら
            //対応するアクションを走査              
            foreach (KeyActionSeveralPushes keyAction in severalPushesActions)
            {
                if ((keyAction.keyName == keyName)
                    && (keyAction.pushesNum == keyTimer.pushedNum)
                    && (keyAction.dontWait == true))
                {
                    //見つかった
                    //このリストに追加すれば、アクションが実行される
                    runningActions.Add(keyAction);
                }
            }
        }
    }
}

/// <summary>
/// タイマーの時間が指定時間を超過したキーについて、対応する回数のアクションを実行する
/// </summary>
private void CheckTimers()
{
    List<KeyTimer> _keyTimers
        = new List<KeyTimer>(keyTimers);
    foreach(KeyTimer keyTimer in _keyTimers)
    {
        //タイマーを進める
        keyTimer.timer += Time.deltaTime;

        //超過した
        if (keyTimer.timer >= pushesInterval)
        {
            //同じキーのアクションが既に登録されていたら、削除
            List<KeyActionSeveralPushes> _runningActions
                = new List<KeyActionSeveralPushes>(runningActions);
            foreach (KeyActionSeveralPushes action in _runningActions)
            {
                if (action.keyName == keyTimer.keyName)
                {
                    //発見されたので、削除
                    runningActions.Remove(action);
                }
            }

            ////実行させる
            //対応するアクションを走査
            foreach (KeyActionSeveralPushes action in severalPushesActions)
            {
                if ((action.keyName == keyTimer.keyName)
                    && (action.pushesNum == keyTimer.pushedNum))
                {
                    //見つかった

                    //登録
                    runningActions.Add(action);
                }
            }

            //タイマーを削除
            keyTimers.Remove(keyTimer);
        }
    }
}

/// <summary>
/// 実際に実行する
/// </summary>
private void RunSeveralPushesActions()
{
    List<KeyActionSeveralPushes> _runningActions
        = new List<KeyActionSeveralPushes>(runningActions);
    foreach(KeyActionSeveralPushes action in _runningActions)
    {
        switch (action.inputType)
        {
            case KeyAction.InputType.GetKey:
                //押下している間
                if (GetKey(action.keyName))
                {
                    action.Invoke();
                }
                else
                {
                    //not push => delete
                    runningActions.Remove(action);
                }
                break;

            case KeyAction.InputType.GetKeyDown:
                //押下した瞬間
                action.Invoke();
                runningActions.Remove(action);
                break;

            case KeyAction.InputType.GetKeyUp:
                //押下解除の瞬間
                //not using GetKeyUp because of possibility of
                //key up during waiting for keyTimer
                if (GetKeyUp(action.keyName))
                {
                    action.Invoke();
                    runningActions.Remove(action);
                }
                break;
        }
    }
}
```

# 最後に
いいね大好きです。ぜひこの記事にいいねしてください。