Creating an Input Component

Implement player controls and AI actions cleanly using the Command pattern
0/108 Completed0%
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we connect player input to entity behavior. We'll first implement a simple InputComponent that directly modifies an entity's state.

Then, we'll refactor this using the command pattern, creating objects that represent actions like "move left" and "shoot".

Finally, we'll use std::unordered_map to map keys to these commands, enabling easy customization of our keybindings.

Starting Point

In our previous lesson, we established communication between components and their entities, explored dependency management. Our Entity objects can hold components like TransformComponent and ImageComponent, and these components can interact.

For this lesson, we’ll let our Entity objects move. To support this, we’ll update our TransformComponent with a new public Move() function:

// TransformComponent.h
#pragma once
#include "Vec2.h"
#include "Component.h"

class TransformComponent : public Component {
public:
  using Component::Component;

  Vec2 GetPosition() const {
    return Position;
  }
  
  void Move(const Vec2& Movement) {
    Position += Movement;
  }

private:
  Vec2 Position{0, 0};
};

A Basic Input Component

Let's start by creating a simple InputComponent. Its primary role will be to listen for specific SDL_Event types (like keyboard presses) and directly tell its owning Entity's TransformComponent to move.

This approach works for simple cases but, as we'll see, it quickly becomes limiting. Here's a basic implementation:

// InputComponent.h
#pragma once
#include "Component.h"

class InputComponent : public Component {
 public:
  using Component::Component;
  void HandleEvent(const SDL_Event& E) override;
};
// InputComponent.cpp
#include <iostream>
#include <SDL.h>
#include "InputComponent.h"
#include "Entity.h"

void InputComponent::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_KEYDOWN) {
    if (!GetOwner()) return;  // Safety check
    TransformComponent* Transform{
      GetOwner()->GetTransformComponent()};
    if (!Transform) return;  // Safety check

    switch (E.key.keysym.sym) {
      case SDLK_LEFT:
        Transform->Move({-1.0, 0.0});  
        std::cout << "Input: Move Left\n";
        break;
      case SDLK_RIGHT:
        Transform->Move({1.0, 0.0});  
        std::cout << "Input: Move Right\n";
        break;
      // Add cases for other keys if needed
      default:
        break;
    }
  }
}

Let’s update our Entity class with an AddInputComponent() and GetInputComponent() , using the same pattern we applied for managing TransformComponents:

// Entity.h
// ...

#include "InputComponent.h"

// ...

class Entity {
public:
  // ...

  InputComponent* AddInputComponent() {
    if (GetInputComponent()) {
      std::cout << "Error: Cannot have "
        "multiple input components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          InputComponent>(this))};

    NewComponent->Initialize();

    return static_cast<InputComponent*>(
      NewComponent.get());
  }

  InputComponent* GetInputComponent() const {
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{
        dynamic_cast<InputComponent*>(C.get())
      }) {
        return Ptr;
      }
    }
    return nullptr;
  }

  // ...
};

Finally, we’ll move over to our Scene and give our player character an InputComponent to verify everything is working:

// Scene.h
// ...

class Scene {
public:
  Scene() {
    EntityPtr& Player{
      Entities.emplace_back(
        std::make_unique<Entity>())};

    Player->AddTransformComponent();
    Player->AddInputComponent();
  }

  // ...
};

Running our game and hitting the arrow keys should trigger the appropriate reactions from our InputComponent:

Input: Move Left
Input: Move Right

The Command Pattern

While the basic InputComponent works, it mixes input detection (checking SDL_Event) with action execution - Move(). The command pattern separates these concerns. Rather than an action being implemented as a direct function call, the command pattern has us represent them as durable entities, such as objects.

We’ll discuss the advantanges of this soon and the capabilities it unlocks, but for now, let’s implement the basic idea. We’ll define a Command class that represents an action that can be executed on some target:

// Commands.h
#pragma once
class Entity;

class Command {
public:
  virtual void Execute(Entity* Target){};
  virtual ~Command() = default;
};

Now, let's create a specific command for movement. It will inherit from Command and override the Execute() method to perform the actual movement using the Target entity's TransformComponent.

As with any object, commands can store additional data or functions as needed to help them fully represent the desired action. In this case, we’ll store a Vec2 representing the direction and magnitiude of movement that the command represents:

// Commands.h
#pragma once
#include "Vec2.h"

