Saturday, January 16

Decorate Thy Keyboard Controls

As part of a game engine called Grease I'm working on (more on that sometime soon) I was thinking about ways to create a clean and efficient api for handling keyboard events. Too often this ubiquitous section of game code consists of a tangle of if/elif statements, a construct that is a personal pet peeve of mine. So to avoid that mess, a typical strategy I've employed is to map keys to methods using a dictionary for easy dispatch. Doing this for one-off applications is simple enough, if not particularly clean and tidy.

Anyway taking a step back for a sec, let's examine some goals. Basically what I'm after is a way to define some methods (or even functions) that get executed in response to key events. Specifically there are three types of key events that I'm interested in: key press, key release and key hold. The first two get dispatched once per key "stroke" as you'd expect. The key hold event fires every game "tick" that a key remains down; useful for continuous functions like thrust, etc.

So I need a clean way to map methods to specific keys and key event types. Sounds like a job for: decorators! Truth be told I haven't felt the need to write many decorators, but after prototyping a non-decorator version that came out less than clean even though it only supported one type of key event, I thought I'd give decorators a go. Below is an example of how to implement key controls using what I've cooked up:

class PlayerControls(KeyControls):

    @KeyControls.key_press(key.LEFT)
    def start_turn_left(self):
        ship.rotation = -ship.turn
    
    @KeyControls.key_release(key.LEFT)
    def stop_turn_left(self):
        if ship.rotation < 0:
            ship.rotation = 0

    @KeyControls.key_press(key.RIGHT)
    def start_turn_right(self):
        ship.rotation = ship.turn
    
    @KeyControls.key_release(key.RIGHT)
    def stop_turn_right(self):
        if ship.rotation > 0:
            ship.rotation = 0

    @KeyControls.key_hold(key.UP)
    def thrust(self, dt):
        ship.body.apply_local_force(
            ship.player.thrust * dt)
    
    @KeyControls.key_press(key.P)
    def pause(self, dt):
        global paused
        paused = not paused
Here's the code needed to wire this into pyglet:

window = pyglet.window.Window()
controls = KeyControls(window)
pyglet.clock.schedule_interval(controls.run, 1.0/60.0)
pyglet.app.run()
Though using the decorators binds the keys to specific methods of the class at compile-time, KeyControls also contains additional methods for changing the key bindings at run-time. I may also add support to load and store key bindings from a configuration file if that feature is needed.

This implementation is designed to work with pyglet, though I think the same general approach could be used with pygame. The module for this, although part of a larger project I am working on, doesn't depend on anything else. Feel free to give it a try and see what you think.

[Edit a new version of this module is now available]

Get the module here.

5 Comments:

Blogger Aigars Mahinovs said...

Isn't 'new' module being dropped for 3.0?

4:04 AM  
Blogger casey said...

Yeah, looks like it. Py 3.x uptake for game dev is pretty much nil afaik though and at least for now Grease will be written for Python 2.x.

I can think of ways to work around this, but if Py 3.x lacks a way to bind methods to instances at run-time that's pretty lame IMO. I suspect they just hid it somewhere else though.

9:00 AM  
Anonymous Anonymous said...

What about 'types' module?

2:25 AM  
Blogger casey said...

Yeah, although it's not very clear from the docs how one instantiates types.MethodType objects. This method binding probably should just be part of a metaclass anyhow, then I think I get the binding stuff for free.

8:43 AM  
Anonymous Foone said...

Awesome! I'd made a stab at implementing something very much like this for PyGame, it didn't get very far.

I'mma play with this as soon as I get home from work, this'll be perfect for my next game.

7:51 AM  

Post a Comment

<< Home