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!
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.)
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.
判定をちゃんとフレームレートから分離できてるかテストしたかったからとはいうても
— C. Plug@音ゲー作ってます (@CollapsedPlug) December 30, 2022
こんなフレームレートで音楽ゲームをプレーするハメになるのは最後だと思いたい
"5 FPS"とかマジで認識能力バグる#indiedev pic.twitter.com/Xapfb2d9iD