class Command {/*...*/}; class MovementCommand : public Command { public: MovementCommand(Vec2 Movement) : Movement{Movement} {} void Execute(Entity* Target) override; Vec2 Movement; };
// Commands.cpp
#include "Commands.h"
#include "Entity.h"

void MovementCommand::Execute(Entity* Target) {
  if (!Target) return; // Safety check
  Target->GetTransformComponent()
        ->Move(Movement);
}

Note, to keep the examples simple, we’re assuming that Target has a TransformComponent. In more robust systems, we’d likely add additional checks and error handling as needed here.

Advantages of Commands

Why go to the trouble of creating command objects? This pattern unlocks several capabilities that are much harder to implement with direct input handling.

Key Customization

Most games allow players to rebind keys so, at the time we’re writing our code, we don’t necessarily know what the left and right arrow keys, for example, are supposed to do. The player might have changed them.

Using commands, we can create a mapping from input events (e.g., SDLK_LEFT) to Command objects (e.g., a MovementCommand configured for leftward movement). Changing bindings simply involves updating this mapping, without touching the core input handling or entity logic.

We’ll build our InputComponent to support this process throughout this lesson.

Context-Specific Bindings

In a relatively complex project, our control scheme varies depending on the context. The actions that buttons take will vary greatly depending on whether the player is in the main menu or in the game, or whether their character is on foot or in a vehicle.

Being able to save and switch an entire set of keybindings in an organized way is very useful in these scenarios.

Non-Player Characters

More complex games include non-player characters, such as enemies that the player has to fight. The AI code we write will need to issue commands to these characters, but these commands are not generated by keyboard and mouse events.

Decoupling player input from the commands that are issued to entities in our games allows us to create more flexible and reusable code. If our entity has a HandleCommand() method that accepts Command objects, that entity can be controlled by either the player or our AI.

In the case of the player, Command objects will be the result of input whilst, for the NPCs, they will be the result of AI logic. But the entity doesn’t need to care - it’s HandleCommand() function just works regardless of where the Command originated from.

Storing Commands

When a command is an object, it has all the usual object capabilities. It can be stored in an container to be executed later (a "command queue")

For example, in some situations, the full command may take some time to construct. For example, a player might select an ability to use and then, some frames later, select the target of that ability. Being able to hold a command in memory and fully define it over multiple steps makes mechanics like these easier to create.

Undo / Redo

In addition to a command being stored for executation later, it can also continue to be stored even after it has been executed. This lets us review and access the history of commands that have been previously executed.

A common feature where this is useful is the ability to undo (or redo) previous actions. Once we have built a command system, undo and redo functionality tends to be much easier to implement:

// Commands.h
// ...

class MovementCommand : public Command {
public:
  // ...
  void Execute(Entity* T) {
    // Remember the target I was executed on
    Target = T;
    Target->GetTransformComponent()
      .Move(Movement);
  }
  
  void Undo() {
    Target->GetTransformComponent()
      .Move(-Movement);
  }
  
  Entity* Target;
  Vec2 Movement;
};

Implementing Commands

Given the inherent flexibility of commands, we’re mostly free to use them in any way that suits our needs. The most basic usage is to create a Command and then call its Execute() method, providing a target:

MovementCommand MoveRight({10.0, 0.0});
MoveRight.Execute(SomeEntityPointer);

Commonly, we’d give our Entity class a way receive a command, and execute it on itself. A straightforward approach is to add a HandleCommand() method to the Entity base class. This method will take a pointer (or reference) to a Command object and call its Execute() method, passing this as the target.

For our simple example, we won’t need to store the commands for long - we’ll just discard them after shortly after they’re executed. Therefore, we’ll give our HandleCommand() function ownership of the command it receives by accepting it as a std::unique_ptr. This means that command will be automatically deleted once the function ends:

// Entity.h
// ...
#include "Commands.h" 
// ...

class Entity {
public:
  // ...
  virtual void HandleCommand(
    std::unique_ptr<Command> Cmd
  ) {
    Cmd->Execute(this);
  }

  // ...
};

Now, instead of directly calling Move(), our InputComponent would create a Command object and pass it to the target Entity's HandleCommand() method. For example, creating and handling a move command might look like this:

// InputComponent.cpp
#include <SDL.h>
#include <memory> // for std::make_unique 
#include "InputComponent.h"
#include "Entity.h"

