Unity InputSystemで「押してる間」の処理

# 何の話？
## InputSystemの書き方
InputSystemではキーボードマウス、ゲームパッドその他の入力を管理する方法を提供しています。

その方法とはInputActionに対して、発動条件を設定し、発動条件についての三種類のタイミングで呼び出したい関数を登録するというものです。

三種類のタイミングというのは
・条件を満たし始めた時(started)
・完全に発動したとき(performed)
・条件を満たさなくなったとき(cancelled)

一度登録すればあとは見えない所でやってくれるので、ループ関数内で値をチェックしたり加工したりする事がなくなります。
```c#
void Update(){
    if(keyboard.current.rkey.wasReleasedThisFrame)
        Reload(); 
}
```
銃ゲームでこんなコードがあった場合、メニュー開いてる時とか、どうやって処理を止めようか等を考えると大変です。
```c#
void Start(){
    basicMap = new();
    var action = basicMap.AddAction("リロード");
    action.AddBinding(Keyboard.current.rKey.path);
    action.performed += co => Reload();
}
void OnChangedMenu(){
    basicMap.Disable();
}
```
InputSystemの書き方で行くと結構簡単にまとめられます。

## 最も基本的な使用ケースの一つであるWASDで失敗します
公式には押している間の処理を管理する方法がありません。
[Unityコミュニティ](https://discussions.unity.com/t/new-input-system-how-to-use-the-hold-interaction/726823)で８年間もこの議論が続く程度にみんなが欲しがってる機能です。

この議論の中で「最も基本的な使用ケースの一つであるWASDで失敗します」というコメントが印象に残っています。

翻訳を通して、斜め読みしたからか、なんでこの仕様が支持されているのか分かりませんでした。

擁護派の方は一貫して「Update内に、ボタンを押している間に呼ばれる処理を書けばいいじゃん」としている印象を受けました。

# 押している間の処理だけループ内に書くのキモすぎだろ！！
というわけでこの度、performedとかに登録するのと同じような感じで「ActionのIsPressed()がtureの間」に「Update内で呼ばれ続けるアクション」を登録できる拡張を作ってみました。

登録されたアクションは常にポーリングされるのでパフォーマンス的には宜しくないと思いますが、パフォーマンスを多少犠牲にしても必要な共通化だと私は思います。

## コード
```c#
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;

public static class InputSystemHoldEx
{
    public static List<IHelperData> data2Update = new();
    [RuntimeInitializeOnLoadMethod]
    static void Init()
    {
        var playerLoop = PlayerLoop.GetCurrentPlayerLoop();

        InsertMethod(ref playerLoop, typeof(Update), UpdateInputSystemHoldEx);
        //InsertMethod(ref playerLoop, typeof(FixedUpdate), FixedUpdateInputSystemHoldEx);

        PlayerLoop.SetPlayerLoop(playerLoop);
        void InsertMethod(ref PlayerLoopSystem loop, Type targetType, PlayerLoopSystem.UpdateFunction func)
        {
            for (int i = 0; i < loop.subSystemList.Length; i++)
            {
                if (loop.subSystemList[i].type == targetType)
                {
                    var newList = new PlayerLoopSystem[loop.subSystemList[i].subSystemList.Length + 1];
                    newList[0] = new PlayerLoopSystem
                    {
                        updateDelegate = func
                    };
                    loop.subSystemList[i].subSystemList.CopyTo(newList, 1);
                    loop.subSystemList[i].subSystemList = newList;
                }
            }
        }
    }
    static void UpdateInputSystemHoldEx()
    {
        for (int i = data2Update.Count - 1; 0 <= i; i--) data2Update[i].CallOrRemove();
    }
    public static void SetUWhilePressing<T>(this InputAction action, Action<T> callMethodWhileActionPressing) where T : struct
    {
        new UpdateHelperData<T>(action, callMethodWhileActionPressing);
    }
    public interface IHelperData
    {
        public void CallOrRemove();
    }
    public class UpdateHelperData<T>:IHelperData where T:struct
    {
        WeakReference<InputAction> reference;
        Action<T> callMethodWhileActionPressing;
        public void CallOrRemove()
        {
            if(reference.TryGetTarget(out var inputAction))
            {
                if (inputAction.IsPressed()) 
                    callMethodWhileActionPressing.Invoke(inputAction.ReadValue<T>());
            }
            else
            {
                InputSystemHoldEx.data2Update.Remove(this);
                reference = null;
                callMethodWhileActionPressing = null;
                Debug.Log("InputActionがGCされたので、ヘルパーデータの参照をクリアして、GC対象にしました");
            }
        }
        public UpdateHelperData(InputAction action, Action<T> callMethodWhileActionPressing)
        {
            reference = new WeakReference<InputAction>(action);
            this.callMethodWhileActionPressing = callMethodWhileActionPressing;
            InputSystemHoldEx.data2Update.Add(this);
        }
    }
}
```

起動時にUnityのループ関数を操作して、Updateが呼ばれるタイミングで独自定義のstatic関数が呼ばれるようにしています。

このstatic関数では、監視対象のヘルパーデータ ( InputActionと発動中に呼ぶAction＜T＞ ) のリストの各要素に対して、「もしInputActionがもう存在しなかったら、このリストから消えろ。存在しててかつIsPressed()だったらAction＜T＞を呼んで」という命令を送ります。

InputActionは弱参照で保存されているため、InputActionがDisposeされると自動的にヘルパーデータもGC対象になるように作っています。
1. ActionMapがDisposeされるなどして、InputActionがGC対象になる
2. InputActionがGC
3. 弱参照先にアクセスできないので、ヘルパーデータリストから自発的に消える
4. 唯一の参照先から消えたので、ヘルパーデータがGC対象になる
5. ヘルパーデータがGC
## 使い方
```c#
map = new();
var action = map.AddAction("test");
action.AddBinding(Gamepad.current.leftStick.up.path);

action.performed += cc => Debug.Log("スティックを前に倒し始めた");
action.SetUWhilePressing((float f) => Debug.Log($"前に倒され続けています,倒され具合は{f}"));
action.canceled += cc => Debug.Log("スティックが元の位置に戻りました");
```
performed+=(CallbackContext cc)=>cc.ReadValue<float>();
という風に普通は書きますが、CallbackContextを作成できなかったので泣く泣くReadValue<T>のTを型引数に取っています。
足し算引き算で書くことはできませんが、
## テストコード
```c#
using System;
using UnityEngine;
using UnityEngine.InputSystem;

public class Test : MonoBehaviour
{
    InputActionMap map;
    WeakReference<InputSystemHoldEx.UpdateHelperData<float>> refe;//テスト用のGCされたか監視参照
    
    void Start()
    {
        map = new();
        var action = map.AddAction("w");
        action.AddCompositeBinding("ButtonWithOneModifier")
            .With("Button", "<Keyboard>/w")
            .With("Modifier", Keyboard.current.shiftKey.path);
        action.AddBinding(Gamepad.current.leftStick.up.path);
     // action.SetUWhilePressing((float f) => Debug.Log($"holding,{f}"));
        refe = new(new InputSystemHoldEx.UpdateHelperData<float>(action, (float f) => Debug.Log($"holding,{f}")));
        map.Enable();
        Debug.Log(Keyboard.current.wKey.path);
        Debug.Log(Gamepad.current.leftStick.up.path);
    }

    void Update()
    {
        if (Keyboard.current.dKey.wasReleasedThisFrame)
        { 
            Debug.Log("inputActionをGC対象にします");
            map?.Dispose(); 
            map = null;
        }
        if (Keyboard.current.gKey.wasReleasedThisFrame)
        { 
            Debug.Log("gc"); 
            GC.Collect(); 
        }
        if (!refe.TryGetTarget(out _)) Debug.Log("ヘルパーもGC済み");
    }
}
```
```c#
// action.SetUWhilePressing((float f) => Debug.Log($"holding,{f}"));
refe = new(new InputSystemHoldEx.UpdateHelperData<float>(action, (float f) => Debug.Log($"holding,{f}")));
```
この部分ですが、通常コメントアウトしたほうを使用するのですが、弱参照を利用して自動的にヘルパーデータがGC対象になるかを監視するためにコンストラクタを書いています。どちらも同じ意味になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286137/bdde0b1f-1a71-4c53-a5b8-5edc3628fe30.png)
このコードを適当なゲームオブジェクトにアタッチすると、shiftを押したままwキーを押している間だけ、またはゲームパッドの左スティックを前に倒している間、ずっとLogが発生します。

そしてdキーを押すと、InputActionMapがDisposeされて、入力が効かなくなります。

自分の環境だと何もせずに20秒くらいで自動的にGCが発生して、「InputActionがGCされたので、ヘルパーデータの参照をクリアして、GC対象にしました」とLog

さらに20秒くらいで「ヘルパーもGC済み」と流れ始めます。

なお、20秒待てない場合はgキーを押すことで即座にGCを起こせます。

InputActionがGCの対象になったはずの時点から、ヘルパーがInputActionのGCを確認するまでに必ずラグができるんですけどなぜでしょうかね。ヘルパーがGC対象になってからＧＣを起こせば確実にヘルパーを抹殺できるんですけどね