
The Simple DirectMedia Layer (SDL) is a lightweight, cross-platform multimedia library widely used for 2D game development. However, SDL doesn’t natively provide graphical user interface (GUI) controls like buttons, labels, or input fields. While many excellent open-source third-party libraries exist, such as the ones listed below, they can be a bit overwhelming for beginners.
- SDL-GUI:
https://github.com/rhydb/SDL-GUI - SDL_gui:
https://github.com/mozeal/SDL_gui - SDLUI:
https://github.com/immortalx74/SDLUI - kiss_SDL:
https://github.com/actsl/kiss_sdl - ImGui-SDL:
https://github.com/ocornut/imgui - libRocket:
https://github.com/librocket/librocket - CEGUI:
http://cegui.org.uk/ - GoDot:
https://github.com/godotengine/godot/tree/master/editor/gui
These libraries are feature-complete and highly encapsulated, which can make them difficult for new developers to learn. This series will guide you through implementing the most fundamental UI controls, explaining the core principles behind drawing, event handling, and state management. The code and structure in this series have been tested on Windows, but they should be easily adaptable to other platforms.
1. Getting Started
Required Libraries
- SDL2
- SDL2_ttf (for font rendering)
For detailed installation instructions, please refer <A Detailed Guide to Installing SDL on Windows, Linux, and macOS>
Basic Program Structure
First, let’s start with a basic game program structure. Typically, a simple game loop looks something like this:
C++
#include <thread>
#include <iostream>
bool game_running = true;
void ProcessInput() {
std::fprintf(stderr, "process keyevent, mouseevent and so on\n");
}
void UpdateGame() {
std::fprintf(stderr, "check and update gameobject status\n");
}
void RenderFrame(int framecount, float fps) {
std::fprintf(stderr, "render frame %d, fps: %.2f\n\n", framecount, fps);
}
int main(int argc, char** argv)
{
int frame_count = 0;
float fps = 60;
float frame_time = 1000.0f / fps;
while (game_running)
{
ProcessInput();
UpdateGame();
RenderFrame(++frame_count, fps);
std::this_thread::sleep_for(std::chrono::milliseconds((int)frame_time));
}
return 0;
}
This is a simple program, but it shows the core elements of a game loop. You can also see how this structure might be the foundation for a basic Win32 GUI system.
Now, let’s integrate the SDL library into our project. We’ll modify the example code to handle events and rendering with SDL.
C++
#include <SDL.h>
#include <iostream>
const int WINDOW_WIDTH = 800;
const int WINDOW_HEIGHT = 600;
const SDL_Color BG_COLOR = { 0, 0, 255, 255 }; // blue background color (R,G,B,A)
SDL_Window* window = nullptr; // main window
SDL_Renderer* renderer = nullptr;
bool quit = false; // Main loop flag
int Init() {
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
SDL_Log("SDL initialization failed: %s", SDL_GetError());
return 1;
}
// Create window
window = SDL_CreateWindow("Blue Window", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (!window) {
SDL_Log("Window creation failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
// Create renderer
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!renderer) {
SDL_Log("Renderer creation failed: %s", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
return 0;
}
void Cleanup() {
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
void ProcessInput(SDL_Event& event) {
// Handle events
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
quit = true; // Click the window's close button
}
// Press 'Q' to quit
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_q) {
quit = true;
}
}
}
void UpdateGame() {
std::fprintf(stderr, "check and update gameobject status\n");
}
void RenderFrame() {
// Set background color to blue
SDL_SetRenderDrawColor(renderer, BG_COLOR.r, BG_COLOR.g, BG_COLOR.b, BG_COLOR.a);
// Clear screen
SDL_RenderClear(renderer);
// Render everything here
// Update screen
SDL_RenderPresent(renderer);
}
int main(int argc, char* argv[]) {
if (Init() != 0) {
return 1;
}
SDL_Event event;
// Main loop
while (!quit) {
ProcessInput(event);
UpdateGame();
RenderFrame();
// Add a small delay to reduce CPU usage
SDL_Delay(16); // ~60 FPS
}
// Cleanup resources
Cleanup();
return 0;
}
After compiling and running this code, you’ll see a blank window with a blue background. You can press the ‘Q’ key while the window is active or click the close button to exit the program.
2. Introducing the Scene Manager
To better align with professional game development, we’ll refine our structure. We’ll introduce the concept of a Scene. If you’ve ever used engines like Unity, Unreal, or GoDot, you know that they are all built around this concept. A scene is a core concept used to organize and manage content in a game. It acts as a container for your game world, where all event handling, object updates, and rendering are performed within a specific scene. This defines the game’s state and content at any given moment.
Let’s refactor our code to use this scene-based approach.
C++
#include <type_traits>
#include <utility>
#include <vector>
#include <iostream>
class SceneBase
{
public:
virtual bool init() = 0;
virtual int ProcessEvent(SDL_Event* e) { return 0; }
virtual int Update(uint32_t dt) { return 0; }
virtual int Render() { return 0; }
virtual void OnUpdate(uint32_t dt) {}
template<typename T, typename... Args, std::enable_if_t<std::is_base_of_v<SceneBase, T>>* = nullptr>
inline static T* Create(Args&&... args)
{
T* t = new T(std::forward<Args>(args)...);
if (!t->init())
{
delete t;
return nullptr;
}
return t;
}
protected:
//....
};
class LaunchScene : public SceneBase
{
public:
bool init() override { return true; }
int ProcessEvent(SDL_Event* e) override { return 0; }
int Update(uint32_t dt) override { return 0; }
int Render() override { return 0; }
};
class SceneManager
{
public:
static SceneManager* Instance()
{
static SceneManager _mgr;
return &_mgr;
}
void Run(SceneBase* s) {}
void SwitchScene(SceneBase* s) {}
void PushScene(SceneBase* s) {}
void PopScene() {}
SceneBase* GetCurrentScene() const { return m_currentScene; }
private:
SceneBase* m_currentScene{ nullptr };
bool m_bRun{ true };
};
#define sSceneMgr SceneManager::Instance()
class SDLRenderer
{
public:
bool Init() { return true; }
static SDLRenderer* GetInstance() { static SDLRenderer instance; return &instance; }
};
#define sRenderer SDLRenderer::GetInstance()
int main(int argc, char** argv)
{
if (!sRenderer->Init())
return 1;
auto scene = SceneBase::Create<LaunchScene>();
if (!scene)
return 1;
sSceneMgr->Run(scene);
return 0;
}
You can find the complete project example here: https://github.com/84378996/simple_sdl_gui_tutorials
This article serves as the starting point of the series, outlining the basic structure for SDL game programming. This structure will serve as the foundation for our UI controls. In the next chapter, we will officially begin developing the controls themselves.
