What is Input Handling, anyway?
Handling input is the process of taking the player’s key presses and converting them to actions in game. Pygame supports two methods for doing this:
1) Examining the state of the keyboard directly each frame, which I’ll refer to as the key state method.
2) Key events, where the game pumps an event queue that is populated only when keys are pressed and released.
Typically, we will attach a function to a key state or event and it will be called when the key is pressed or held.
For the key state approach, the part of your main loop that deals with input will look something like this:
... keystates = pygame.key.get_pressed() if keystates[pygame.K_UP]: moveUp() if keystates[pygame.K_DOWN]: moveDown() ... continues for each key tested ...
Meanwhile, if you take a key event approach, it will look more like this:
... events = pygame.event.get() for event in events: if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: startMovingUp() elif event.key == pygame.K_DOWN: ... elif event.type == pygame.KEYUP: if event.key == pygame.K_UP: stopMovingUp() elif event.key == pygame.K_DOWN: ... ...
The first obvious difference is that we won’t check the keys we’re watching for in the key event approach until someone actually presses a key to populate the events variable. The second is that we have a different idea about how movement should be handled: On key down, it starts and will likely continue every frame until key up. The key state approach typically moves players directly (I say typically, because we’ll see later on that it’s not always the case), while the key event approach modifies the player’s velocity, letting the game loop sort out actually moving the player. The former (direct movement) is frowned upon for a reason I’ll touch on later.
The Big Problem with Key Events
With that out of the way, I’d like to talk about one of the interesting challenges when implementing a multi-handler system. Suppose a player is moving their ship to the left when he pauses the game (switching the handler). If the player lets go of the left key and unpauses the game, what is their expectation? Is it:
- The ship comes to a standstill after unpausing, or…
- The ship continues moving to the left, with further movements to the left doubling the player’s speed, with right key presses causing the movement to stop while the key is held?
I have a pretty good idea myself about what I’d expect. The latter is a pretty serious bug, and it’s one I’ve encountered over and over again.
The above problems are caused when using key events for actions that require both the key down (begin movement) and key up (end movement) events to be caught in pairs. We have issues when beginning a movement, but then the corresponding end movement event is lost. In some implementations of key events, this can be achieved by losing application focus while the key is held, releasing the key, then giving the application focus again. The end result is that the game believes the key is still held down when it is not! Another example using our handler system: holding right and pausing the game will switch the current handler to the pause screen. Letting go of the key then sends the key up event to the pause screen handler, and not the game screen: The key up is lost. When we return to the game, we’ll see the dreaded behaviour above.
A brief aside to discuss the underlying method of moving a ship around the screen: Something you learn from a few game programming websites is that you should never directly modify a player’s position as a result of input. Instead, input should modify the player’s velocity. That way, because you’ve been a good programmer and decoupled the input and game logic, the game itself will be able to decide if the velocity applied will affect a movement. It shouldn’t if the player is at the bounds of the game area, for instance, but directly moving the player means you lose the information about how the player got out of bounds, so they may not be correctly placed back on the field. So directly modifying position is out. The idea that helps solve this: Key down means you affect the player’s velocity in some way, key up means you reverse that change.
This is a great piece of advice when implementing the movement stuff for your game. A common pattern I’ve seen on game tutorial sites is to add to the player’s velocity on key down events, and to subtract the same value on key up. This unfortunately causes the problems described above if a large number of precautions aren’t taken, including but not limited to:
- Making sure a key up is ignored if we haven’t seen a key down from that key
- Maintaining a list of all held keys when switching between handlers (can be rolled in to the above)
- Making sure any paired events don’t let one half of the pair fire more than once (two key downs in a row, say)
- When switching between handlers, checking every key in the new handler, seeing if it is waiting for an event, and firing it if the key state has changed (not ideal, because this means the player will have to repress the button after unpausing, say)
It’s a lot of messing about. I might add, it’s not wrong either (when you correctly code the safeguards). It can be a pain to code, and the edge cases extend beyond what I’ve outlined above. As a result, I attempt to keep things simple: we should never require an event pair in the first place. Instead, for movement functionality traditionally provided by events, I instead use the key state: A map of each key to a boolean value, either true for pressed or false for released. Every frame I check if the left or right key is pressed, I add to the velocity as normal. However, rather than subtract when the key is lifted, I reset the velocity at the end of the game logic for that frame. Suddenly, all of the above problems disappear: with no events to miss, we can’t get the velocity in a weird state, especially when it is reset at the end of every frame anyway.
This introduces something that some might be considered a bug: Suppose the game runs at 60 FPS and I’m moving to the right. That is to say, I am holding the right key. If I release the key after I’ve moved that frame and before the next frame hold the key again, it would be as though I never let go of the key in the first place. Given that we’re running at 60FPS, that gives you roughly 0.0167 seconds to push it. Most likely, you simply can’t release and re-press that fast. Even if you could, under most scenarios this is not a problem. Indeed, for movement this is the exact same behaviour you’d see with key events: On the second frame you would see a key up and a key down event, with the net result of no change in your velocity. There are conceivably scenarios where this is a problem. Consider you could charge a shot by holding space, and fire by releasing. If the player wants to fire a shot and then begin charging a new one, the key event approach would win out: Space bar comes up, space bar comes down, so you get both events on the queue and they’re processed on frame 2. Under the key state approach, we wouldn’t fire the shot. Again, this is a bug, but we would only see the problem if the player is really, really, really fast. I can’t stress how fast he would have to be! They would have to be faster than this dude. So we sacrifice a small bit of functionality in the case where the player is faster at pushing buttons than the world champion. It’s a sacrifice I’m willing to make.
In actual fact, key events will fall over here for movement too: If you tap the right key and get the key down/key up within 0.0167 seconds, the net result will be no movement, just like the key state approach. Maybe it’s not so bad after all! At least for movement. But what about performance? As it turns out, I did some number-crunching on my desktop computer and came up with the following: It takes one millionth of a second more to watch 10 key states than it does to wait for any number of key events (which takes no time, really, because the event loop isn’t populated until you actually press things). I think that’s acceptable! And even on a non-monster machine, it’s not going to take up a significant amount of time compared to the rendering or actual proper game logic. If you want to compare the approaches, here’s a gist of what I did. You’ll need pygame to run it, of course.
So, we’re trading a millionth of a second and a little bit of velocity resetting logic for the peace-of-mind of not having to deal with the edge cases of the key event approach for paired events. I’d call that a fair trade. Hullet Bells runs on a hybrid system: Key state-based function calls for movement (and later shooting), and key events for rarer events like using a special attack or pausing the game. In fact, the input handler presented in the gist is the same one used in Hullet Bells! Yes, it has some pretty gnarly named functions (what’s all this talk about callbacks? They’re just functions!), but it is basically the complete implementation of the system. One limitation is that I have no support for key combinations (say, calling a function if two keys are pressed, rather than just one), but for Hullet Bells (and many shooting games) this is an acceptable limitation.
The Hullet Bells InputHandler
The goal of the InputHandler was to allow isolation of the input logic between handlers, and to make programming input stuff as painless as possible. As long as you’ve defined the logic you want behind a key, actually attaching it is as easy as making the following calls:
# For key states self.inputHandler.addPerFrameCallback(self._moveUp, pygame.K_UP) # For key events self.inputHandler.addPerFrameCallback(self._startMoveUp, pygame.K_UP, KEYDOWN) self.inputHandler.addPerFrameCallback(self._stopMoveUp, pygame.K_UP, KEYUP)
These cause the event/key to be put in to lists that are evaluated every time inputHandler.update() is called:
events = pygame.event.get() for event in events: if event.type == pygame.KEYDOWN: for keydownCallback in self.keydownCallbacks: if keydownCallback.key == event.key: keydownCallback.callback() elif event.type == pygame.KEYUP: for keyupCallback in self.keyupCallbacks: if keyupCallback.key == event.key: keyupCallback.callback() ... keystates = pygame.key.get_pressed() for perframeCallback in self.perframeCallbacks: if keystates[perframeCallback.key]: perframeCallback.callback()
For the key event stuff, it has just occurred to me that it would be nicer to store all KEYDOWN etc. events in a map of [event -> (key, callback)]. Also, for key states it might be nice to attempt to call all functions related to pressed keys regardless of whether a function exists or not, then we can eliminate the if statement too.. Anyway, those are actions for another time! As it stands, the input handler runs pretty well, so I’m not exactly looking to optimise yet. For the complete code, you can check out the Github or the above gist.
This was far too long, and didn’t really go anywhere. But that pretty much covers everything I wanted to talk about: Input handling in general, some of the problems you might have, and finally, my own implementation using the pygame library. I’d love to say the next post will be about the scripting system, but I’ll have to finish that before I can write about it. So if I want to write a post before then, I’ll talk about the game handler itself as it stands.