Build a Game Engine

Mouse Input

Prerequisites

Article

With our keyboard implemented, we can now implement our next device: the mouse. We will again be extending our InputDevice base class and ensuring we are registered with, discoverable by, and managing events through our InputSystem.

We'll be extending GLFW's mouse functionality so that we can always switch to another framework later on.

Most of the functionality will be implementations of our InputDevice functions - however, a mouse has a special property: a position on the screen. We'll need an additional device-specific function to find the current position. In addition, a mouse has a scroll wheel, so we'll treat that as another button state or axis on which we can poll.

Of course a mouse can do more as well: we could capture mouse movement, drag and drop, cursor enter/exit events, etc. These are things we will use in our game editor, but since we're using Qt for that, we'll also use Qt's mouse object. They're generally not something you'll need in a game.

Let's look at our Mouse class:

enum UMouseButtons {
    BUTTON_LEFT = 1,
    BUTTON_RIGHT,
    BUTTON_MIDDLE,
    BUTTON_SCROLL,
    BUTTON_MOUSE_LAST
};

class UENGINE_API UMouse : public UInputDevice {
public:
    UMouse();
    ~UMouse() = default;

    /**
     * Initialize
     */
    void Initialize();

    /**
     * Get the state of a button
     */
    float GetButtonState(int button);

    /**
     * Process a button action
     */
    void SetButtonState(int button, float state);

    /**
     * Get device type
     */
    int Type() { return UInputDeviceType::MOUSE; }

    /**
     * Get cursor position
     */
    void GetCursorPosition(int& x, int& y);

protected:
    // Button states
    int m_buttonStates[BUTTON_MOUSE_LAST];
};

At the top is our enumeration of buttons - left, middle, right, and the scroll wheel. In this article, we'll use the scroll button to track movement on the Y-axis. It is possible with a trackpad or a mouse with a ball that there is X-axis movement - as you'll see, it would be fairly easy to extend if you wanted to capture that.

We will again use the Initialize function to register callbacks with GLFW that we will listen for to set button states. Here is the Initialize function:

void UMouse::Initialize() {
    UWindow* window = UApplication::Instance()->WindowHandle();
    glfwSetMouseButtonCallback(window->Handle(), UMouseCallback);
    glfwSetScrollCallback(window->Handle(), UMouseScrollCallback);
}

This gets our window handle from the UApplication singleton and let's GLFW know we want to receive callbacks for mouse button presses and releases and scrolling movement from the mouse wheel.

Since both functions are similar, we'll look at both at the same time:

void UMouseCallback(GLFWwindow* window, int button, int action, int mods) {
    // Get our input system
    UInputSystem* ioSystem = GetUSystem<UInputSystem>();

    // Find our mouse device
    UMouse* mouse = ioSystem->FindInputDevice<UMouse>();

    // Dispatch event
    int state = (action == GLFW_PRESS) ? 1 : 0;
    mouse->SetButtonState(button, state);

    // Potentially trigger an action
    ioSystem->TriggerAction(mouse, button, state);
}

void UMouseScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
    // Get our input system
    UInputSystem* ioSystem = GetUSystem<UInputSystem>();

    // Find our mouse device
    UMouse* mouse = ioSystem->FindInputDevice<UMouse>();

    // Set state (additive)
    float scroll = mouse->GetButtonState(UMouseButtons::BUTTON_SCROLL);
    scroll += yoffset;
    mouse->SetButtonState(UMouseButtons::BUTTON_SCROLL, scroll);

    // Potentially trigger an action
    ioSystem->TriggerAction(mouse, UMouseButtons::BUTTON_SCROLL, yoffset);
}

Both of these functions call our GetUSystem service locator function (discussed in the article on ECS and the World), find our Mouse, and call SetButtonState and then TriggerAction on the InputSystem (which will fire an event if a device/button pair triggers an action). The UMouseCallback for button presses simply sets the state to one (1) or zero (0) depending on whether it was pressed or not.

The UMouseScrollCallback function is a little more interesting: it accumulates scrolling motion into the scroll "button" state. That is, this callback, which is interrupt based, may be called multiple times before the next game loop - for example, once with 1.0f, once with 2.0f, once with 1.5f, and finally with 0.0f. We want to ensure that the user gets all scrolling motion since their last check of the button state, so we accumulate the changes.

Likewise, once the user does check the button state, then we'll want to clear it. You can see us doing exactly that in GetButtonState:

float UMouse::GetButtonState(int button) {
    UASSERT(0 <= button && button < BUTTON_MOUSE_LAST, "Button outside of range.");

    // If mouse scroll, reset since it is cumulative
    if (button == UMouseButtons::BUTTON_SCROLL) {
        float amount = m_buttonStates[button];
        m_buttonStates[button] = 0.0f;
        return(amount);
    }

    return(m_buttonStates[button]);
}

First we do a range check, and then we return the state of the button. For left, right, and middle button "clicks", this will be 1.0f or 0.0f. However, if we're checking the scroll button state, it will return the amount it has been scrolled since the last check and will reset the button state to zero.

We also have the SetButtonState implementation, which also does a range check and then sets the state to whatever value has been passed in.

void UMouse::SetButtonState(int button, float state) {
    UASSERT(0 <= button && button < BUTTON_MOUSE_LAST, "Button outside of range.");
    m_buttonStates[button] = state;
}

Finally, we have a function called GetCursorPosition. This will get the current position of the cursor from GLFW at the time the function is called:

void UMouse::GetCursorPosition(int& x, int& y) {
    UWindow* window = UApplication::Instance()->WindowHandle();

    double xpos, ypos;
    glfwGetCursorPos(window->Handle(), &xpos, &ypos);

    x = xpos;
    y = ypos;
}

You could extend this function in your script or game code to look for changes in position (let's say if you were dragging something).

Conclusion

That's it! Building on our InputDevice class, we have now implemented both keyboard and mouse inputs. For PC users, this would probably be enough, but the true flexibility of the action system we've created comes from supporting a wide range of input devices that all map to the same shared actions and states. Next, we'll round out our input section by implementing a game pad.