void InputComponent::HandleEvent(
  const SDL_Event& E
) {
  if (E.type == SDL_KEYDOWN) {
    Entity* Owner{GetOwner()};
    if (!Owner) return;
    TransformComponent* Transform{
      Owner->GetTransformComponent()};
    if (!Transform) return;

    switch (E.key.keysym.sym) {
      case SDLK_LEFT:
        Owner->HandleCommand(
          std::make_unique<MovementCommand>(
            Vec2{-1.0, 0.0}));
        break;
      case SDLK_RIGHT:
        Owner->HandleCommand(
          std::make_unique<MovementCommand>(
            Vec2{1.0, 0.0}));
        break;
      default:
        break;
    }
  }
}

Unordered Maps

Let’s update our InputComponent to support dynamic key rebinding. To manage our key bindings (linking specific keys to specific commands), we need a way to quickly find the command associated with a pressed key (like SDLK_LEFT).

The std::unordered_map container is perfect for this. It stores key-value pairs and provides fast lookups based on the key. If you’re familiar with other programming languages, a std::unordered_map is the C++ equivalent to what you might recognize as a hash map, a dictionary, or an associative array.

We’ll cover the key points of std::unordered_map below, but we also have a dedicated lesson in our advanced course if you want a more detailed introduction:

Creating Unordered Maps

To use a std::unordered_map, you first need to include the <unordered_map> header. When you declare a map, you specify the type of the key and the type of the value as template arguments. Here are two examples:

#include <string>
#include <unordered_map>
#include <SDL.h>

int main(int argc, char** argv) {
  // A map where keys are strings (e.g., setting names)
  // and values are integers (e.g., setting values)
  std::unordered_map<std::string, int> Settings;

  // A map where keys are SDL Keycodes (which are integers)
  // and values are strings (e.g., action names)
  std::unordered_map<
    SDL_Keycode, std::string> KeyToActionName;

  return 0;
}

Remember, we can alias complex types if we prefer:

using KeyMap = std::unordered_map<
  SDL_Keycode, std::string>
  
KeyMap KeyToActionName;

The keys in an unordered map must be unique. If you try to insert an element with a key that already exists, the map's behavior depends on the insertion method used (some methods overwrite, others don't).

Accessing Elements

The most common way to access elements in a std::unordered_map is using the square bracket operator [], similar to arrays or vectors.

If you use [] with a key that already exists in the map, it returns a reference to the corresponding value, which you can read or modify.

If you use [] with a key that does not exist in the map, it performs two actions:

  1. It inserts a new element into the map with the given key. The value associated with this new key is value-initialized (e.g., 0 for integers, nullptr for pointers, default-constructed for objects).
  2. It then returns a reference to this newly created value.

This automatic insertion behavior can be convenient, but also potentially surprising if you only intended to read a value.

#include <SDL.h>
#include <unordered_map>
#include <string>
#include <iostream>

int main(int argc, char** argv) {
  std::unordered_map<
    SDL_Keycode, std::string> KeyToActionName;

  // Add mappings using []
  KeyToActionName[SDLK_LEFT] = "Move Left";   
  KeyToActionName[SDLK_RIGHT] = "Move Right";  
  KeyToActionName[SDLK_SPACE] = "Jump";       

  // Retrieve and print a value
  std::cout << "\nSDLK_LEFT maps to: "
    << KeyToActionName[SDLK_LEFT];

  // Modify an existing value
  KeyToActionName[SDLK_SPACE] = "Primary Action";  
  std::cout << "\nSDLK_SPACE now maps to: "
    << KeyToActionName[SDLK_SPACE];

  // Accessing a non-existent key adds it with a
  // default value.  For std::string, the
  // default value is an empty string ""
  std::cout << "\nSDLK_LSHIFT maps to: '"
    // Key doesn't exist
    << KeyToActionName[SDLK_LSHIFT]  << "'"; 

  // Check the size - it increased because
  // SDLK_LSHIFT was added
  std::cout << "\nMap size: "
    << KeyToActionName.size();

  return 0;
}
SDLK_LEFT maps to: Move Left
SDLK_SPACE now maps to: Primary Action
SDLK_LSHIFT maps to: ''
Map size: 4

Checking if a Map Contains an Element

Because using [] automatically inserts an element if the key isn't found, it's often necessary to check if a key exists before attempting to access its value, especially if you don't want accidental insertions.

C++20 introduced the contains() method for associative containers, including std::unordered_map. It simply returns true if the key exists and false otherwise.

