Lisp Game Jam Log #8

Yesterday I got pretty far along with implementing user input. I spent more time on it than I should, but I’m fairly happy with the results. The system is much more complex than other demos, in order to be flexible enough to allow the user to customize their controls.

The way I ended up programming user input has several parts:

  • The event handling system, which passes each event to a series of handler procedures
  • The controls.scm file, which defines the user’s preferred control bindings
  • The user input event handlers, which translate input events into gameplay events
  • The gameplay event handlers, which cause changes to the game state

The event handling system consists of a procedure, handle-event!, which accepts a single event and a list of event handler procedures. Each event handler procedure is a procedure (lambda) which takes the event, inspects it, and decides what to do with it. The event handler procedure may return true or false to indicate whether it “consumed” the event. If the event was consumed, no further handling of that event is performed. If the event was not consumed, the event is passed to the next procedure in the list.

Each event handler is focused on a specific kind of event. For example, there is an event handler which specifically looks for the S key to be pressed while the Ctrl key is being held. If it sees such an event occur, it saves a screenshot to disk, and returns true to indicate that it consumed the event. If it is given an event that it does not care about, it returns false so that the next event handler can try.

The controls.scm file is a list of s-expressions, like so:

(action: p1-up     scancode: w)
(action: p1-left   scancode: a)
(action: p1-right  scancode: d)

This says that when the W key is pressed or released, the p1-up action should occur, with state true or false depending on whether the key was pressed or released. Likewise for the A key triggering p1-left, and the D key triggering p1-right. I am using scancodes (rather than keycodes) because it’s not the key letters that matter, only their physical positions on the keyboard. That means on an AZERTY keyboard, it is actually the Z key which triggers p1-up, and the Q key which triggers p1-left.

The game loads the controls.scm file at startup, and generates an event handler procedure for each s-expression in the file. For example, it generates a handler that looks for the W scancode to be pressed or released. If the handler sees that happen, it calls the process-user-input-action with the symbol p1-up. The process-user-input-action procedure then creates an instance of a custom event type which indicates that player1 started (or stopped, if the key was released) trying to move up.

There are five custom event types related to players, corresponding to the (hypothetical) five possible players: player1, player2, player3, player4, and player5. These types are registered with SDL using register-events!. They are sdl2:user-events, but I created several procedures to make them easier to work with. Aside from having different symbols, the event types are all the same. They hold an action (a symbol), and optionally one or two extra integer values. sdl2:user-event can’t directly store symbols, so I had to do some clever tricks. The action symbol is mapped to an integer using a lookup table, then stored in the event’s code integer field. The two extra integer values are stored in the pointer addresses of the event’s data1 and data2 fields. There are other ways I could have handled this (such as evicting a Scheme object and storing a pointer to it), but this way works well.

There are many player event actions, indicating the various things that can happen to a player, such as trying to move in various directions, jumping, falling, landing on a tile, collecting treasure, and so on. I created an event handler procedure which looks for a player event, finds the player entity that it belongs to, and calls the appropriate procedure, such as player-start-up or player-stop-up.

Those procedures then look at the player’s state, and modify the player accordingly. For example, player-start-up set’s the player’s holding-up? property to true, and if the player is currently standing on the ground, calls player-jump!. player-jump! set’s the player’s in-air? and jumping? properties to true, and gives the player some upward velocity so they jump into the air. It also nudges the player upwards a tiny amount so that they are no longer colliding with the tile they are standing on; otherwise, the collision detection would immediately think that the player had landed.

As I mentioned at the start of the post, I think I spent too much time on this system. It is very flexible, but I should have done something dead simple (like hardcoded key bindings) for the game jam, then made it more flexible later. Oh well, too late now!

I am now working on refining the player movement logic. In particular, the player’s acceleration should change depending on the player’s state. For example, if the player is on the ground and holding left or right, they should accelerate to the left or right to counteract friction. And the friction of the tile that the player is standing on should also affect their acceleration, so that slippery tiles are more challenging to walk on. Players should also probably accelerate a small amount while holding left or right in the air, so that the user has some control while in the air.

There are less than 30 hours left in the jam, so I better get cracking!