DEV Community

Cover image for How the Unity's new Input System liberates the input detection from frame-rate
C. Plug
C. Plug

Posted on • Edited on • Originally published at clpsplug.com

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");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
    // But keep in mind that NOT ALL DEVICES ARE COMPATIBLE.
    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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

...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.");
}
Enter fullscreen mode Exit fullscreen mode

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.)

GitHub logo Clpsplug / InputManager

Wrapper to handle Unity's new Input System's triggers from code. NOTE: It is not production ready yet.

Input Manager

WARNING!: This plugin is not stable yet!!

This plugin for Unity3D is the wrapper for using Unity's New Input Manager programatically (i.e., without using related components.)
With this plugin, you can:

  • Handle key presses as your custom enums
  • Handle key presses in a 'frame-unlocked' manner
  • "Hold frame count" with small effect from framerate fluctuation
  • Rebinding the assigned key (i.e., key config.)
    • Output the custom bindings as serializable dictionary format
    • "Duplicate keys" detection; if rebind causes one key bound to two actions, the plugin will try to swap the binds between them instead

How to use?

Please see the README inside the package.

Acknowledgements

The sample project includes a TextMeshPro-rendered version of M+ Font, which is avaiable under SIL Open Font License 1.1.




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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
coryleach profile image
Cory Leach

Thanks for this post! I'm currently also working on a Rhythm game in Unity and trying to solve this problem. My goal is to have millisecond scoring. I'm wondering if you ever tested this with a keyboard on windows or Mac? So far it looks to me like keyboard on windows even using the InputSystem method you describe isn't affected by the polling rate. If the game is running at 60 fps then 1 frame = 16 ms. Therefore I expect player input delay between 0-16ms assuming I actually achieve a polling rate near 1000. Instead, I get values much lower than that in the range 0-5ms in editor but in builds it is often < 1ms. However if I use a Gamepad I get the expected 0-16ms. I assume Gamepad is polling at 1000hz but keyboard is not. Like you said it seems not all devices support this and keyboard seems to be one of them. Did you happen to find any solution for keyboard input?