When we’re working on a mouse-based game, creating an engaging user interface typically requires us to change the visual appearance of the cursor.
For example, we might customize the cursor to fit the theme of our game. We may also want to apply further modifications to the cursor based on what the user is pointing at, like changing the cursor to a sword if the pointer is hovering over an enemy. This helps players quickly understand what effect clicking will have.
This lesson will guide you through the various cursor customization options available in SDL2. We’ll cover:
Similar to trapping the cursor, we also often want to hide it. That can be done with SDL_ShowCursor()
. We pass SDL_ENABLE
to show the cursor, or SDL_DISABLE
to hide it:
// Show the Cursor
SDL_ShowCursor(SDL_ENABLE);
// Hide the Cursor
SDL_ShowCursor(SDL_DISABLE);
We can also use SDL_ShowCursor()
to determine the current visibility of the cursor, without changing it. To do this, we pass SDL_QUERY
as the argument, and examine the return value:
if (SDL_ShowCursor(SDL_QUERY) == SDL_ENABLE) {
std::cout << "Cursor is visible";
} else {
std::cout << "Cursor is hidden";
}
Cursor is visible
In addition to returning the new visibility of the cursor (SDL_ENABLE
or SDL_DISABLE
), the SDL_GetError()
function can alternatively return a negative value. This happens when something goes wrong, and we can call SDL_GetError()
to get an explanation of the error:
if (SDL_ShowCursor(SDL_DISABLE) < 0) {
std::cout << "Error hiding cursor: "
<< SDL_GetError();
}
We pass an SDL_SystemCursor
enum value describing which cursor we’d like to create. For example, we can create the default arrow cursor by passing SDL_SYSTEM_CURSOR_ARROW
:
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
The value returned by SDL_CreateSystemCursor()
is an SDL_Cursor
pointer (an SDL_Cursor*
):
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};
We should treat these SDL_Cursor
objects as opaque - that is, they are not designed for us to read or modify their properties. However, we still need these SDL_Cursor
pointers for use with other SDL functions that we’ll cover later.
SDL_SystemCursor
ValuesThe SDL_SystemCursor
enum includes a set of values that are commonly cursors. Its possible values are as follows:
SDL_SYSTEM_CURSOR_ARROW
: The default cursor, usually designed to look like an arrowSDL_SYSTEM_CURSOR_HAND
: An alternative cursor designed to look like a handSDL_SYSTEM_CURSOR_CROSSHAIR
: An alternative cursor designed to look like a crosshairSDL_SYSTEM_CURSOR_IBEAM
: The I-beam cursor, typically used for text editingSDL_SYSTEM_CURSOR_WAIT
: A cursor designed to indicate something is loadingSDL_SYSTEM_CURSOR_WAITARROW
: An alternative design to the loading cursorSDL_SYSTEM_CURSOR_NO
: Used to indicate blocked or disabled actionsMost systems have cursors designed to indicate resizing actions, such as when you move your pointer next to the edge of a resizable window. We can load these system cursors using the following enum values:
SDL_SYSTEM_CURSOR_SIZENWSE
: Double arrow pointing northwest and southeast,SDL_SYSTEM_CURSOR_SIZENESW
: Double arrow pointing northeast and southwestSDL_SYSTEM_CURSOR_SIZEWE
: Double arrow pointing west and eastSDL_SYSTEM_CURSOR_SIZENS
: Double arrow pointing north and southSDL_SYSTEM_CURSOR_SIZEALL
: Four pointed arrow pointing north, south, east, and westIt is possible for the SDL_Cursor
pointer returned by SDL_CreateSystemCursor()
to be a nullptr
. This represents an error, and we can call SDL_GetError()
for an explanation:
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};
if (!Cursor) {
std::cout << "Failed to create cursor: "
<< SDL_GetError();
}
Once we’ve created a cursor and have access to it through an SDL_Cursor
pointer, we can instruct SDL to use that cursor for our mouse. We do this by passing the pointer to SDL_SetCursor()
:
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};
SDL_SetCursor(Cursor);
When we create a cursor using a function like SDL_CreateSystemCursor()
, SDL allocates the memory required for that cursor and manages it internally.
However, when we no longer need that cursor, we should communicate that to SDL so it can free the memory and prevent leaks. We do this by passing the SDL_Cursor*
to SDL_FreeCursor()
:
// Create the cursor
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};
// Use the cursor...
// Free it
SDL_FreeCursor(Cursor);
Note this only applies to cursors we create using functions like SDL_CreateSystemCursor()
or SDL_CreateColorCursor()
. Every invocation of a cursor creation function should be matched with an invocation to SDL_FreeCursor()
.
Functions that retrieve a pointer to an existing SDL_Cursor
, such as SDL_GetCursor()
and SDL_GetDefaultCursor()
, do not allocate new memory, so we do not need to free the pointer they return.
We can get the cursor we are currently using SDL_GetCursor()
. This will return the SDL_Cursor
pointer applied from our most recent call to SDL_SetCursor()
, or the default cursor if we never changed it:
SDL_Cursor* Cursor{
SDL_GetCursor()
};
We can use this to check if a specific cursor is currently active:
SDL_Cursor* Crosshair{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR)
};
if (SDL_GetCursor() != Crosshair) {
std::cout << "Crosshair cursor is not active";
}
SDL_SetCursor(Cursor);
if (SDL_GetCursor() == Crosshair) {
std::cout << "\nBut now it is";
}
Crosshair cursor is not active
But now it is
We can get a pointer to the system’s default cursor using the SDL_GetDefaultCursor()
function:
SDL_Cursor* DefaultCursor{
SDL_GetDefaultCursor()
};
This is primarily used when we need to revert a change we made using SDL_SetCursor()
:
SDL_Cursor* Crosshair{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR)
};
// Use a non-default cursor
SDL_SetCursor(Crosshair);
// ...
// Revert to the default
SDL_SetCursor(SDL_GetDefaultCursor());
If SDL_GetDefaultCursor()
fails, it will return a nullptr
, and we can call SDL_GetError()
to understand why:
SDL_Cursor* DefaultCursor{
SDL_GetDefaultCursor()
};
if (!DefaultCursor) {
std::cout << "Unable to get default cursor: "
<< SDL_GetError();
}
The first step of creating a custom cursor is to get the pixel data for that cursor into an SDL_Surface
. Remember, we can use the SDL_image library to load an image file into an SDL_Surface
:
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
int main(int, char**) {
// 1. Initialize SDL
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
// 2. Load image into an SDL_Surface
SDL_Surface* Surface{IMG_Load("cursor.png")};
if (!Surface) {
std::cout << "IMG_Load Error: "
<< SDL_GetError();
}
// 3. Use surface
// ...
// 4. Cleanup resources
SDL_FreeSurface(Surface);
IMG_Quit();
SDL_Quit();
return 0;
}
Once we have our SDL_Surface
, we can create a cursor from it using the SDL_CreateColorCursor()
function. We pass the SDL_Surface
pointer as the first argument, in addition to two integers representing horizontal (x
) and vertical (y
) coordinates:
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
int main(int, char**) {
// 1. Initialize SDL
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
// 2. Load image into an SDL_Surface
SDL_Surface* Surface{IMG_Load("cursor.png")};
if (!Surface) {
std::cout << "IMG_Load Error: "
<< SDL_GetError();
}
// 3. Create and use cursor
SDL_Cursor* Cursor{SDL_CreateColorCursor(
Surface, 10, 20)};
SDL_SetCursor(Cursor);
// The SDL_CreateColorCursor() function copies
// the image data so we can safely free the
// surface after it completes
SDL_FreeSurface(Surface);
// 4. Game Loop
// ...
// 5. Cleanup resources
IMG_Quit();
SDL_Quit();
return 0;
}
The integer x
and y
arguments tell SDL where the "hot spot" is within the cursor image. The hot spot is the exact part of the cursor image that corresponds to what the user is pointing at.
For example, if our cursor was an image of an arrow, the hot spot would be the exact tip of that arrow. For a cursor designed to look like a hand, the hot spot would be the tip of the finger.
The x
value is the distance of this pixel from the left edge of the image, and the y
value is the distance from the top edge.
If SDL_CreateCursor()
is unable to create a cursor, it will return a nullptr
. We can react accordingly to this, and call SDL_GetError()
if we want an explanation of what went wrong:
SDL_Cursor* Cursor{SDL_CreateColorCursor(
nullptr, // Passing nullptr as surface
10, 20
)};
if (!Cursor) {
std::cout << "Error creating cursor: "
<< SDL_GetError();
}
Error creating cursor: Parameter 'surface' is invalid
To manage custom cursors and take care of all the memory management required, we’d commonly want to create a dedicated class for this. For example:
// Cursor.h
#pragma once
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
class Cursor {
public:
Cursor(std::string Path, int HotX, int HotY)
: CursorSurface{IMG_Load(Path.c_str())},
CursorPtr{nullptr},
HotspotX{HotX},
HotspotY{HotY}
{
if (!CursorSurface) {
std::cout << "IMG_Load Error: "
<< SDL_GetError();
return;
}
CursorPtr = SDL_CreateColorCursor(
CursorSurface, HotspotX, HotspotY);
if (!CursorPtr) {
std::cout <<
"SDL_CreateColorCursor Error: "
<< SDL_GetError();
SDL_FreeSurface(CursorSurface);
CursorSurface = nullptr;
}
}
~Cursor() {
if (CursorPtr) {
SDL_FreeCursor(CursorPtr);
}
if (CursorSurface) {
SDL_FreeSurface(CursorSurface);
}
}
void Activate() const {
if (CursorPtr) {
SDL_SetCursor(CursorPtr);
}
}
void Deactivate() {
SDL_SetCursor(SDL_GetDefaultCursor());
}
private:
SDL_Cursor* CursorPtr;
SDL_Surface* CursorSurface;
int HotspotX, HotspotY;
};
The following program uses this class to create a custom cursor. The cursor is applied when the player presses 1
, and reverted to the default when they press 2
:
// main.cpp
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
#include "Cursor.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
// Create a window
SDL_Window* Window{SDL_CreateWindow(
"Cursor Example",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 600, 0
)};
// Create a Cursor instance
Cursor CustomCursor{"cursor.png", 16, 16};
// Event loop
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_QUIT) {
SDL_DestroyWindow(Window);
IMG_Quit();
SDL_Quit();
return 0;
}
if (E.type == SDL_KEYDOWN) {
if (E.key.keysym.sym == SDLK_1) {
CustomCursor.Activate();
} else if (E.key.keysym.sym == SDLK_2) {
CustomCursor.Deactivate();
}
}
}
}
}
SDL_CreateCursor()
SDL also includes the SDL_CreateCursor()
function, but it is less capable and more difficult to use than SDL_CreateColorCursor()
.
The SDL_CreateCursor()
function can only be used to create monochrome cursors, and the API for providing the pixel data is less friendly than the SDL_Surface
used by SDL_CreateColorCursor()
.
As such, we tend to not use it, but the official documentation is available here for those interested.
We’re not forced to use SDL’s mechanisms for cursor customization if we don’t want to. We can take on full responsibility for cursor rendering within our code. This allows us to do more advanced things with the cursor, such as animation or physics-based motion
To set this up, we’d hide the system-managed cursor using SDL_ShowCursor(SDL_DISABLE)
, and then render our object on the screen using, for example, the surface blitting techniques we covered earlier.
We’d update the blitting position based on SDL_MouseMotion
events coming through our event loop, or SDL_GetMouseState()
calls within a tick function.
Below, we’ve updated our Cursor
class to use this technique. We also included a Tick()
function as an example, but it’s not currently doing anything:
// Cursor.h
#pragma once
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
class Cursor {
public:
Cursor(std::string Path, int HotX, int HotY)
: CursorSurface{IMG_Load(Path.c_str())},
HotspotX{HotX},
HotspotY{HotY} {
if (!CursorSurface) {
std::cout << "IMG_Load Error: "
<< SDL_GetError();
}
}
~Cursor() {
if (CursorSurface) {
SDL_FreeSurface(CursorSurface);
}
}
void Tick() {
// Do tick things - eg animation
// ...
}
void HandleMouseMotion(
const SDL_MouseMotionEvent& E
) {
// Update the cursor position such that the next
// render blits the image in the correct place
CursorX = E.x;
CursorY = E.y;
}
void Render(SDL_Surface* targetSurface) const {
if (!CursorSurface || !targetSurface) {
return;
}
// Position the blitting such that the image's
// hotspot matches the last reported cursor position
SDL_Rect Destination{
CursorX - HotspotX,
CursorY - HotspotY,
0, 0
};
SDL_BlitSurface(
CursorSurface, nullptr,
targetSurface, &Destination
);
}
private:
SDL_Surface* CursorSurface;
int HotspotX, HotspotY;
int CursorX, CursorY;
};
Our event loop now needs to forward mouse motion events to our custom Cursor
object, and our application loop needs to Tick()
and Render()
the cursor at the appropriate time.
Here’s an example:
// main.cpp
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
#include "Cursor.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
// Create a window
SDL_Window* Window{SDL_CreateWindow(
"Cursor Example",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 600, 0
)};
SDL_Surface* WindowSurface{
SDL_GetWindowSurface(Window)};
// Create a Cursor instance
Cursor CustomCursor{"cursor.png", 16, 16};
// Application loop
SDL_Event E;
while (true) {
// 1. HANDLE EVENTS
while (SDL_PollEvent(&E)) {
if (E.type == SDL_QUIT) {
SDL_DestroyWindow(Window);
IMG_Quit();
SDL_Quit();
return 0;
}
if (E.type == SDL_MOUSEMOTION) {
CustomCursor.HandleMouseMotion(E.motion);
}
}
// 2. TICK
CustomCursor.Tick();
// 3. RENDER
// Start a new frame by clearing the surface
SDL_FillRect(
WindowSurface,
nullptr,
SDL_MapRGB(WindowSurface->format, 0, 0, 0
));
// Render the game objects and UI elements
// ...
// Render the cursor last to ensure it is on
// top of the other content
CustomCursor.Render(WindowSurface);
// Display the new frame
SDL_UpdateWindowSurface(Window);
}
}
Note that, whilst rendering the cursor through our application loop gives us more flexibility, there is a downside compared to using the native cursor rendering approach provided by functions like SDL_CreateColorCursor()
and SDL_SetCursor()
.
When we render the cursor as part of our regular game loop, it means cursor updates are constrained by how fast our loop can output each frame.
Reduced frame rates are particularly noticable to players when it affects their cursor motion, so we should be careful here. We should only use this approach if it we require the more advanced capabilities it unlocks, or if we’re certain our application will run quickly enough to ensure the user’s mouse actions remain highly responsive.
In this lesson, we covered all the main cursor management techniques available within SDL games. Here are the key points:
SDL_ShowCursor()
SDL_CreateSystemCursor()
SDL_CreateColorCursor()
Cursor
class for better organizationLearn how to control cursor visibility, switch between default system cursors, and create custom cursors
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games