#include <SDL.h>
#include <unordered_map>
#include <string>
#include <iostream>

int main(int argc, char** argv) {
  std::unordered_map<
    SDL_Keycode, std::string> KeyToActionName;
  KeyToActionName[SDLK_LEFT] = "Move Left";
  KeyToActionName[SDLK_RIGHT] = "Move Right";

  if (KeyToActionName.contains(SDLK_LEFT)) {
    std::cout << "\nMap contains the LEFT key with value: "
      << KeyToActionName[SDLK_LEFT];
  }

  if (!KeyToActionName.contains(SDLK_a)) {
    std::cout << "\nMap does not contain the A key";
  }

  return 0;
}
Map contains the LEFT key with value: Move Left
Map does not contain the A key

Pre-C++20 Alternatives to contains()

The contains() method was added to std::unordered_map (and other associative containers) in the C++20 standard. If you're working with an older standard, you need a different way to check if a key exists.

The most common alternative is the count() method. It returns the number of elements in the map that have the specified key. Since keys in std::unordered_map must be unique, count() will only ever return 0 (if the key is not present) or 1 (if the key is present).

#include <SDL.h>
#include <unordered_map>
#include <string>
#include <iostream>

int main(int argc, char** argv) {
  std::unordered_map<
    SDL_Keycode, std::string> KeyToActionName;
  KeyToActionName[SDLK_LEFT] = "Move Left";
  KeyToActionName[SDLK_RIGHT] = "Move Right";

  if (KeyToActionName.count(SDLK_LEFT) > 0) { 
    std::cout << "\nMap contains LEFT key";
  }

  if (KeyToActionName.count(SDLK_a) == 0) { 
    std::cout << "\nMap does not contain A key";
  }

  return 0;
}
Map contains LEFT key
Map does not contain A key

You might wonder why a count() method exists if the result is only ever 0 or 1. This method originated from containers like std::unordered_multimap and std::multimap, which do allow multiple elements to have the same key.

For consistency across the standard library's map-like containers, std::unordered_map also provides count().

We recommend using contains() if your compiler supports C++20 or later, as it clearly expresses the intent of checking for existence.

Key Bindings

Now we can combine the command pattern and std::unordered_map to create a flexible key binding system. The idea is to map specific SDL_Keycode values (the keys) to a mechanism that creates the appropriate Command object (the values).

Since we want to create commands that potentially need runtime information, we'll store functions capable of creating the commands. Functions that are primarily designed to create and return objects are sometimes called factories or factory functions.

Command is a polymorphic type, so code that uses our factory functions doesn’t necessarily know exactly what type the command will have - just that the type derives from Command. As such, our factory functions will need to create the command in dynamic memory, and return a pointer to it.

Rather than trying to manage this memory manually, we’ll use smart pointers. We’ll have our factory return std::unique_ptr<Command> instances, so whoever calls our factory immediately assumes ownership of the command it created.

We can use std::function to store our factories. Let’s create aliases for these types above our InputComponent class:

// InputComponent.h
// ...
#include <functional> // for std::function
#include <memory> // for std::unique_ptr
// ...

class Command;
using CommandPtr = std::unique_ptr<Command>;

// Command factories take no args and return
// CommandPtr (std::unique_ptr<Command>)
using CommandFactory = std::function<
  CommandPtr()>;

// ...

class InputComponent : public Component {
  // ...
};

Next, our InputComponent needs to store an mapping between SDL keycodes and the factory that creates the command associated with that button. Let’s add an unordered_map with a type alias for that:

// InputComponent.h
// ...
#include <unordered_map> 
// ...

class Command;
using CommandPtr = std::unique_ptr<Command>;
using CommandFactory = std::function<
  CommandPtr()>;
using KeyToFactoryMap = std::unordered_map<
  SDL_Keycode, CommandFactory>;

class InputComponent : public Component {
public:
  // ...

private:
  KeyToFactoryMap KeyDownBindings;
};

We’ll also provide a way to add (or change) a keybinding:

// InputComponent.h
// ...

class InputComponent : public Component {
 public:
  // ...
  void BindKeyDown(
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyDownBindings[Key] = Factory;
  }
  // ...
};

Finally, we’ll need a way to set up our default keybindings. Overriding the Initialize() function is likely the best place for that logic.

Our final InputComponent.h class looks like the following. We still have to implement some of this logic in our source file, which we’ll do next.

