Designing the input system for my game engine

Creating an easy to use input system for my game engine, similar to Unreal Engine

I have never been a fan of input systems where you just use a bunch of if statements to check if a key is down, like unity's legacy input system. For my engine I decided to create an Input API similar to the legacy Unreal Engine input system, where you bind callbacks to specific key states.

Here is code from my editor camera's BeginPlay function, you can see it looks pretty similar to binding inputs in Unreal Engine.

void EditorCamera::BeginPlay()
{
	Entity::BeginPlay();

	auto inputsystem = GlobalSystem::Get().GetInput();

	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_DOWN, SDLK_W, std::bind(&EditorCamera::BeginForward, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_UP, SDLK_W, std::bind(&EditorCamera::StopForward, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_DOWN, SDLK_S, std::bind(&EditorCamera::BeginBack, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_UP, SDLK_S, std::bind(&EditorCamera::StopBack, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_DOWN, SDLK_A, std::bind(&EditorCamera::BeginLeft, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_UP, SDLK_A, std::bind(&EditorCamera::StopLeft, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_DOWN, SDLK_D, std::bind(&EditorCamera::BeginRight, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_UP, SDLK_D, std::bind(&EditorCamera::StopRight, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_DOWN, SDLK_LCTRL, std::bind(&EditorCamera::BeginDown, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_UP, SDLK_LCTRL, std::bind(&EditorCamera::StopDown, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_DOWN, SDLK_SPACE, std::bind(&EditorCamera::BeginUp, this));
	inputsystem->AddKeyboardInput(SDL_EVENT_KEY_UP, SDLK_SPACE, std::bind(&EditorCamera::StopUp, this));
	inputsystem->AddMouseButtonInput(SDL_EVENT_MOUSE_BUTTON_DOWN, SDL_BUTTON_RIGHT, std::bind(&EditorCamera::LockMouse, this));
	inputsystem->AddMouseButtonInput(SDL_EVENT_MOUSE_BUTTON_UP, SDL_BUTTON_RIGHT, std::bind(&EditorCamera::UnlockMouse, this));
	inputsystem->AddGamepadAxisInput(SDL_EVENT_GAMEPAD_AXIS_MOTION, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, std::bind(&EditorCamera::GamepadLeftTrigger, this, std::placeholders::_1));
	inputsystem->AddGamepadAxisInput(SDL_EVENT_GAMEPAD_AXIS_MOTION, SDL_GAMEPAD_AXIS_LEFTX, std::bind(&EditorCamera::GamepadLeftXAxis, this, std::placeholders::_1));
	inputsystem->AddGamepadAxisInput(SDL_EVENT_GAMEPAD_AXIS_MOTION, SDL_GAMEPAD_AXIS_LEFTY, std::bind(&EditorCamera::GamepadLeftYAxis, this, std::placeholders::_1));
	inputsystem->AddGamepadAxisInput(SDL_EVENT_GAMEPAD_AXIS_MOTION, SDL_GAMEPAD_AXIS_RIGHTX, std::bind(&EditorCamera::GamepadRightXAxis, this, std::placeholders::_1));
	inputsystem->AddGamepadAxisInput(SDL_EVENT_GAMEPAD_AXIS_MOTION, SDL_GAMEPAD_AXIS_RIGHTY, std::bind(&EditorCamera::GamepadRightYAxis, this, std::placeholders::_1));
	inputsystem->AddMouseWheelInput(std::bind(&EditorCamera::ChangeSpeed, this, std::placeholders::_1));
	inputsystem->AddMouseAxisInput(std::bind(&EditorCamera::CameraMouseLook, this, std::placeholders::_1));
}

The entire Input system is less than 300 lines of code between both the header and translation unit. I simply have structs that contain the input event, and input type. These are stored in a hash map, where the key is the hash of the struct, this is simple since SDL input types already support std::hash. The value in the hash map is just a vector of std::function.

struct KeyboardInputMapping {
	SDL_EventType inputEvent; // SDL_KEY_DOWN | SDL_KEY_UP
	SDL_Keycode inputButton;

	bool operator==(const KeyboardInputMapping& other) const {
		return inputEvent == other.inputEvent && inputButton == other.inputButton;
	}
};

struct GamepadButtonInputMapping {
	SDL_EventType inputEvent; // SDL_EVENT_GAMEPAD_BUTTON_DOWN | SDL_EVENT_GAMEPAD_BUTTON_UP
	SDL_GamepadButton inputButton;

	bool operator==(const GamepadButtonInputMapping& other) const {
		return inputEvent == other.inputEvent && inputButton == other.inputButton;
	}
};

std::unordered_map<KeyboardInputMapping, std::vector<std::function<void()>>> m_keyboardInputMap;
std::unordered_map<GamepadButtonInputMapping, std::vector<std::function<void()>>> m_gamepadButtonInputMap;
std::unordered_map<uint8_t, std::vector<std::function<void(int16_t)>>> m_gamepadAxisInputs

The input system has a ProcessEvent() function called from SDL::PollEvent(), it has a switch case for each of SDL's input types, and then just indexes into the hash map and calls every std::function in the returned vector.

void InputSystem::ProcessEvent(SDL_Event event)
{
	ZoneScopedN("InputSystem::ProcessEvent");

	switch (event.type) {
      case SDL_EVENT_KEY_UP:
      case SDL_EVENT_KEY_DOWN:
          ProcessKeyboardInput(event);
      break;
  
      case SDL_EVENT_MOUSE_BUTTON_DOWN:
      case SDL_EVENT_MOUSE_BUTTON_UP:
          ProcessMouseButtonInput(event);
      break;
  
      case SDL_EVENT_MOUSE_MOTION:
          ProcessMouseAxisInput(event);
      break;
  
      case SDL_EVENT_MOUSE_WHEEL:
          ProcessMouseWheelInput(event);
      break;
  
      case SDL_EVENT_GAMEPAD_BUTTON_UP:
      case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
          ProcessGamepadButtonInput(event);
      break;
  
      case SDL_EVENT_GAMEPAD_AXIS_MOTION:
          ProcessGamepadAxisInput(event);
      break;
	}
}

void InputSystem::ProcessGamepadAxisInput(SDL_Event event)
{
	for (std::function<void(int16_t)> callback : m_gamepadAxisInputs[event.gaxis.axis])
	{
		int16_t axisValue = event.gaxis.value;

		callback(axisValue);
	}
}

I am pretty happy with how well the system works for how simple it is, but ideally I want it to work with some sort of InputAction prefab like Unreal Engine's new Input System, this would make it much easier to organize Keyboard/Gamepad inputs together, and allow for easy rebinding. Would also be nice to have a macro over std::bind to make the code nicer to read.