Building Simple GLFW Input Handling System
Goal
I want to build a simple OpenGL app for rendering 3D scenes as a learning exersise to get better at OpenGL, C++, and graphics programming at large. Part of this is project requires handling input from mouse and keyboard, and because learnopengl.com uses GLFW, I'm building a more abstracted input management system on top of GLFW's input handling.
High Level Design
I'm going for a simple design that has two major parts inside of one single Input class:
- Input querying functions
- GLFW and instance callback management
Input Querying Functions
These are just the functions that are called publicly to check the state of keys/mouse buttons. State will be stored in the Input object, and updated by the callbacks. These querying functions will essentially just be getters for whats stored in the Input objects with some minor logic.
GLFW and Instance Callback Management
This portion is the larger chunk of the system. GLFW uses C-style function pointers for callbacks, which means I can't directly use member functions as callbacks. To get around this, I'm going to use static functions as "middle-men" that will forward the calls to the appropriate Input instance functions.
This creates the distinction between the static GLFW callbacks, and the instance callback handlers as described above.
Implementation
Input Class
This is what our header will look like:
class Input
{
private:
GLFWwindow* m_window;
bool m_keys_curr[GLFW_KEY_LAST + 1] = {false};
bool m_keys_prev[GLFW_KEY_LAST + 1] = {false};
bool m_mouse_buttons_curr[GLFW_MOUSE_BUTTON_LAST] = {false};
bool m_mouse_buttons_prev[GLFW_MOUSE_BUTTON_LAST] = {false};
double m_mouse_x = 0.0;
double m_mouse_y = 0.0;
double m_mouse_x_prev = 0.0;
double m_mouse_y_prev = 0.0;
public:
Input(GLFWwindow* window);
~Input();
/* call once per frame before input. copies current state to previous state */
void update();
/* key input */
bool isKeyDown(int key) const;
bool isKeyPressed(int key) const;
bool isKeyReleased(int key) const;
/* mouse button and position input */
bool isMouseButtonDown(int button) const;
bool isMouseButtonPressed(int button) const;
bool isMouseButtonReleased(int button) const;
void getMousePos(double& x, double& y) const;
void getMouseDelta(double& dx, double& dy) const;
private:
/* GLFW callbacks */
static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
static void cursorPosCallback(GLFWwindow* window, double xpos, double ypos);
static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods);
/* instance callback handlers */
void handleKey(int key, int scancode, int action, int mods);
void handleCursorPos(double xpos, double ypos);
void handleMouseButton(int button, int action, int mods);
};
Notice the separation between the static GLFW callbacks and the instance callback handlers I mentioned earlier!
As we see, the class just stores state, has querying functions, and the callbacks. There's also an update function that will be called once per frame to copy the current state to the previous state.
Constructor and Destructor
I like the style of initialize on construction, so I'm going to be doing as much "automatic" work as I can in our constructor and destructor. This is usually called RAII (Resource Acquisition Is Initialization) in C++ and is pretty common.
Firstly, we need a way to associate the GLFW window with our Input instance. GLFW allows us to set a user pointer on the window, which we can use to store a pointer to our Input instance. This user pointer can be anything and is stored as a void*, so we can just store our Input* there and cast it back to Input* when we need it.
glfwSetWindowUserPointer(m_window, this);
where m_window is our GLFWwindow* passed into the constructor, and this is the pointer to the current instance of the Input class.
Next, we need to register our static callback functions with GLFW:
glfwSetKeyCallback(m_window, Input::keyCallback);
glfwSetCursorPosCallback(m_window, Input::cursorPosCallback);
glfwSetMouseButtonCallback(m_window, Input::mouseButtonCallback);
This just tells GLFW to call our static functions whenever the respective event occurs.
If we put that all together, our constructor and destructor look like this:
Input::Input(GLFWwindow* window) : m_window(window)
{
// set first before registering callbacks
glfwSetWindowUserPointer(m_window, this);
// register callbacks
glfwSetKeyCallback(m_window, Input::keyCallback);
glfwSetCursorPosCallback(m_window, Input::cursorPosCallback);
glfwSetMouseButtonCallback(m_window, Input::mouseButtonCallback);
}
Input::~Input()
{
// unregister callbacks
if (m_window)
{
glfwSetKeyCallback(m_window, nullptr);
glfwSetCursorPosCallback(m_window, nullptr);
glfwSetMouseButtonCallback(m_window, nullptr);
}
}
GLFW allows us to unregister callbacks by setting them to NULL, or nullptr in C++.
Update Function
The update function is pretty straightforward. We just need to copy the current state arrays to the previous state arrays, and store the previous mouse position.
void Input::update()
{
// copy current state to previous state for next frame comparisons
std::memcpy(m_keys_prev, m_keys_curr, sizeof(m_keys_curr));
std::memcpy(m_mouse_buttons_prev, m_mouse_buttons_curr, sizeof(m_mouse_buttons_curr));
m_mouse_x_prev = m_mouse_x;
m_mouse_y_prev = m_mouse_y;
}
Instance Callback Handlers
Again, these functions are called by our static callbacks, but they can do stuff on the instance level. We have three that we are implementing: key handling, cursor position handling, and mouse button handling. Same as the static callbacks.
Note their function signatures. Most are pretty straightforward, but lets take a look at what scancode and mods are, specifically for the glfwSetKeyCallback.
Per the GLFW docs on the function pointer type for keyboard key callbacks (what a mouthful), we can see that:
scancode : The platform-specific scancode of the key.
mods : Bit field describing which modifier keys were held down.
For us, that just means that we will not be using scancodes because we only care about the key name itself, not the actual physical location on our keyboard. Not very useful if all you want to do is check if the "W" key is pressed, for example. As for mods, this could be useful since we can see if keys like Shift, Ctrl, or Alt were held down when the event was invoked. For now, I won't be using it though. Maybe I will add this later.
Here is how I'm implementing the handleKey callback:
void Input::handleKey(int key, int /* scancode */, int action, int /* mods */)
{
if (key < 0 || key > GLFW_KEY_LAST)
return;
if (action == GLFW_PRESS || action == GLFW_REPEAT)
{
m_keys_curr[key] = true;
}
else if (action == GLFW_RELEASE)
{
m_keys_curr[key] = false;
}
}
The other two handlers are very similar, so I won't bother putting them here. These mostly boil down to doing bounds checking if needed then updating state.
GLFW Static Callbacks
This part was harder for me to understand initially, but its actually pretty simple once you understand what GLFW is allowing you to do with the user pointer.
For this part, if you remember the high-level flow, we have static functions that are registered with GLFW as callbacks. These static functions can't access instance data directly. So how do we get to the instance level?
Remember in our constructor, we called glfwSetWindowUserPointer and passed this and window? This is where that comes into play. Inside our static callback functions, we can retrieve the user pointer we set earlier using glfwGetWindowUserPointer, cast it back to Input*, and then call the appropriate instance handler!
GLFW allows us to do this trick, and possibly other tricks that I don't know about yet because of how flexible the user pointer system is.
Here's how the GLFW static key callback looks:
void Input::keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
// retrieve Input instance pointer from GLFW window user pointer
Input* input = static_cast<Input*>(glfwGetWindowUserPointer(window));
if (input)
{
input->handleKey(key, scancode, action, mods);
}
}
It's probably a good idea to check if input exists after getting it in case the user pointer isn't set for some reason. Again, the other two functions are very similar, so I won't put them here.
Input Querying Functions
Finally, we have the public facing querying functions that allow us to check the state of keys, mouse buttons, whatever. These functions mostly just do proper bounds checking, read stored state, and return true/false values based on whatever logic we need.
For example, here is the isKeyReleased function that tells us if a key was released this frame:
bool Input::isKeyReleased(int key) const
{
if (key < 0 || key > GLFW_KEY_LAST)
return false;
// true if key isn't down now, but was last frame
return (!m_keys_curr[key] && m_keys_prev[key]);
}
And that's it! All the other querying functions are very similar to that.
Something Extra
I previously implemented a custom Window class that wraps GLFW window stuff. I wanted to be able to pass in a Window* type instead of GLFWwindow* to the Input constructor for a bit better abstraction.
I did some research and found a cool C++11 feature called constructor delegation that allows one constructor to call another constructor in the same class. This was exactly what I needed!
This is all I did to add that functionality:
Input::Input(Window* window) : Input(window->getGLFWWindow())
{
}
You can also add extra logic in the body of the delegating constructor if you want to. As a note: stuff in the body of a constructor always runs after the initializer list!
Thanks C++11!
Edit after initial post
So after writing this (literally a day later), I started to integrate this input system into the rest of my project. I have a custom Window class that also handles some GLFW stuff related only to window management. That's fine and dandy, expect that I didn't realize that I was also using the GLFW user pointer for the Window instance! That's one GLFWwindow* for two different classes!
This is obviously bad, since setting the user pointer in one class overwrites the other. To fix this, I set up the Window class as a one-to-many owner of Input instances. This means that the Window class will store a list of Input instances, and when a GLFW event occurs, it will forward the event to all registered Input instances. Now, only one class manages the user pointer, and Input instances still get their events.
TL;DR: Don't have different classes using the GLFW user pointer on the same window. Just make one class manage it, and forward events or other things if needed.
Conclusion
That's it for my simple GLFW input system for now. I want to keep working on this project in other areas, so maybe later I will come back to this and add more features or just make the abstraction layer a bit better (not as reliant on GLFW).
Thanks for reading!