I wrote a mouse and keyboard event system in C++, using the GLFW library to get mouse and keyboard states.
I've been working on a 3D game engine of my own for a long time (about 9 months) and wanted to have a way to handle input independent of having to intertwine GLFW calls randomly in my code (thought this would be good design and would modularize input). This also uses an entity-component system designed by Austin Morlan and I like it a lot, it makes thinking about objects much easier, kind of like building blocks. I'm working on collision detection/response soon, but since I had the time I wondered how I did with this event system, since its mostly work of my own.
I'm not sure what design pattern this would be called (maybe inversion of control?), as I'm still an amateur in software engineering, but it uses callback functions and "managers" to subscribe to certain keys and the mouse in general, something I've seen vaguely while modding games.
Things I've noticed myself, but could use some clarification/justification/agreement:
KeySubscription
feels oddly redundant. Can this just be removed?- The
MouseState
struct feels as though it is containing too much information and violates some principle of object oriented design. Maybe I could pass around 2MouseState
objects instead to show thecurrentMouseState
and thepreviousMouseState
? It would be annoying to pass around two states all the time, so maybe I could convert it to aMouse
(containing generic mouse data) and then contain aMouseState
with twoMouse
objects? - Theres some globals around that I didn't want to pass around in every method that needs them, like mCamera (what if I want multiple cameras in the world, like with portals?), or gameWindow (multiple windows in the future?). How can I avoid this? Do I need a gigantic Engine class that keeps track of everything?
keyboard_manager.hpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <memory>
#include <functional>
#include "key_subscription.hpp"
namespace Input {
// Function pointer to a keyboard callback function.
using key_callback_fn = std::function<void(const GLFWKey key, const bool state)>;
class KeyboardManager {
public:
KeyboardManager();
size_t subscribe(key_callback_fn Function, std::array<GLFWKey, MAX_KEYS> Keys, size_t keysLen);
void unsubscribe(size_t indexToRemove);
void callbackAll(GLFWwindow* handle);
private:
// Array of key callbacks.
std::array<key_callback_fn, MAX_KEY_CALLBACKS> keyCallbacks{};
// Map of callback functions to their key subscriptions.
std::array<std::unique_ptr<KeySubscription>, MAX_KEY_CALLBACKS> callbackToSubscription{};
size_t usedCallbacks{};
};
}
keyboard_manager.cpp
#include "keyboard_manager.hpp"
using namespace Input;
KeyboardManager::KeyboardManager() : keyCallbacks(), usedCallbacks(0) {
}
size_t KeyboardManager::subscribe(key_callback_fn Function, std::array<GLFWKey, MAX_KEYS> Keys, size_t keysLen) {
size_t newIndex = usedCallbacks;
keyCallbacks[newIndex] = Function;
callbackToSubscription[newIndex] = std::make_unique<KeySubscription>(Keys, keysLen);
++usedCallbacks;
return newIndex;
}
void KeyboardManager::unsubscribe(size_t indexToRemove) {
size_t lastIndex = usedCallbacks;
keyCallbacks[indexToRemove] = std::move(keyCallbacks[lastIndex]);
callbackToSubscription[indexToRemove] = std::move(callbackToSubscription[lastIndex]);
callbackToSubscription[lastIndex].release();
--usedCallbacks;
}
void KeyboardManager::callbackAll(GLFWwindow* handle) {
for (size_t i = 0; i < usedCallbacks; i++) {
const auto& Function = keyCallbacks[i];
auto& sub = *callbackToSubscription[i].get();
const auto& list = sub.getKeyList();
for (size_t j = 0; j < sub.size(); j++) {
const auto& key = list[j];
const auto& state = glfwGetKey(handle, key);
sub.setKeyState(key, state);
}
for (const auto& key_state : sub.getKeyStates()) {
Function(key_state.first, key_state.second);
}
}
}
key_subscription.hpp
#pragma once
#include "types.hpp"
#include <GLFW/glfw3.h>
#include <array>
#include <algorithm>
#include <unordered_map>
#include <format>
#include <assert.h>
namespace Input {
// Defines the maximum number of keys a KeySubscription can subscribe to.
constexpr size_t MAX_KEYS = 8;
// Defines the maximum number of callback functions that can be stored in the KeyboardManager.
constexpr size_t MAX_KEY_CALLBACKS = 64;
// Enumeration of the GLFW keys used in this program.
enum GLFWKey {
Space = GLFW_KEY_SPACE,
A = GLFW_KEY_A,
D = GLFW_KEY_D,
F = GLFW_KEY_F,
Q = GLFW_KEY_Q,
S = GLFW_KEY_S,
W = GLFW_KEY_W,
Escape = GLFW_KEY_ESCAPE
};
class KeySubscription {
public:
KeySubscription();
KeySubscription(std::array<GLFWKey, MAX_KEYS> _keysList, size_t _keySize) {
keySize = _keySize;
assert(keySize <= MAX_KEYS && std::format("Maximum number of keys is {}, provided {}", MAX_KEYS, keySize).c_str());
std::copy(std::begin(_keysList), std::end(_keysList), std::begin(keysList));
for (size_t i = 0; i < keySize; i++) {
keyStates.insert({ keysList[i], GLFW_RELEASE });
}
}
// Get the keys list.
const std::array<GLFWKey, MAX_KEYS>& getKeyList() const {
return keysList;
}
// Get the key states.
std::unordered_map<GLFWKey, bool> getKeyStates() const {
return keyStates;
}
// Sets the key state.
void setKeyState(GLFWKey key, bool state);
// Get the number of keys used.
size_t size() const { return keySize; }
private:
// The list of keys subscribed.
std::array<GLFWKey, MAX_KEYS> keysList{};
// The key states.
std::unordered_map<GLFWKey, bool> keyStates{};
// The number of keys subscribed.
size_t keySize;
};
}
key_subscription.cpp
#include "key_subscription.hpp"
using namespace Input;
KeySubscription::KeySubscription() : keysList(), keyStates() {
keySize = 0;
}
void KeySubscription::setKeyState(GLFWKey key, bool state) {
keyStates.at(key) = state;
}
mouse_manager.hpp
#pragma once
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <functional>
#include <memory>
#include <array>
namespace Input {
// The mouse state.
using MouseState =
struct {
double mouseXo;
double mouseYo;
double lastMouseX;
double lastMouseY;
double deltaX;
double deltaY;
};
// Function pointer to a mouse callback function.
using mouse_callback_fn = std::function<void(const MouseState& mouse)>;
// Defines the maximum number of callback functions that can be stored in the MouseManager.
constexpr size_t MAX_MOUSE_CALLBACKS = 64;
class MouseManager {
public:
MouseManager();
// Subscribe to the mouse manager with a function.
void subscribe(mouse_callback_fn Function);
void unsubscribe(mouse_callback_fn Function);
// Updates the mouse state. Called automatically by glfw.
void updateMouseState(double xpos, double ypos);
private:
// Array of mouse callback functions.
std::array<std::shared_ptr<mouse_callback_fn>, MAX_MOUSE_CALLBACKS> mouseCallbacks{};
size_t usedCallbacks{};
// Map of mouse callback functions to their indices.
std::unordered_map<std::shared_ptr<mouse_callback_fn>, size_t> mouseCallbackToIndex{};
// The internal mouse state.
MouseState mouse{};
void callbackAll();
};
}
mouse_manager.cpp
#include "mouse_manager.hpp"
using namespace Input;
MouseManager::MouseManager() : mouseCallbacks(), mouseCallbackToIndex(), usedCallbacks(0) {
mouse = MouseState {};
}
void MouseManager::subscribe(mouse_callback_fn Function) {
auto sharedFunction = std::make_shared<mouse_callback_fn>(Function);
size_t newIndex = usedCallbacks;
mouseCallbacks[newIndex] = sharedFunction;
mouseCallbackToIndex.insert({ sharedFunction, newIndex });
++usedCallbacks;
}
void MouseManager::unsubscribe(mouse_callback_fn Function) {
auto sharedFunction = std::make_shared<mouse_callback_fn>(Function);
size_t indexToRemove = mouseCallbackToIndex[sharedFunction];
size_t lastIndex = usedCallbacks;
mouseCallbacks[indexToRemove] = mouseCallbacks[lastIndex];
mouseCallbacks[lastIndex] = 0;
mouseCallbackToIndex.erase(sharedFunction);
--usedCallbacks;
}
void MouseManager::updateMouseState(double xpos, double ypos) {
if (!mouse.lastMouseX || !mouse.lastMouseY) {
mouse.lastMouseX = xpos;
mouse.lastMouseY = ypos;
}
double xo = xpos - mouse.lastMouseX;
double yo = mouse.lastMouseY - ypos;
mouse.lastMouseX = xpos;
mouse.lastMouseY = ypos;
mouse.deltaX = xo;
mouse.deltaY = yo;
callbackAll();
}
void MouseManager::callbackAll() {
for (size_t i = 0; i < usedCallbacks; i++) {
const auto& Function = mouseCallbacks[i];
(*Function.get())(mouse);
}
}
window.hpp
#pragma once
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <cstdint>
#include <string>
#include "input_manager.hpp"
#define BASIC_TITLE "Physics and rendering demo {}"
class Window {
private:
GLFWwindow* handle;
const uint16_t width;
const uint16_t height;
std::string title;
Input::KeyboardManager keyboardManager;
Input::MouseManager mouseManager;
public:
Window(const uint16_t _width, const uint16_t _height, std::string _title);
~Window();
GLFWwindow* getHandle();
void updateTitle(std::string title);
uint16_t getWidth();
uint16_t getHeight();
void updateKeyboard();
Input::KeyboardManager& getKeyboardManager();
Input::MouseManager& getMouseManager();
};
window.cpp
Window::Window(const uint16_t _width, const uint16_t _height, std::string _title) : width(_width), height(_height), title(_title) {
// Unrelated window stuff...
// Prepare our input managers.
mouseManager = Input::MouseManager();
keyboardManager = Input::KeyboardManager();
glfwSetWindowUserPointer(handle, this);
auto cursorPosCallback = [](GLFWwindow* handle, double xpos, double ypos) {
static_cast<Window*>(glfwGetWindowUserPointer(handle))->mouseManager.updateMouseState(xpos, ypos);
};
glfwSetCursorPosCallback(handle, cursorPosCallback);
// We callback the keyboard manager's callbacks later with our other stuff.
}
// Unrelated window stuff...
Input::KeyboardManager& Window::getKeyboardManager() {
return keyboardManager;
}
Input::MouseManager& Window::getMouseManager() {
return mouseManager;
}
void Window::updateKeyboard() {
keyboardManager.callbackAll(handle);
}
main.cpp (consumer perspective)
// Global scoped window
std::unique_ptr<Window> gameWindow;
void main_input(Input::GLFWKey key, bool state) {
using namespace Input;
if (key == GLFWKey::Escape && state == GLFW_PRESS) {
glfwSetWindowShouldClose(gameWindow->getHandle(), true);
}
}
void start() {
// Other stuff...
gameWindow->getKeyboardManager().subscribe(&main_input, {Input::GLFWKey::Escape}, 1);
}
// ...
render_system.hpp
// Externs
extern Coordinator gCoordinator;
extern std::unique_ptr<Window> gameWindow;
using namespace Systems;
// Global camera for now
Entity mCamera;
void camera_movement(const Input::GLFWKey key, const bool state) {
using namespace Input;
auto& transform = gCoordinator.getComponent<Components::Transform>(mCamera);
const auto& orientation = gCoordinator.getComponent<Components::Orientation>(mCamera);
auto& camera = gCoordinator.getComponent<Components::Camera>(mCamera);
static float speed = 0.1f;
if (key == GLFWKey::W && state == GLFW_PRESS) {
transform.Position += speed * orientation.Front;
} else if (key == GLFWKey::A && state == GLFW_PRESS) {
transform.Position -= speed * orientation.Right;
} else if (key == GLFWKey::S && state == GLFW_PRESS) {
transform.Position -= speed * orientation.Front;
} else if (key == GLFWKey::D && state == GLFW_PRESS) {
transform.Position += speed * orientation.Right;
}
}
// FPS camera
void camera_look(const Input::MouseState& mouse) {
static double sensitivity = 0.05f;
auto& orientation = gCoordinator.getComponent<Components::Orientation>(mCamera);
auto& camera = gCoordinator.getComponent<Components::Camera>(mCamera);
double yawOff = mouse.deltaX * sensitivity;
double pitchOff = mouse.deltaY * sensitivity;
camera.Yaw += yawOff;
camera.Pitch += pitchOff;
if (camera.Pitch > 89.0f)
camera.Pitch = 89.0f;
if (camera.Pitch < -89.0f)
camera.Pitch = -89.0f;
camera.update(orientation);
}
void RenderSystem::init() {
mCamera = gCoordinator.createEntity();
gCoordinator.addComponent(mCamera,
Components::Transform{
.Position = glm::vec3(0.0f),
.Rotation = glm::vec3(0.0f),
.Scale = glm::vec3(0.0f)
});
gCoordinator.addComponent(mCamera,
Components::Orientation{
.Front = glm::vec3(0.0f, 0.0f, -1.0f),
.Up = glm::vec3(0.0f, 1.0f, 0.0f),
.Right = glm::vec3(1.0f, 0.0f, 0.0f)
});
gCoordinator.addComponent(mCamera,
Components::Camera{
.Projection = Components::Camera::createProjection(45.0f, 0.1f, 1000.0f, 1920.0f, 1080.0f),
.Pitch = 0.0f,
.Yaw = -90.0f
});
gameWindow->getKeyboardManager().subscribe(&camera_movement, {Input::GLFWKey::W, Input::GLFWKey::A, Input::GLFWKey::S, Input::GLFWKey::D }, 4);
gameWindow->getMouseManager().subscribe(&camera_look);
// Shader stuff...
}
MouseState
struct is fine. Maybe separate along X and Y ? \$\endgroup\$struct Point
which has membersx
&y
and then you havePoint mouse_last(x_val, y_val);
ormouse_last.x=some_value;
etc. On reflection not sure splitting down X & Y is so good, it was late your honour ;-) \$\endgroup\$