Keyboard Controller


September 26th, 2024

Hello! Today I'll be covering my Keyboard Controller, a class that sends messages to classes that consume input.

The Problem

Below is the player's movement. The way I've been handling input was through two switch statements, adding the direction when a key is pressed and setting it to 0 when the button is released:

The switch statement only allows one input to be read at a time. This makes moving diagonally in the game look awkward, so we're going to rework this. 

Supporting Classes

ListenerConditions

There are a few supporting classes I'll need to introduce first, starting with ListenerConditions:

This struct will be set by the user when adding a listener. By default, we assume we're listening for all kinds of input. 

There are certain conditions where we'd want to ignore input. For example, by default, WASD controls the player. But when we pause the game, we want to navigate the pause menu with WASD, not control the player. To do this, we'd just change the ListenerConditions when the game is paused. 

BaseEntity

I'll introduce a new class, BaseEntity. It's simply an Entity that has functions for reading input. Every Entity will derive from this class: 

We can read from the inputArray to determine if a key was pressed or not. The AddToKeyboardController function is a wrapper function since AddListener the function call is kind of long. Each child class will override HandleInput to define its reactions to input.

ListenerData

The Keyboard Controller will have an array of this struct.

System

A system represents an object that watches other Entities. The Refresh function will be used to remove any deleted entities that a System is watching. I've made my CollisionManager a System, for example.

EntityEntry

An EntityEntry holds data about an Entity, and holds any type of T data. This class keeps a std::shared_ptr<Entity> (AKA SharedEntity) to prevent an object that's being watched by the KeyboardController from being deleted. 

Listener Pair

This typedef is used for overriding the ListenerConditions for an entity. This will be further explained in the KeyboardController.

Keyboard Controller

Finally, onto the KeyboardController. Here is our primary data structure and only member:

This is a map of all Entities listening for input, using the EntityID as the key. We use an EntityEntry because each Entity should only have one associated ListenerData.

I'll introduce the functions now:

The most important one is GetInputThisFrame, which calls SDL_GetKeyboardState. This function returns an array of every key on the keyboard. If the key was pressed, the value is set to 1, if not, it's set to 0. 

For my AddListener functions, we simply fill out the ListenerData for the value in the map: 

We must grab the SharedEntity from the EntityManager, so that we now have 2 strong references to the entity. If not, if the EntityManager removes the entity from its list, the shared pointer will be destroyed, and the KeyboardController will have a pointer to that destroyed memory. 

The NotifyListeners function, which is called in Update, is very simple as well: 

It goes through the list of current listeners and calls their HandleInput function, but only if isListening is true. This way, we can toggle input listening for an Entity.

The last important function is SetListenerConditions:

This is where the ListenerPair comes into play. By filling out the ListenerPair, we can change the ListenerConditions for an Entity at runtime. 

All that's left now is to create a KeyboardController in the DribbleKing class and it's ready to be used!

In my Player, I'll remove the switch statement and override the HandleInput accordingly:

In Player::Init, the player will be added as a Listener. And that's all!

Demonstration

In this demonstration, I'll toggle allowing the camera to be moved with the arrow keys. By default, the camera will follow the player. 

First, I'll add the BaseCamera as a listener, but set isListening to false: 

Here is the logic for detaching the camera:

And here is the logic for reattaching it. 

All we have to do is tweak the ListenerConditions. Each class can determine what to do with those conditions themselves. 

Here's a video of it working:

Conclusion

I like the versatility of this implementation, especially with the ListenerConditions. Moving forward, I'm thinking of making an interface to handle input because every Entity won't need input support. But for now, I think this is a nice solution. Next time, I'll be covering my Clock class.

Thanks for reading,

Jamari

Leave a comment

Log in with itch.io to leave a comment.