I plan to post an article on the handling of input in a stable manner for fixed-frame-rate games even in high-latency situations soon.
While, of course, the player can never react to something he or she cannot see, if you handle input in such a way that, even if there is a sudden spike in the frame-rate, if the player hits buttons during the lag the end result would be the same as if it had been perfectly smooth the whole time.
This basically facilitates the player’s mental simulation during the laggy period and it is absolutely necessary for smooth “responsive” input, not even just during times of lag but throughout the game. If your input handler is able to allow the player to hit buttons through the lag and end up with the correct result as if the lag had never been there, the player at least has a chance of not dying etc. based on his or her mentally queued commands (we all think ahead and time our jumps, dodges, etc.—small bouts of lag interfere with this by surprising us, but we can still time our jumps etc. for a short while even if we can’t see what is happening).
My article will explain this in detail and it will state close to the start that
inputs are not events.
A naïve implementation simply takes the input events from the OS and immediately applies some action to the game or player. The popular misconception that inputs are events stems from application programming, which is indeed event-based and the most common event is input.
The article will go into detail on this but I will explain the overall system briefly here.
Firstly, you do need to catch inputs as they are given to you by the OS, which means by listening to events. That is as far as events go in an input structure.
The thread that listens for inputs from the OS is its own thread. For Windows, this has to be the main thread—the thread that created the window. That means keep your game and render thread(s) off it. You can’t catch inputs accurately if the thread that listens for inputs is busy drawing a monster or colliding objects together.
The caught input is time-stamped and put into a buffer to be processed by the logical loop (remember, in a fixed time-step the logic is not updated every frame, as apposed to the rendering, and although they happen in the same game loop, we call them the logical loop and render loop—don’t be confused in thinking that they are 2 separate loops running). The time-stamp was synchronized with your logical loop’s timer at the beginning of the game.
For each logical update, request from the input buffer only the amount of input up to the current logical game time.
If you lag for 300 milliseconds and update logic only every 30 milliseconds, when you stop lagging you will basically do 10 logical updates before continuing with the next render.
There are 2 naïve implementations you could accidentally do here:
#1: Pure event-based input. As you get events, you immediately modify the player’s state, sometimes overwriting previous states from previous keystrokes. In this case, after 300 milliseconds, the first logical update (out of 10) will just react based off the last state it sees, ignoring any keypresses that came before and got overwritten. This is obviously wrong.
#2: Buffered input, but consuming the whole buffer on each logical update. The result is exactly the same as #1 in the end.
The correct solution is to only consume from the input buffer as many input events as took place during the logical-update period.
That is, since our logical update is 30 milliseconds, you consume only the next 30 millisecond’s worth of events from the buffered input, leaving later inputs in the buffer for the next logical updates to consume.
As each logical update consumes and processes only the inputs that actually took place at that time during the game simulation, the simulation itself remains stable and faithful to what the player had intended.
The player sees a barrel coming and mentally times his or her jump. Suddenly 1 second of lag.
The player is surprised but had already figured out to hit the button after 0.5 seconds.
0.5 seconds into the lag the player sticks to the plan and waits another 0.5 seconds to see the result.
And, success! The player is impressed with your input mechanism and goes to your site to donate $10,000. If only you had added a “Donate” button.
L. Spiro