// InputComponent.h
#pragma once
#include <functional>
#include <memory>
#include "Component.h"

class Command;
using CommandPtr = std::unique_ptr<Command>;
using CommandFactory = std::function<
  CommandPtr()>;
using KeyToFactoryMap = std::unordered_map<
  SDL_Keycode, CommandFactory>;

class InputComponent : public Component {
 public:
  using Component::Component;
  void Initialize() override;

  void HandleEvent(const SDL_Event& E) override;

  void BindKeyDown(
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyDownBindings[Key] = Factory;
  }

private:
  KeyToFactoryMap KeyDownBindings;
};

Implementing Keybindings

To complete our input component, there are three remaining things we need to do in our InputComponent.cpp:

  1. Define the factory functions for creating command objects.
  2. Implement our Initialize() override to bind some keys to our factory functions - that is, add entries to our KeyDownBindings map where the key is the SDL keycode and the value is the function that creates the command associated with that key
  3. Update our HandleEvent() function to dynamically create the correct event based on the current state of our KeyDownBindings map

First, let's define the functions that will create our different commands. These functions return a CommandPtr, which is the alias we created for std::unique_ptr<Command>:

// InputComponent.cpp
#include <SDL.h>
#include "Entity.h"

// Our type aliases are coming from here
#include "InputComponent.h"

namespace{
// Factory function for moving left (negative X)
CommandPtr CreateMoveLeftCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{-5.0, 0.0});
}

// Factory function for moving right (positive X)
CommandPtr CreateMoveRightCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{5.0, 0.0});
}
}

// ...

Using the namespace keyword with no name here creates an anonymous namespace. Things that are in an anonymous namespace are only accessible within that same file. This is desirable here, as we don’t want to pollute the global namespace with things that are only relevant to our InputComponent.cpp file.

Next, let’s implement our Initialize() override to set up some default keybindings:

// InputComponent.cpp
// ...

void InputComponent::Initialize() {
  BindKeyDown(SDLK_LEFT, CreateMoveLeftCommand); 
  BindKeyDown(SDLK_RIGHT, CreateMoveRightCommand); 
  // Bind other keys as needed...
}

// ...

Finally, let’s update our HandleEvent() function to work with our dynamic keybindings. It will check our KeyDownBindings map to see if the key is associated with a factory function. If so, it will call that function to create a Command.

We will then pass that command to our owning entity’s HandleCommand() function or, alternatively, execute the command right here:

// InputComponent.cpp
// ...

void InputComponent::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_KEYDOWN) {
    Entity* Owner{GetOwner()};
    if (!Owner) return;  // Safety check
    SDL_Keycode Key{E.key.keysym.sym};
    if (KeyDownBindings.contains(Key)) {
      // This line is explained in the next section
      Owner->HandleCommand(
        KeyDownBindings[Key]()
      );
      
      // Alternatively:
      // KeyDownBindings[Key]()->Execute(Owner);
    }
  }
}

R-Values

The previous example contains a fairly complex expression that is likely to be confusing:

Owner->HandleCommand(KeyDownBindings[Key]());

Let’s break this down step-by-step:

  • KeyDownBindings[Key] retrieves the command factory associated with the key the player pressed. We know the KeyDownBindings map has a factory for this key because our earlier contains() check returned true.
  • KeyDownBindings[Key]() immediately invokes that factory, returning a command in the form of a std::unique_ptr<Command> (or a CommandPtr, if we use our alias)
  • HandleCommand(KeyDownBindings[Key]()) forwards this command to the HandleCommand() function.

The memory ownership aspect adds additional complexity here. Our factory function returns a std::unique_ptr, meaning that the HandleEvent() function should "own" that command. How can it transfer its ownership to the HandleCommand() function without using std::move()?

The reason is that, in our expression, the compiler can see that our HandleEvent() function is not storing the value returned by KeyDownBindings[Key](). It just immediately forwards it to the HandleCommand() function without storing a local copy. As such, it can be implicitly moved - if HandleEvent() isn’t storing the value, it can’t use it in the future, so it doesn’t need to own it.

If our HandleEvent() function was storing the command by, for example, saving it in a variable, then it would need to use std::move() to transfer its ownership to the HandleCommand() function:

void InputComponent::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_KEYDOWN) {
    Entity* Owner{GetOwner()};
    if (!Owner) return;
    SDL_Keycode Key{E.key.keysym.sym};
    if (KeyDownBindings.contains(Key)) {
      CommandPtr Command{KeyDownBindings[Key]()};
      Owner->HandleCommand(std::move(Command));
    }
  }
}

