Input polling

2015-03-31

So I'm building a canvas game. Actually I've been working on this thing for most of last year before being side tracked, again, to something else. Ok I digress. In this canvas game there's obviously an fps setting, which determines at how many frames per second (FPS) to run the game. But aside from fps there's also "tps", or how many ticks per second to compute. In this a tick is actually one game cycle. One call to frame(). And of course everything runs under requestAnimationFrame, unless explicitly configured otherwise.

This leads to interesting problems. One of them is input handling. You basically have three "threads" when it comes to reading input. Which one do you use? "Threads?", you ask? Well they're more like points of the program where I could flush the input.

The Input class I've written basically caches all inputs when they happen. You press a button nothing happens in the game, instead a cache is updated to mark that button as pressed and held. Next "input tick" (when we flush the input) the button is no longer pressed but only held. Once you release the button, the cache is updated so the key is logged at released, not pressed nor held. Every input tick the held counters go up so you know how long you've been holding the button.

The same goes for mouse or other ways of input. Don't actually do anything on input, only register the input and wait for an input tick.

So obviously that would be one point to flush the input; as it happens. But that's really just a waste. The input frequency is potentially much higher than the frame rate. Unfortunately this cuts both ways; the (game) tick rate may actually be much much higher than the input frequency. At max, my sloppy overhead-o-rama coded game can do about 15k~25k game ticks per second. Obviously with requestAnimationFrame this still only shows about 30 fps. But there's simply no way you need to read input at 20k/second. There's no point(er).

The main reason is that you won't see the input being reflected on screen. Well at 20k/s you don't really see anything relevant, but even when you're doing two ticks per frame one of them won't be visible. So there's no real point flushing the input for them and act accordingly. At faster rates most of these flushes will be useless anyways as there won't be any new input to act on. Worse yet, it ends up being a bad user experience to do this in invisible frames because your visual cues (that one-frame click hint) could get lost in the hidden frames. Or show up too short.

So I now have a system where there's a game loop and a paint loop and they run completely independent of each other. I could lock them together though. The game loop can run faster than the paint loop or slower. The paint loop runs semi-stable, though due to overhead trouble of inaccurate timers some frames may get lost of the game loop allots too much time for one game loop slice (time between frames to run game cycles).

Since everything is painted in canvas I can't hackily fall back to html for UI. This means doing UI manually as well. Enter the "ui tick". The UI part of the game, like menus or anything you can click on in the game, reads the input cache once per frame, before the first game tick in a slice. It's useless to update the UI more than once per slice since, well, there's absolutely no point to it since it merely serves for visual aid to the user. Updating it on hidden frames is therefor pointless so it happens once per frame, like the input.

Right now I flush the input once before each slice of game cycles, right before the ui tick, so between each frame. This means that the first frame of each cycle gets and handles fresh input and the others don't. That should be fine as long as the tps is locked to the fps. But as it stands it leads to inaccurate clicks, depending on how fast the game runs. Sometimes clicks don't seem to be registered at all. I still have to figure out why exactly that is because I can't match that symptom with the above, yet, because most "clicks" are mostly triggered by mouse ups, and they should properly register and be handled regardless of the speed.

You don't really appreciate the effort that has gone into something until you had to manually do it yourself...