Page thumbnail

Unityの新しいInput Systemではフレームに関係なく入力を取れる

Unityのチュートリアルで大体出てくる入力判定には「フレームに強く結びつく」という制限がある。

次のコードを見てほしい。おそらく、Unityを学び始めた時にはこのようなコードを書いたことだろう。

using UnityEngine;
public class Sample: MonoBehaviour {
  private void Update() {
    if (Input.GetKeyDown(KeyCode.Space)) {
        Debug.Log("Space pressed now");
    }
  }
}

このコードには重要な制限がある。Update() が実行される時にしかキー入力が判定できない、ということである。
Update()はフレームごとに呼ばれるから、大概の場合は1秒あたり60回キー入力を判定していることになる。
そして多くの場合はそれで十分、つまりプレイヤーに不利になることは一切ないため、この制限はあまり気にされないようである。

しかし、もし不利になるケースがあったらどうだろうか?

実はそんなケースは存在するのである。

  • ゲームの中でマウスの動きが重要なのに、フレームレートが落ちるとマウスの動きの検出頻度も落ちてしまってガタガタの線になる場合
  • フレームとフレームの間のどのタイミングで押されたかまで知らなければならないほど、ゲームにおいてタイミングが重要である場合

新しいInput Systemでは、これを解決する仕組みがある。


InputActionTrace

(Input System自体のセットアップに関しては今回は省略する。)

新しいInput SystemにはInputActionTraceという強力なクラスが存在し、このように使うとフレームに関係なく入力を検出できる。

  1. InputActionTrace を作る
  2. 監視したい入力に対応する InputAction.SubscribeTo() する
  3. フレーム描画のタイミングになったら (i.e., Update() 内)、InputActionTraceをそのまま foreach して、検出した入力を処理する
  4. .Clear() でそれまでの情報をクリアする
  5. 必要無くなったら、.UnsubscribeFromAll().Dispose() を実行して片付ける。
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Utilities;

public class Sample: MonoBehaviour {
  [SerializeField] private InputActionAsset asset;
  private InputActionTrace _trace;
  private void Start() {
    _trace = new InputActionTrace();
    _trace.SubscribeTo(
      asset.FindActionMap("Default").FindAction("Action", true)
    );

    // Polling Frequencyを1000HzにしてInput Systemにできるだけ入力を検出させる
    // 注: デバイスの種類によってはそもそも60Hz以上の入力監視ができないことがあるので注意
    InputSystem.pollingFrequency = 1000;
  }

  private void Update() {
    foreach (var action in _trace) {
        var val = action.ReadValue<float>();
        // ここで色々やる
    }
  }

  private void OnDestroy() {
    _trace.UnsubscribeFromAll();
    _trace.Dispose();
  }
}

このように書いておくと、最後のフレームから今のフレームまでに起こった入力が _trace に 保存されるようになる。 また、入力そのもの以外の便利情報も入っていたりする。

.ReadValue<T>()

_traceの中身は InputActionで直接入力監視するのと同じように扱えるので、 単純にReadValue<T>() で情報を取ることができる。

foreach (var action in _trace) {
  var val = action.ReadValue<float>();
}

直接監視と違うのは、フレームの間で起こったこと全てが記録されているので、 例えばゲームが5FPSでしか動いていなかったとしても、どのようにマウスが動いたかが正確に検出できる。

.time プロパティ

入力情報にはタイムスタンプまで用意されている。 これと Time.realtimeSinceStartupAsDouble との差を取ると (基準となる時間が同じなので、) 何秒前に入力があったかを検出できる。

foreach (var action in _trace) {
  var diff = Time.realTimeSinceStartUpAsDouble - action.time;
  Debug.Log($"This action has occurred {diff} seconds before this frame.");
}

サンプル

.time プロパティの利用の仕方についてサンプルを作ってみた。

clpsplug/inputmanager on GitHub

ちなみにこのプロジェクトはUPMを通して利用できる「できるだけUnityエディタ上で設定し直すハメにならない入力監視プラグイン」として 開発中である。stableではないが、興味があれば試していただきたい。

FrameUnlockedSampleSceneでは、スペースキーを押すと「いつキーが押されたか、それはこのフレームの何秒前か」 という情報が表示されるようにしている。 Game ウィンドウのVSync設定を切り替えることで、「何秒前」の情報がどのように変化するか確認してほしい。

VSyncなし

VSyncしないのでFPSは1,000を超えている。従って、ほとんどの場合「このフレームの0.01秒前」みたいな数字は出ないことが確認できる。

Image from Gyazo

VSyncあり

VSyncしているので、この環境では60FPSとなる。差分の時間が最大でも0.016秒程度になっていることが確認できる。

Image from Gyazo

ちなみに

foreachで呼ばれたInputActionTrace.GetEnumerator()にて少しGC Allocが出ているようで、定期的にGCが発生してしまうようだ。 ただ、GCによってフレームが落ちてもその間の入力は監視できる (そのためのTrace) ので、そんなに問題にはならないかもしれない。

また、プロパティInputSystem.pollingFrequencyをどこかで変更し、 デフォルトの設定である 60Hz で入力状態を監視する状態から変更しておくとよい。

ただし、デバイスごとにこの設定に反応するかしないかが違う可能性もあるので注意が必要である。 ドキュメントによれば、「ポーリング」によって入力監視されているデバイスのみ、実際に入力検出間隔が変更される。 それ以外のものは、どうしても60Hzで入力監視されてしまうことに注意しよう。