Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 52

Mouse Interaction Basics

Recommended resources

Introduction

Welcome back! In our last lesson, we built a robust, non-blocking keyboard input system using callbacks and a two-array state tracking method. This replaced our simple getchar() and laid the foundation for real-time interaction in our game.

This lesson builds directly on that foundation. Our learning outcome is to handle mouse movement and button clicks. We will extend our existing input module to track the mouse's cursor position and the state of its buttons. The patterns we'll use—callbacks for events and state arrays for queries—will be very familiar from the previous lesson.

Specifically, we will:

  1. Implement state tracking for mouse buttons (press, release, hold).
  2. Add functions to get the current mouse cursor position.
  3. Discuss the critical difference between screen coordinates and game world coordinates.
  4. Preview how we'll use this system for 3D camera control in an upcoming lesson.

Let's enhance our input system.


1. Handling Mouse Button Clicks

Handling mouse buttons is almost identical to handling keyboard keys. We'll use a callback to be notified of button presses and releases, and we'll store that information in current and previous state arrays to determine press, release, and hold states.

The official GLFW documentation is the best place to start.

GLFW - Input guide

The GLFW Input guide explains how to manage mouse button input. We'll focus on the functions for setting a callback and for polling the button state.

Read the subsection titled 'Mouse button input'. Pay close attention to the glfwSetMouseButtonCallback function and its callback signature. Notice how the action parameter can be GLFW_PRESS or GLFW_RELEASE, just like with the key callback.

As you read, you can see the parallels to our keyboard system. The mouse_button_callback gives us the button that was acted upon and the action that occurred. We can use this to update a state array.

Let's update our input module to incorporate this.

Implementation Steps

  1. State Arrays: We'll add two new boolean arrays to input.c to store the current and previous states of the mouse buttons. GLFW supports up to 8 mouse buttons.

  2. Callback Function: We'll create a mouse_button_callback function that updates the g_current_mouse_buttons array based on the events GLFW sends us.

  3. Register the Callback: In Input_Init, we will register this new callback using glfwSetMouseButtonCallback.

  4. Update Loop: In Input_Update, we'll copy the g_current_mouse_buttons state to g_previous_mouse_buttons.

  5. Query Functions: We'll add IsMouseButtonDown, IsMouseButtonPressed, and IsMouseButtonReleased to our public API in input.h.

The implementation is very straightforward. Below are the changes for input.h and input.c.

input.h (additions)

// input.h

// ... existing keyboard functions ...

// --- Mouse Button State Queries ---

// Returns true if the mouse button is currently held down
bool IsMouseButtonDown(int button);

// Returns true only on the frame the mouse button was first pressed
bool IsMouseButtonPressed(int button);

// Returns true only on the frame the mouse button was released
bool IsMouseButtonReleased(int button);

input.c (additions and modifications)

// input.c



#include "input.h"
#include <GLFW/glfw3.h>
#include <string.h>

// --- State Variables (static) ---



#define MAX_KEYS 350
#define MAX_MOUSE_BUTTONS 8 // GLFW supports up to 8 mouse buttons

static bool g_current_keys[MAX_KEYS];
static bool g_previous_keys[MAX_KEYS];

// New state for mouse buttons
static bool g_current_mouse_buttons[MAX_MOUSE_BUTTONS];
static bool g_previous_mouse_buttons[MAX_MOUSE_BUTTONS];

// --- Callbacks (static) ---

static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
    // ... no changes here ...
}

// New callback for mouse buttons
static void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) {
    if (button < 0 || button >= MAX_MOUSE_BUTTONS) {
        return;
    }

    if (action == GLFW_PRESS) {
        g_current_mouse_buttons[button] = true;
    } else if (action == GLFW_RELEASE) {
        g_current_mouse_buttons[button] = false;
    }
}

// --- Public API ---

void Input_Init(GLFWwindow* window) {
    memset(g_current_keys, 0, sizeof(g_current_keys));
    memset(g_previous_keys, 0, sizeof(g_previous_keys));

    // Initialize mouse button states
    memset(g_current_mouse_buttons, 0, sizeof(g_current_mouse_buttons));
    memset(g_previous_mouse_buttons, 0, sizeof(g_previous_mouse_buttons));

    glfwSetKeyCallback(window, key_callback);
    // Register the new mouse button callback
    glfwSetMouseButtonCallback(window, mouse_button_callback);
}

void Input_Update() {
    memcpy(g_previous_keys, g_current_keys, sizeof(g_current_keys));
    // Update mouse button previous state
    memcpy(g_previous_mouse_buttons, g_current_mouse_buttons, sizeof(g_current_mouse_buttons));
}

// ... existing keyboard functions IsKeyDown, IsKeyPressed, IsKeyReleased ...

// --- New Mouse Button Query Functions ---

bool IsMouseButtonDown(int button) {
    if (button < 0 || button >= MAX_MOUSE_BUTTONS) return false;
    return g_current_mouse_buttons[button];
}

bool IsMouseButtonPressed(int button) {
    if (button < 0 || button >= MAX_MOUSE_BUTTONS) return false;
    return g_current_mouse_buttons[button] && !g_previous_mouse_buttons[button];
}

bool IsMouseButtonReleased(int button) {
    if (button < 0 || button >= MAX_MOUSE_BUTTONS) return false;
    return !g_current_mouse_buttons[button] && g_previous_mouse_buttons[button];
}

With these changes, our input system now fully supports mouse button state tracking, using the exact same logic as our keyboard system.


2. Handling Mouse Movement

