What I wanted to do
In a game, there was a control where pressing the same arrow key twice quickly made the character dash (a short burst of movement) in that direction. If I pressed two keys alternately, it dashed diagonally, but if I pressed two keys together twice, it dashed along only one axis. To the eye it was the same action with a different result, so it looked like a bug.
Here are a few concepts that are good to know in advance.
keydownevent: A signal the browser sends when you press a key. There is one event per key, and there is no concept of "pressed at the same time." Even if you press W and D together, the events arrive separately.- Fixed simulation step: A game runs the loop that draws the screen (
requestAnimationFrame) and the fixed-interval computation that advances the rules by one tick separately. Input events can slip in anywhere between the two.
What I did
The cause was the event order. The previous code committed the direction right away on the event of the first key that made the double-tap valid, and consumed it on the next step. Once the W event reserved the dash first, the D event that followed could not be merged into the direction that was already decided. When pressing alternately, the other key was already held down, so it happened to come out diagonal.
- Open a 50ms collection window on the first key event, and merge the direction by adding only the vertical and horizontal axes that arrive inside the window. The window starts only on the first key and is not extended.
- Consume the dash buffer only once at the simulation step, not in the render loop. If there is no key on the other axis, it runs on the single axis after the window ends.
- When the window loses focus (blur), pauses, or restarts, clear the buffer and the first-tap record.
- Rejected alternatives: committing immediately fails again whenever a frame slips in between the key events of the same chord, and a longer wait increases input latency, so I kept it short.
I verified it with a unit test that inserts a frame between the second keydown events of pressing together. I confirmed that it failed before the fix and passed after it, and I also checked eight combinations in the actual runtime (four diagonals times two key orders) and cancellation during a pause.
What I learned
Don't commit inputs belonging to one chord on the first event. Collect them in a short, bounded collection window, and verify the consumption point and the reset paths separately. → Keyboard chord collection window
The same action is a different path when the order differs. The existing tests only checked the path of holding one key and pressing the other key twice, so they missed this regression.
What I have not confirmed yet: how it feels on a physical keyboard and whether it is reflected in the deployed build. The browser check sent DOM KeyboardEvents into the runtime, and since it is a single experience, there is no basis for generalizing to other input policies or other games.