New Input System をふわっと理解する　～大八耐2019 in Tokyoを添えて～

# まえがき
　[大八耐2019 in Tokyo](https://daihachitai.connpass.com/event/151038/)に参加したときに気になっていたNew Input Systemを触ってみたのでふわっとまとめたみました。

 *Input System - 1.0.0 時点での記事なので以降のバージョンと差異がある可能性があります。

# Input System のインストール

　Input SystemはPackage Managerを通じてインストールします。
　*2019年11月時点では、まだpreview packageなのでShow preview packagesにチェックを入れないとリストに現れません。
![スクリーンショット 2019-11-12 16.31.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/c51af4c1-690b-5e65-63e0-a326b7f0f50c.png)
　次に、Project Settings > PlayerからActive Input Handlingを設定します。設定を反映させるにはUnity Editor の再起動が必要なので注意しましょう。
![スクリーンショット 2019-11-12 16.40.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/64a1a22e-cf9e-02b0-48fd-8442de4bb18e.png)

# Input Settings を作る
　次は、Project Settings > Input System Package からCreate settings assetを選択し、`InputSystem.inputsettings`を作成します。
![スクリーンショット 2019-11-12 17.37.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/7645b9b4-d214-43d1-59e4-bd908a83cc8c.png)

　作成したInputSystem.inputsettings からOpen Input Setting Windowを選択し、`Supported Devices`に入力を取りたいデバイスを登録します。
![スクリーンショット 2019-11-13 16.17.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/df6b8ff9-8086-2adb-0e7a-1d87f5a4d736.png)

# Input Actionを使ってみる
　Input Actionは、入力があった際のイベントをデリゲートに登録しておくことで動作します。今回は、キーボードのスペースキーとスクリーンのタップと、マウスとタップの座標を取得してみます。
<img width="331" alt="スクリーンショット 2019-11-14 22.24.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/1253982e-2823-1d85-644e-c8e2af61e082.png">

```c#:GettingFromDevice.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class GettingFromDevice : MonoBehaviour
{
    [SerializeField] private InputAction _jumpInput = default;
    [SerializeField] private InputAction _moveInput = default;
    
    private void Awake()
    {
        // A事前にイベントを登録しておく
        _jumpInput.performed += callbackContext =>
        {
            // Buttonの入力はfloat
            var value = callbackContext.ReadValue<float>();
            if (value > 0)
            {
                Debug.Log("On Jump.");
            }
        };

        _moveInput.performed += callbackContext =>
        {
            var value = callbackContext.ReadValue<Vector2>();
            Debug.Log($"position {value.x},{value.y}");
        };
    }

    private void OnEnable()
    {
        // Enable()で有効化しないと動作しない
        _jumpInput.Enable();
        _moveInput.Enable();
    }

    private void OnDestroy()
    {
        _jumpInput.Dispose();
        _moveInput.Dispose();
    }
}
```
　InputActionフィールドを定義し、インスペクタで各InputActionにBindした入力受けたときのイベントをInputAction.performedに記載します。この時InputAction.Enable()で有効かしておかないと動作しないので注意が必要です。
　CallbackContext.ReadValueで受け取る値は、インスペクタ上の各InputActionの歯車ボタン内のAction TypeとControl Typeから設定しています。
 今回の例では、Jump InputはAction TypeはButton、Move InputはAction TypeはValue、Control TypeをVector2としています。

<img width="588" alt="スクリーンショット 2019-11-14 22.32.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/0389d11d-b70d-b0b0-bd4c-37d4357886e7.png">
<img width="582" alt="スクリーンショット 2019-11-14 22.34.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/a3c1a795-9c52-775f-4413-aa3151edcc68.png">

　ActionをButtonにした場合は、Control Typeはfloatになっています。

　キーボードのWASDをなど複数のキーを入力として取りたい場合、Add Bingindではなく`Add 2D Vector Composite`を使います。上下左右のにそれぞれのキーをBind
することでVector2の入力を作ることができます。ただしMouseのPositionとは異なり座標ではなく-1から１の正規化された入力なので併用する場合は工夫が必要です。

<img width="454" alt="スクリーンショット 2019-11-14 22.39.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/a62223d7-9440-344d-afd9-789bfdf0653c.png">

#ActionMapを生成してみる
　先ほどは、InputActionフィールドを用意してデリゲートにイベントを登録しました。今度はActionMapを生成し、自動生成されたコードを継承してコールバックを受け取る方法を試してみます。先ほどと同じくキーボードのスペースキーとスクリーンのタップと、マウスとタップの座標を取得してみます。
<img width="761" alt="スクリーンショット 2019-11-14 22.56.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/9ee44621-81ff-4bea-bdb0-31ec65fe8e8c.png">
　Asset > Create > Input ActionsからSampleControles.inputactionsを生成し、SampleMapsの中に先ほどと同じくMoveActionとJumpActionをを設定しました。次にGeberate C# Classにチェックを入れてApplyすると同じフォルダに同名のクラスが生成されます。今度はこれを動かすコードを書いていきます。

```C#:ActionMapSample.cs
using UnityEngine;
using UnityEngine.InputSystem;

// 生成されたクラスはI[ActionMap]インターフェスを持っているので、これを実装します。
public class ActionMapSample : MonoBehaviour, SampleControls.ISampleMapsActions
{
    private SampleControls.SampleMapsActions _sampleMapsActions = default;

    private void Awake()
    {
        // SampleControlsに登録したActionMapを生成ます。
        _sampleMapsActions = new SampleControls.SampleMapsActions(new SampleControls());
        // SampleControls.ISampleMapsActionsが実装されたクラスをSetCallbacksに指定します。
        _sampleMapsActions.SetCallbacks(this);
    }

    // SampleControls.ISampleMapsActionsによって定義されたMoveActionのコールバック
    public void OnMoveAction(InputAction.CallbackContext callbackContext)
    {
        var value = callbackContext.ReadValue<Vector2>();
        Debug.Log($"position {value.x},{value.y}");
    }

    // SampleControls.ISampleMapsActionsによって定義されたJumpActionのコールバック
    public void OnJumpAction(InputAction.CallbackContext callbackContext)
    {
        var value = callbackContext.ReadValue<float>();
        if (value > 0)
        {
            Debug.Log("On Jump.");
        }
    }

    private void OnEnable()
    {
        // 忘れずEnabl(）
        _sampleMapsActions.Enable();
    }

    private void OnDestroy()
    {
        // こっちはDisposeではなくDisable(）
        _sampleMapsActions.Disable();
    }
}
```
　自動生成されたクラスはファイル名.I[ActionMap名]のインターフェースを持っています。これを実装したクラスをSetCallbacksで指定することで入力を取得できるようになります。この時、ReadValueで取得する型とControl Typeが一致していないとエラーになってしまいます。


# 入力の検知タイミングを指定する
　これまでJumpActionとしてキーボードやタップを入力にしましたが、現在の状態では押した時、離した時の両方で入力を受け取ってしまいます。これを回避するためにInteractionsを設定する必要があります。
<img width="654" alt="スクリーンショット 2019-11-14 23.39.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/ab8b279b-a996-caf2-3fa9-12c93c13a8a2.png">
　上記のようにJumpActionのInteractionsにPressを、そしてTrigger Behavior
nにPress Onlyを設定しました。これによってJumpActionはButtonのPressのみを検知するようになりました。今回はJumpActionに対して設定しましたが、SpaceキーやTouchそれぞれ個別に設定することも可能です。

　*ここまで実装した方は気づいたかもしれませんがNew Input Systemは１フレームに複数回の入力を受け取ることができます。フレームに依存しないので処理落ち処理落ちした場合でも入力を受け取ることができますが、同じ処理を複数回しないために工夫する必要があります。

#大八耐2019 in Tokyo
　大八耐2019ではパズルのピースを使ったテトリスっぽいものを~~作ってみました~~(完成しませんでした)。ピースを回転させて横一列に並べるというミニゲームで、New Input Systemを使ってキーボード入力からピースを左右に回転させました。
<img width="256" alt="スクリーンショット 2019-11-14 23.53.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527414/df20c882-b5fc-5fe9-b877-379750bd3476.png">

```C#:SpinInputView.cs
using System;
using papicra.Scripts.Presentation.Presenter;
using UniRx;
using UnityEngine;
using UnityEngine.InputSystem;

namespace papicra.Scripts.Presentation.View
{
    public class SpinInputView : MonoBehaviour, PieceRollControls.IPieceRollActionActions, ISpinInputPort
    {
        private PieceRollControls.PieceRollActionActions _input = default;

        private readonly Subject<Unit> _spinClockwise = new Subject<Unit>();
        private readonly Subject<Unit> _spinUnClockwise = new Subject<Unit>();

        public IObservable<Unit> OnSpinClockwise() =>
            _spinClockwise.Publish().RefCount().ThrottleFrame(1);

        public IObservable<Unit> OnUnSpinClockwise() =>
            _spinUnClockwise.Publish().RefCount().ThrottleFrame(1);

        private void Awake()
        {
            _input = new PieceRollControls.PieceRollActionActions(new PieceRollControls());
            _input.SetCallbacks(this);
        }

        public void OnClockwise(InputAction.CallbackContext context)
        {
            if (context.ReadValue<float>() > 0)
            {
                _spinClockwise.OnNext(Unit.Default);
            }
        }

        public void OnUnClockwise(InputAction.CallbackContext context)
        {
            if (context.ReadValue<float>() > 0)
            {
                _spinUnClockwise.OnNext(Unit.Default);
            }
        }

        private void OnEnable()
        {
            _input.Enable();
        }

        private void OnDestroy()
        {
            _spinClockwise?.OnCompleted();
            _spinClockwise?.Dispose();
            _spinUnClockwise?.OnCompleted();
            _spinUnClockwise?.Dispose();
            _input.Disable();
        }
    }
}
```
　その時に使用したコードがこちらです。[テラシュールブログ様](http://tsubakit1.hateblo.jp/entry/2019/01/09/001510) *2 を参考に結構さっくりと実装ができました(inputsettingsの項目に気がつかなくて入力が取れず沼ったのは秘密)。今回紹介したInputActionフィールドを定義してデリゲートにイベントを登録する方法、ActionMapをつかったコールバックを取る方法、ここでは紹介していないPlayerInputを使う方法など色々ありますが、個人的にはコールバックを取る方法が一番やりやすいなぁと感じました。インターフェースあると実装漏れないし楽でした。
　大八耐の時間内でゲームを完成させることはできませんでしたがNew Input Systemを触るいい機会になったので個人的には大満足です。ゲーム自体はちまちま作っていこうかな。

#あとがき
　大八耐2019 in TokyoにてNew Input Systemを軽く触ってみたのでまとめてみました。BindingsやProcessorsなどなど調べ切れていないことが多々あるのでまたいずれ......。
　内容に誤りがありましたら、@sai_maple_にご連絡いただけると幸いです。


# 参考

*1 [Input System documentation](https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/index.html)
*2 [テラシュールブログ](http://tsubakit1.hateblo.jp/entry/2019/01/09/001510)