テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】低フレームレートでも、きれいな入力を受け取りたい

InputManagerでは入力情報はフレームレートに強く依存しており、その中間で取得する情報の大抵は破棄されていました。
InputSystemでは中間の情報をバッファとして確保・使用できるようになったので、それを使用してフレームが低くてもキレイな線を引ける方法を考えてみます。

なお動作はOSの動作に強く依存します。

動作環境:Unity 2018.3 b12、 Input System 1.0 preview

実際の動作

まず普通に低フレームレート環境下でInputManagerを使用してマウスの位置を追跡して線を引くようなコードを実装した場合、下のような形になります。
動作では、マウスは滑らかに円を描く形で動いていますが、線は非常に角張った形で描画されています。これはマウスの座標を取得する間隔が広いために起こります。

https://user-images.githubusercontent.com/1644563/69897856-96194400-1349-11ea-8975-ecfc786afe51.gif

InputSystemを使用した場合はコチラ。こちらもフレームレートを落としていますが入力情報はキレイに補完されており、ちゃんと曲線を描けている事が確認出来ます。1フレームに複数回の入力を受け取れるという認識が一番認識しやすい概念です。

https://user-images.githubusercontent.com/1644563/69897857-974a7100-1349-11ea-8a7d-3725403598e5.gif

コード全文

DrawLine.cs · GitHub

InputManagerの場合

まず最初に、InputManagerを使用したコードです。普通に位置情報を取得し、描画を依頼するコンポーネントに情報を渡します。細かいコードは上の全文を見てください。

// GetInput3.cs
void Update()
{
    var pos = Input.mousePosition;
    line.AddPosition(pos); // 座標を注入
}

InputSystemでマウスの位置を取得する

InputSystemでmousePositionを取得するコードです。これはまだInputManagerと同じような動きをします。

void Update()
{
    var pos = Mouse.current.position.ReadValue(); // マウスの位置を取得
    line.AddPosition(pos);
}

InputSystemでバッファを使用して取得する

バッファを使用して取得します。ここでは InputStateHistoryを使用します。
このアプローチでは中間バッファを補完しているので、滑らかな線が引けます。

なおバッファはnewした地点で確保されるので、自分で開放を行わないとメモリリークを起こします。

InputStateHistory history;

void Awake()
{
    history = new InputStateHistory<Vector2>(Mouse.current.position); //マウスの位置を観測
}

void OnDestroy()
{
    history.Dispose(); // 無効もしくは破棄のタイミングで必ず開放
}

void OnEnable() => history.StartRecording(); // バッファをレコード開始
void OnDisable() => history.StopRecording(); // バッファをレコード終了

void Update()
{
    foreach( var record in history)
    {
        var pos = record.ReadValue<Vector2>();
        line.AddPosition(pos);
    }
    history.Clear(); // 今回の分のバッファは開放
}

InputActionを使用する(ポーリング)

InputActionのポーリングを使用してみます。つまり InputActionTrace を使用します。

事前準備としてInputActionにPosition、そのControlにはMousePositionを入れます。これはTouchを始めとした他のControlでも良いです。
これをPlayerInputに設定しセットアップしてもらい、GetComponent<PlayerInput>().currentActionMap["Position"];でInputActionを取得・使用します。

f:id:tsubaki_t1:20191130171153j:plain

f:id:tsubaki_t1:20191130171814j:plain

private InputAction inputAction;
private InputActionTrace trace;

void Awake()
{
    inputAction = GetComponent<PlayerInput>().currentActionMap["Position"];
    trace = new InputActionTrace();
}

void OnDestroy()
{
    trace.Dispose();
}

void OnEnable() => trace.SubscribeTo(inputAction);   // 観測開始

void OnDisable() => trace.UnsubscribeFrom(inputAction); // 観測終了

void Update()
{
    foreach (var record in trace) // 観測内容から一つずつ取り出して処理
    {
        var v = record.ReadValue<Vector2>();
        line.AddPosition(v);
    }

    trace.Clear();
}

イベント駆動

上のイベント駆動する場合です。Updateを書かなくても良いという反面、1フレームに同じ処理を複数回呼ばれた時の対策が少し面倒といえば面倒かもしれません。

private DrawLine line;
private InputAction inputAction;

void Awake()
{
    inputAction = GetComponent<PlayerInput>().currentActionMap["Position"];
}

void OnEnable() => inputAction.performed += Input_performed;

void OnDisable() => inputAction.performed -= Input_performed;

void Input_performed(InputAction.CallbackContext c)
{
    var v = c.ReadValue<Vector2>();
    line.AddPosition(v);
}

ポーリングの頻度

入力イベントをポーリングするタイプのデバイスの場合、ポーリングの頻度をInputSystem.pollingFrequencyで設定できます。初期値は60Hz(1秒間に60回チェック)で、これを上げたり下げたりすることで入力イベントのサンプリング精度を上げたり出来ます。

ただ上げすぎると(1000とか)エディターごと落ちる事があったので注意が必要かもしれません。また幾つかのデバイスはポーリングする形ではなくOSからの入力を受け取るタイプなので、このAPIは動作しません。