Unlike button presses, which are discrete events, mouse movement is continuous. There are two primary ways to get the cursor's position with GLFW:

  1. Polling: At any time, you can ask GLFW for the cursor's current position using glfwGetCursorPos. This is simple and often sufficient.
  2. Callback: You can register a glfwSetCursorPosCallback to be notified every time the mouse moves. This is useful if you need to process every single micro-movement, but can be overkill for many applications.

For our engine, a polling-based approach is a great place to start. We can create a function that our game logic can call once per frame to get the latest coordinates.

GLFW - Input guide

Let's return to the GLFW Input guide to see the functions for getting the cursor position.

Read the subsection titled 'Cursor position'. Note the two functions glfwGetCursorPos (for polling) and glfwSetCursorPosCallback (for event-driven notifications). We will focus on implementing the polling method first.

The Cherno's video on input provides a good practical demonstration of implementing polling, although it's in C++. The core GLFW calls are identical.

Input Polling | Game Engine series

This video from The Cherno's game engine series discusses polling for input, including mouse position. Watching his implementation will help solidify the concept.

Watch the following segments: 16:23 - 18:43: He implements IsMouseButtonPressed using glfwGetMouseButton, which is the polling equivalent of our callback system. This is a good review. 18:43 - 22:05: Here, he implements polling for the cursor position using glfwGetCursorPos. Notice how he needs the window handle to do this. He also discusses returning both X and Y values, opting for a std::pair in C++. We will use output parameters (pointers) to achieve the same result in C.

Implementation Steps

To implement this, we need a way to access the GLFW window handle from within our input functions. A simple way to do this in C is to store it in a static global variable within input.c.

  1. Store Window Handle: Add a static GLFWwindow* g_window = NULL; to input.c and set it in Input_Init.
  2. State Variables: We will also add static variables to hold the mouse coordinates.
  3. Query Function: Create a GetMousePosition function in input.h and implement it in input.c.
  4. Update Mouse Position: In Input_Update, we'll call glfwGetCursorPos to refresh our stored coordinates once per frame.

input.h (additions)

// input.h

// ... existing functions ...

// --- Mouse Position State Queries ---

// Gets the mouse cursor position relative to the top-left of the window
void GetMousePosition(double* xpos, double* ypos);

input.c (additions and modifications)

// input.c

// ... includes ...

// ... state arrays ...

// New state for mouse position
static double g_mouse_x = 0.0;
static double g_mouse_y = 0.0;

// New static variable to hold the window pointer
static GLFWwindow* g_window = NULL;

// ... callbacks ...

void Input_Init(GLFWwindow* window) {
    g_window = window; // Store the window handle

    // ... memset calls ...

    glfwSetKeyCallback(window, key_callback);
    glfwSetMouseButtonCallback(window, mouse_button_callback);
}

void Input_Update() {
    // ... memcpy calls for keys and mouse buttons ...

    // Poll for the latest mouse position
    glfwGetCursorPos(g_window, &g_mouse_x, &g_mouse_y);
}

// ... IsMouseButton... functions ...

// --- New Mouse Position Query Function ---

void GetMousePosition(double* xpos, double* ypos) {
    *xpos = g_mouse_x;
    *ypos = g_mouse_y;
}

3. The Coordinate System Mismatch

This is a subtle but extremely important point. The mouse coordinates you get from GLFW are in screen space. This means:

  • The origin (0, 0) is at the top-left corner of the window.
  • The Y-axis value increases as you move down the screen.

However, the coordinate system used in most 3D mathematics and by OpenGL (after projection) has:

  • The origin (0, 0) typically at the center or bottom-left.
  • The Y-axis value increasing as you move up.
Y-Up vs. Y-Down Coordinate Systems
This image illustrates the difference between the 'y-down' system used for screen coordinates (like mouse input) and the 'y-up' system used for 3D world coordinates. We will need to convert between these.

For now, our GetMousePosition function correctly returns the raw screen coordinates. When we implement our 3D camera, we will need to remember to convert the Y-coordinate. For example, to map a screen Y to a typical graphics Y, you would use: graphics_y = window_height - screen_y.


4. Integrating and Testing

Let's add a quick test to our main.c to see our new system in action.

main.c (inside the main loop)

// In main.c's while loop, after StateManager_Update

double mouse_x, mouse_y;
GetMousePosition(&mouse_x, &mouse_y);

// Check if the left mouse button was pressed THIS frame
if (IsMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT)) {
    printf("Left mouse button pressed at: (%.2f, %.2f)\n", mouse_x, mouse_y);
}

// Check if the right mouse button is HELD DOWN
if (IsMouseButtonDown(GLFW_MOUSE_BUTTON_RIGHT)) {
    printf("Right mouse button is being held down!\n");
}

Run your application, and you should now see messages printed to the console when you click the left mouse button, and a stream of messages while you hold the right button.


Conclusion

You have successfully extended your input system to handle mouse buttons and movement. This is a critical step towards creating interactive 3D scenes.

Key Takeaways:

  • Mouse buttons can be handled using the same two-array state tracking system as keyboard keys, powered by the glfwSetMouseButtonCallback.
  • Mouse position can be efficiently retrieved once per frame by polling with glfwGetCursorPos.
  • A crucial concept in graphics programming is the difference between screen coordinates (y-down) and world/NDC coordinates (y-up). We must account for this when using mouse input to manipulate the 3D world.

Preview of the Next Lesson:
We now have robust functions for querying raw keyboard and mouse state (IsKeyPressed, GetMousePosition, etc.). However, hardcoding GLFW_KEY_W or GLFW_MOUSE_BUTTON_LEFT directly in our game logic is inflexible. In the next lesson, we will create an abstract input mapping system. This will allow us to decouple raw input from game actions, defining abstract concepts like "Jump" or "Fire" that can be mapped to any key or button, making our code cleaner and our controls easily reconfigurable.