Page thumbnail

How the Unity's new Input System liberates the input detection from frame-rate

Unity’s conventional input system is easy to use, but there is one issue with it - it is frame locked.

Not sure what that means? Let me show you: if you are new to Unity, chances are that you have read or even written this piece of code yourselves.

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

This code has one major restriction - it can detect key presses only when the Update() is called.
Update() is called every frame, so as you may usually enable VSync and/or with everything going on with your actual game, you would be only checking if the key is pressed around 60 times per a second.
This is actually fine for most of the use cases - after all, it may be sufficient for you to check the key input every frame and there will be no disadvantages to the player.

But what if there was one?

There actually are several use cases.

  • You need to detect the mouse movement as a part of the player’s action set, but any frame rate drop makes the movement jaggy, which you don’t want.
  • The precise timing of the input is required to the point that you want to know when within the interval between the frames the key was pressed. (e.g., music games should be doing this or must find a workaround such that the judgment is bound to the frames.)

It’s possible with the new input system!


Introduce InputActionTrace

(Disclaimer: I’m skipping how we migrate to the conventional Input System to the new one - look it up)

With the new Input System, we have a powerful class called InputActionTrace in our arsenal. You will be using this class as follows:

  1. Create InputActionTrace
  2. .SubscribeTo() the InputAction you want to check the input outside the confine of the frame rate
  3. Directly foreach on the InputActionTrace
  4. .Clear() the trace
  5. When done, .UnsubscribeFromAll() and .Dispose() of the trace.
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)
    );

    // Important - required to open up the potential of your input devices!
    InputSystem.pollingFrequency = 1000;
  }

  private void Update() {
    foreach (var action in _trace) {
        var val = action.ReadValue<float>();
        // This is where you do stuff
    }
  }

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

For each frame, your _trace will contain everything occurred from the last frame to the current one; and it contains useful information!

.ReadValue<T>()

The value returned from the iterator can be used just like the regular InputAction object, for the most part. So you can ReadValue<T>() off of it like nothing has changed:

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

…except that now you have every event happened from the last frame!
If you are drawing a line, then this will help you draw a smooth line even if your game runs at 5fps or something!

.time property

This is the great property to make use of - this is the timestamp of the action occurred! Combined with Time.realtimeSinceStartupAsDouble (as it shares the epoch with this property,) you can tell how many seconds before the frame the action occurred!

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

Sample

I have been making an input manager for my games, and with this example, I show the concept of using the .time property to retrieve the timestamp for the action precisely - take a look! (I implement the input detection from the code side as much as possible, so there’s stuff that is a bit off from many tutorials out there.)

clpsplug/inputmanager on GitHub

In the FrameUnlockedSampleScene I prepared several labels that displays the data for the action - press the Space key to check the output, and also toggle VSync in the Game window.

Without VSync

Note the FPS - it’s over 1000 so we won’t be seeing values bigger than like 10ms difference for the most part.

Image from Gyazo

With VSync

Now we’re confined to 60fps - we should be seeing values around 16ms at most, which we do!

Image from Gyazo

Notes

It seems there is a small GC allocation at the foreach section - it is small, but you should know that GC will occur periodically. The trace can still pick up the input actions missed during the dropped frames, so it shouldn’t be too much of a problem, though.

Another point to make is that although this line:

    InputSystem.pollingFrequency = 1000;

causes the polling frequency to be 1000Hz, not all devices are compatible with this settings.
Devices that are “polled” will be affected by this line. Although I don’t have Windows PC to test, I heard that mice on Windows are most likely compatible with this. Keyboards may also be.
If you set this to high number and you still don’t get as much events as you hoped, then the device in question may not be getting polled (and instead, inputs are received from OS’s API or something.)
It’s still a good idea, though, because unexpected frame loss (= Update() failing to fire) can happen. In the example below, I’m playing my own music game, but I set Application.targetFrameRate = 5;. Notice that though this is a game that requires strict timing, I can keep getting “Perfect”s.