These "temporary" values that we don’t store have a slightly obscure but official name: r-values. We have a dedicated lesson covering this topic in more detail in the advanced course:

Continous Input

Our current InputComponent uses SDL_KEYDOWN events. This works perfectly for discrete actions – actions that happen once when a key is pressed.

However, in many situations, we want an input to be continuous - the action continues to happen as long as the button is held down.

A better approach for continuous input is polling. Instead of waiting for an event, we actively ask SDL for the current state of the entire keyboard each frame.

We covered keyboard state and polling in more detail here:

The key point is that SDL provides the SDL_GetKeyboardState() function for getting the current keyboard state. It returns a pointer to an internal SDL array where each element represents the state of a physical key (scancode). A value of 1 means the key is currently pressed down, and 0 means it's up.

We'll need a way to map the SDL_Keycode values we use in our bindings (like SDLK_LEFT) to the SDL_Scancode indices used by this array. SDL provides SDL_GetScancodeFromKey() for this conversion.

Updating the InputComponent for Polling

To implement polling, we need our InputComponent to do work every frame. We can add a Tick() override to handle this continuous checking.

We'll also add a separate map specifically for bindings related to continuous actions, keeping them distinct from the discrete key-down bindings. We’ll add a BindKeyHeld() function for adding bindings to this map:

// InputComponent.h
// ...

class InputComponent : public Component {
 public:
  // ...
  void Tick(float DeltaTime) override; 
  
  // For reference - this hasn't changed
  void BindKeyDown(
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyDownBindings[Key] = Factory;
  }
  
  void BindKeyHeld( 
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyHeldBindings[Key] = Factory;
  }
  // ...

private:
  // Map for discrete key presses (events)
  // For reference - this hasn't changed
  KeyToFactoryMap KeyDownBindings;
  
  // Map for continuous key holds (polling)
  KeyToFactoryMap KeyHeldBindings;  
};

Now, let's implement the Tick() method in InputComponent.cpp. Inside Tick(), we get the keyboard state array.

Then, we iterate through our KeyHeldBindings. For each binding, we convert the SDL_Keycode to an SDL_Scancode, check the state array at that scancode's index, and if the key is pressed, we create and dispatch the associated command:

// InputComponent.cpp
// ...

void InputComponent::Tick(float DeltaTime) {
  Entity* Owner{GetOwner()};
  if (!Owner) return; // Safety check

  // Get the current keyboard state
  const Uint8* CurrentKeyStates{
    SDL_GetKeyboardState(nullptr)};

  // Check bindings for keys being held down
  for (const auto& [Key, Factory] : KeyHeldBindings) {
    SDL_Scancode Scancode{SDL_GetScancodeFromKey(Key)};
    if (CurrentKeyStates[Scancode]) {
      // Key is held, create and handle command
      Owner->HandleCommand(Factory());
    }
  }
}

// ...

Let’s update our Initialize() function to bind our movement actions to our new continuous input handling instead:

// InputComponent.cpp
// ...

void InputComponent::Initialize() {
  // Bind keys for continuous holding
  BindKeyDown(SDLK_LEFT, CreateMoveLeftCommand); 
  BindKeyHeld(SDLK_LEFT, CreateMoveLeftCommand); 
  BindKeyDown(SDLK_RIGHT, CreateMoveRightCommand); 
  BindKeyHeld(SDLK_RIGHT, CreateMoveRightCommand); 

  // Example: Bind a discrete action
  // BindKeyDown(SDLK_SPACE, CreateJumpCommand);
}

// ...

Key Repeat Events

When a keyboard button is held down, most desktop platforms have a mechanism for reporting this, sometimes called key repeat events. The platform will continuously report key events as long as the keyboard button is held down, and SDL will make those events visible to us through the event loop.

We covered key repeats in more detail in our introduction to keyboard handling:

However, platforms tune their key repeat mechanism on the assumption the keyboard is being used for text input, rather than controlling a game character.  Specifically:

  • There tends to be a long delay between the initial keyboard event, and the first "repeat" event. That means the user has to hold their key down for a long time before we start getting repeat events.
  • That delay before the first repeat event, and the delay between repeat events, is inconsistent across platforms and can sometimes be configured within the system settings.

