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:
- Create
InputActionTrace
-
.SubscribeTo()
theInputAction
you want to check the input outside the confine of the frame rate - Directly
foreach
on theInputActionTrace
-
.Clear()
the trace - 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();
}
}
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
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.
With VSync
Now we're confined to 60fps - we should be seeing values around 16ms at most, which we do!
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.
Top comments (1)
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?