If the keyboard is being used for text input in our game, this is all fine. But if it’s being used for something like character movement, we don’t want that to be delayed or changed based on system settings. So, in those scenarios, we should use the polling approach instead.

Ignoring Key Repeats

Now that we have a dedicated mechanism for handling continuous input, we may want to explicitly ignore key repeat events for actions that are supposed to be discrete.

SDL key events include a repeat variable that lets us detect if a key event is a freshly pressed button (in which case, repeat will be 0), or a button that is still being held from a previous event (in which case, repeat will be a positive value).

In our HandleEvent() function, we can check if an event is a repeat and ignore it like this:

// InputComponent.cpp
// ...

void InputComponent::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_KEYDOWN) {
    if (E.key.repeat) return; 
} }

Complete Code

The complete version of our InputComponent and Command classes we created in this lesson are provided below:

#pragma once
#include <functional>
#include <memory>
#include <unordered_map>
#include <SDL.h>
#include "Component.h"

class Command;
using CommandPtr = std::unique_ptr<Command>;
using CommandFactory = std::function<
  CommandPtr()>;
using KeyToFactoryMap = std::unordered_map<
  SDL_Keycode, CommandFactory>;

class InputComponent : public Component {
 public:
  using Component::Component;
  void Initialize() override;
  void Tick(float DeltaTime) override;

  void HandleEvent(const SDL_Event& E) override;

  void BindKeyDown(
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyDownBindings[Key] = Factory;
  }

  void BindKeyHeld(
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyHeldBindings[Key] = Factory;
  }

private:
  // Map for discrete key presses (events)
  KeyToFactoryMap KeyDownBindings;
  // Map for continuous key holds (polling)
  KeyToFactoryMap KeyHeldBindings;
};
#include <SDL.h>
#include "InputComponent.h"
#include "Entity.h"
#include "Commands.h"
#include "Vec2.h"

namespace{
CommandPtr CreateMoveLeftCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{-5.0, 0.0});
}

CommandPtr CreateMoveRightCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{5.0, 0.0});
}
}

void InputComponent::Initialize() {
  BindKeyHeld(SDLK_LEFT, CreateMoveLeftCommand);
  BindKeyHeld(SDLK_RIGHT, CreateMoveRightCommand);
  // Bind other keys...
}

void InputComponent::Tick(float DeltaTime) {
  Entity* Owner{GetOwner()};
  if (!Owner) return; // Safety check

  // Get the current keyboard state
  const Uint8* CurrentKeyStates{
    SDL_GetKeyboardState(nullptr)};

  // Check bindings for keys being held down
  for (const auto& [Key, Factory] : KeyHeldBindings) {
    SDL_Scancode Scancode{SDL_GetScancodeFromKey(Key)};
    if (CurrentKeyStates[Scancode]) {
      // Key is held, create and handle command
      Owner->HandleCommand(Factory());
    }
  }
}

void InputComponent::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_KEYDOWN) {
    Entity* Owner{GetOwner()};
    if (!Owner) return;
    SDL_Keycode Key{E.key.keysym.sym};
    if (KeyDownBindings.contains(Key)) {
      Owner->HandleCommand(KeyDownBindings[Key]());
      // Alternatively:
      // KeyDownBindings[Key]()->Execute(Owner);
    }
  }
}
#pragma once
#include "Vec2.h"

class Entity;

class Command {
 public:
  virtual void Execute(Entity* Target) {}
  virtual ~Command() = default;
};

class MovementCommand : public Command {
public:
  MovementCommand(Vec2 Movement)
  : Movement{Movement} {}
  void Execute(Entity* Target) override;
  Vec2 Movement;
};
#include "Commands.h"
#include "Entity.h"
#include "TransformComponent.h"

void MovementCommand::Execute(Entity* Target) {
  if (!Target) return;  // Safety Check
  Target->GetTransformComponent()
        ->Move(Movement);
}

We also made smaller changes to our Entity, Scene, and TransformComponent classes, which we’ve highlighted below:

#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "Component.h"
#include "TransformComponent.h"
#include "ImageComponent.h"
#include "InputComponent.h"
#include "Commands.h" 

using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;

class Entity {
public:
  virtual void HandleEvent(const SDL_Event& E) {
    for (ComponentPtr& C : Components) {
      C->HandleEvent(E);
    }
  }

  virtual void Tick(float DeltaTime) {
    for (ComponentPtr& C : Components) {
      C->Tick(DeltaTime);
    }
  }

  virtual void Render(SDL_Surface* Surface) {
    for (ComponentPtr& C : Components) {
      C->Render(Surface);
    }
  }

  virtual ~Entity() = default;

  virtual void HandleCommand(
    std::unique_ptr<Command> Cmd
  ) {
    Cmd->Execute(this);
  }

  TransformComponent* AddTransformComponent() {
    if (GetTransformComponent()) {
      std::cout << "Error: Cannot have "
        "multiple transform components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          TransformComponent>(this))};

    NewComponent->Initialize();

    return static_cast<TransformComponent*>(
      NewComponent.get());
  }

  TransformComponent* GetTransformComponent() const {
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{
        dynamic_cast<TransformComponent*>(C.get())
      }) {
        return Ptr;
      }
    }
    return nullptr;
  }

  InputComponent* AddInputComponent() {
    if (GetInputComponent()) {
      std::cout << "Error: Cannot have "
        "multiple input components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          InputComponent>(this))};

    NewComponent->Initialize();

    return static_cast<InputComponent*>(
      NewComponent.get());
  }

  InputComponent* GetInputComponent() const {
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{
        dynamic_cast<InputComponent*>(C.get())
      }) {
        return Ptr;
      }
    }
    return nullptr;
  }

  ImageComponent* AddImageComponent() {
    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<ImageComponent>(
          this))};

    NewComponent->Initialize();

    return static_cast<ImageComponent*>(
      NewComponent.get());
  }

  using ImageComponents =
    std::vector<ImageComponent*>;
  ImageComponents GetImageComponents() const {
    ImageComponents Result;
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{dynamic_cast<
        ImageComponent*>(C.get())}
      ) {
        Result.push_back(Ptr);
      }
    }
    return Result;
  }

  void RemoveComponent(Component* PtrToRemove) {
    for (size_t i{0}; i < Components.size(); ++i) {
      if (Components[i].get() == PtrToRemove) {
        Components.erase(Components.begin() + i);
        return;
      }
    }

    std::cout << "Warning: Attempted to remove "
      "a component not found on this entity.\n";
  }

private:
  ComponentPtrs Components;
};
#pragma once
#include <SDL.h>
#include <vector>
#include "Entity.h"

using EntityPtr = std::unique_ptr<Entity>;
using EntityPtrs = std::vector<EntityPtr>;

class Scene {
public:
  Scene() {
    EntityPtr& Player{
      Entities.emplace_back(
        std::make_unique<Entity>())};

    Player->AddTransformComponent();
    Player->AddInputComponent();
  }

  void HandleEvent(SDL_Event& E) {
    for (EntityPtr& Entity : Entities) {
      Entity->HandleEvent(E);
    }
  }

  void Tick(float DeltaTime) {
    for (EntityPtr& Entity : Entities) {
      Entity->Tick(DeltaTime);
    }
  }

  void Render(SDL_Surface* Surface) {
    for (EntityPtr& Entity : Entities) {
      Entity->Render(Surface);
    }
  }

private:
  EntityPtrs Entities;
};
#pragma once
#include "Vec2.h"
#include "Component.h"

class TransformComponent : public Component {
public:
  using Component::Component;

  Vec2 GetPosition() const { return Position; }

  void Move(const Vec2& Movement) {
    Position += Movement;
  }

private:
  Vec2 Position{0, 0};
};

Summary

We've significantly improved our input system by implementing the command design pattern. Instead of directly manipulating components based on input events, we now create Command objects that represent the desired action. This separation enhances flexibility and maintainability.

We saw how this pattern benefits key customization, NPC control, and potentially advanced features like undo. We integrated command execution into our Entity class and used std::unordered_map to build a data-driven key binding mechanism using command factories.

Key takeaways:

  • The command pattern encapsulates requests as objects (e.g., MovementCommand).
  • It decouples the invoker (input handler) from the receiver (entity).
  • We can use it to allow key mapping using std::unordered_map.
  • It allows different sources (player input, AI) to issue the same commands.
  • Commands can store state for execution and potentially undo operations.
  • Using command factories (std::function) in maps provides flexibility for command creation.

Was this lesson useful?

Next Lesson

Creating an Image Component

Display graphics by creating an ImageComponent that loads files and renders them via SDL_image.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
0/108 Completed0%
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Creating Components
Next Lesson

Creating an Image Component

Display graphics by creating an ImageComponent that loads files and renders them via SDL_image.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved