In earlier lessons, we saw how to draw shapes and handle mouse events for individual elements in SDL. While this works for simple examples, managing dozens or hundreds of UI elements directly in our main function quickly becomes unmanageable.
This lesson introduces techniques for structuring larger SDL applications. We'll learn how to create "manager" classes that take responsibility for specific parts of the UI, keeping our main code clean and organized.
We will cover:
std::vector
to manage multiple components dynamically.We'll build upon the code from our previous lesson. This starting point includes an SDL application that initializes the library, creates a Window
, and manages a single Rectangle
object. The Rectangle
handles its own rendering and responds to mouse hover and click events.
#include <SDL.h>
#include "Window.h"
#include "Rectangle.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Rectangle Rect{SDL_Rect{50, 50, 50, 50}};
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
Rect.HandleEvent(E);
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Render();
Rect.Render(GameWindow.GetSurface());
GameWindow.Update();
}
return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>
class Window {
public:
Window() {
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
700, 300, 0
);
}
void Render() {
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() const {
return SDL_GetWindowSurface(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(const SDL_Rect& Rect)
: Rect{Rect} {}
void Render(SDL_Surface* Surface) const {
auto [r, g, b, a]{
isPointerHovering ? HoverColor : Color
};
SDL_FillRect(
Surface, &Rect,
SDL_MapRGB(Surface->format, r, g, b)
);
}
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
if (isPointerHovering &&
E.button.button == SDL_BUTTON_LEFT
) {
std::cout << "A left-click happened "
"on me!\n";
}
}
}
void SetColor(const SDL_Color& NewColor) {
Color = NewColor;
}
SDL_Color GetColor() const {
return Color;
}
void SetHoverColor(const SDL_Color& NewColor) {
HoverColor = NewColor;
}
SDL_Color GetHoverColor() const {
return HoverColor;
}
private:
SDL_Rect Rect;
SDL_Color Color{255, 0, 0};
SDL_Color HoverColor{0, 0, 255};
bool isPointerHovering{false};
bool isWithinRect(int x, int y) {
if (x < Rect.x) return false;
if (x > Rect.x + Rect.w) return false;
if (y < Rect.y) return false;
if (y > Rect.y + Rect.h) return false;
return true;
}
};
In more complex projects, we’re likely to find our game managing thousands or maybe millions of objects. We can’t just orchestrate all of them from our main
function - that would quickly get unmanagable.
To deal with this, we can introduce intermediate, manager classes that take ownership over some area of our program. For example, we might have a UI
class, responsible for managing all the buttons and menus in our program.
That class would need to react to events and render the objects it manages, so let’s add HandleEvent()
and Render()
functions:
// UI.h
#pragma once
#include <SDL.h>
class UI {
public:
void Render(SDL_Surface* Surface) const {
// ...
}
void HandleEvent(SDL_Event& E) {
// ...
}
};
Now, in our main
loop, managing the entire UI only requires three lines of code. We construct a UI manager, we notify it of events, and we render it:
#include <SDL.h>
#include "Window.h"
#include "Rectangle.h"
#include "UI.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Rectangle Rect{SDL_Rect{50, 50, 50, 50}};
UI UIManager;
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
Rect.HandleEvent(E);
UIManager.HandleEvent(E);
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Render();
Rect.Render(GameWindow.GetSurface());
UIManager.Render(GameWindow.GetSurface());
GameWindow.Update();
}
return 0;
}
Let’s add some objects for our UI
to manage. We’ll create three rectangles and, every time our UI
receives an event or a request to render, it forwards that request to all of its children:
// UI.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class UI {
public:
void Render(SDL_Surface* Surface) const {
A.Render(Surface);
B.Render(Surface);
C.Render(Surface);
}
void HandleEvent(SDL_Event& E) {
A.HandleEvent(E);
B.HandleEvent(E);
C.HandleEvent(E);
}
private:
Rectangle A{SDL_Rect{50, 50, 50, 50}};
Rectangle B{SDL_Rect{150, 50, 50, 50}};
Rectangle C{SDL_Rect{250, 50, 50, 50}};
};
A manager class can have a dynamic number of children by storing them in a container such as a std::vector
. Below, we update our UI
class to manage 60 rectangles.
Our constructor uses some loops to create all of our rectangles, and some arithmetic to position them into a grid layout:
// UI.h
#pragma once
#include <SDL.h>
#include <vector>
#include "Rectangle.h"
class UI {
public:
UI() {
int RowCount{5}, ColCount{12};{
Rectangles.reserve(RowCount * ColCount);{
for (int Row{0}; Row < RowCount; ++Row) {
for (int Col{0}; Col < ColCount; ++Col) {
Rectangles.emplace_back(SDL_Rect{
60 * Col, 60 * Row, 50, 50
});
}
}
}
void Render(SDL_Surface* Surface) const {
// ...
}
void HandleEvent(SDL_Event& E) {
// ...
}
private:
std::vector<Rectangle> Rectangles;
};
When a UI
receives a Render()
or HandleEvent()
request, it now iterates over all of the elements in it’s array, forwarding the request to each of them:
// UI.h
// ...
class UI {
public:
// ...
void Render(SDL_Surface* Surface) const {
for (auto& Rectangle : Rectangles) {
Rectangle.Render(Surface);
}
}
void HandleEvent(SDL_Event& E) {
for (auto& Rectangle : Rectangles) {
Rectangle.HandleEvent(E);
}
}
// ...
};
Remember, once we have a class
set up, we can use inheritance to extend that class, or provide variations. Let’s create a GreenRectangle
class that inherits from our Rectangle
.
We’ll just forward the SDL_Rect
to the inherited constructor and then, from the constructor body, set our color to green:
// GreenRectangle.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class GreenRectangle : public Rectangle {
public:
GreenRectangle(const SDL_Rect& Rect)
: Rectangle{Rect} {
SetColor({0, 255, 0, 255});
}
};
In our UI
class, we can switch to using green rectangles by updating the type of our array:
// UI.h
#pragma once
#include <SDL.h>
#include <vector>
#include "Rectangle.h"
#include "GreenRectangle.h"
class UI {
// ...
private:
std::vector<GreenRectangle> Rectangles;
};
When we’re using inheritance, our UI
class can store objects of varying types within the same collection. They just need to share the same base class, such as Rectangle
.
To use polymorphism, our UI
needs to store its children as pointers. We’ll use std::unique_ptr
for this, as it automates the memory management, and the UI should conceptually "own" the objects that it is managing:
// UI.h
// ...
#include <memory> // For std::unique_ptr
class UI {
public:
// ...
private:
std::vector<std::unique_ptr<
Rectangle>> Rectangles;
};
As our Rectangles
array is now storing pointers rather than Rectangle
values, we need to update our Render()
and HandleEvent()
functions to use the ->
operator instead of .
:
// UI.h
// ...
class UI {
public:
// ...
void Render(SDL_Surface* Surface) const {
for (auto& Rectangle : Rectangles) {
Rectangle.Render(Surface);
Rectangle->Render(Surface);
}
}
void HandleEvent(SDL_Event& E) {
for (auto& Rectangle : Rectangles) {
Rectangle.HandleEvent(E);
Rectangle->HandleEvent(E);
}
}
// ...
};
Let’s update our constructor to create an alternating grid of red and green rectangles using the modulus operator:
// UI.h
#pragma once
#include <SDL.h>
#include <vector>
#include <memory>
#include "Rectangle.h"
#include "GreenRectangle.h"
class UI {
public:
UI() {
int RowCount{5}, ColCount{12};
Rectangles.reserve(RowCount * ColCount);
for (int Row{0}; Row < RowCount; ++Row) {
for (int Col{0}; Col < ColCount; ++Col) {
bool useGreen{(Row + Col) % 2 == 0};
Rectangles.emplace_back(useGreen
? std::make_unique<GreenRectangle>(SDL_Rect{
60 * Col, 60 * Row, 50, 50})
: std::make_unique<Rectangle>(SDL_Rect{
60 * Col, 60 * Row, 50, 50})
);
}
}
}
// ...
};
virtual
KeywordIn our previous example, our UI
constructor is calling the GreenRectangle
constructor on every other iteration of the loop. It is this constructor that sets the rectangle’s color to green.
However, invocations of other functions, such as Render()
, are using the base Rectangle
version of that function, even if the Rectangle
smart pointer pointer is specifically pointing to a GreenRectangle
.
If we want derived classes to have the option to override the behaviour of a function they’re inheriting, we should add the virtual
keyword to that function. We introduced virtual
here:
Applying it to Rectangle::Render()
, for example, would look like this:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
virtual void Render(
SDL_Surface* Surface
) const {
}
// ...
};
Any derived class that overrides this function should add the override
keyword:
// GreenRectangle.h
// ...
class GreenRectangle : public Rectangle {
public:
// ...
void Render(
SDL_Surface* Surface
) const override {
// ...
}
};
When we want to use polymorphism, it is also highly recommended to mark the base classes destructor as virtual
:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
virtual ~Rectangle() = default;
// ...
};
This is because, if a derived type implements a custom destructor, we want to ensure that destructor is used even if the object is deleted through a reference to the base type.
Non-virtual destructors are a common source of memory leaks as, if the GreenRectangle
allocates some memory in its constructor and deallocates it in the destructor, we want to make sure that destructor is used:
// This will call the GreenRectangle constructor...
Rectangle* Rect{
new GreenRectangle{SDL_Rect{1, 2, 3, 4}}
}
// ...so we want this to call the GreenRectangle destructor,
// even though Rect is a base Rectangle pointer
delete Rect;
Our UI class successfully abstracts complexity away from main. However, in a large application with multiple distinct UI areas (like a header, main content area, and footer), the UI class itself could become overly complex, managing dozens or hundreds of disparate components.
We can address this simply by repeating the pattern again, introducing another layer of manager-style classes. We can repeat this pattern as many times as needed, thereby creating a hierarchy of managers.
For example, instead of the main UI
class managing every single rectangle, it can manage a few higher-level manager objects, each responsible for a specific section of the UI
. For example, UI could own instances of Header
, Footer
, and Grid
classes.
The Header
class would manage components specific to the header, the Footer
class would manage its components, and the Grid
class would manage the grid of rectangles.
The top-level UI class then simply delegates tasks: UI::Render()
calls Render()
on Header
, Grid
, and Footer
, and UI::HandleEvent()
forwards events similarly:
// UI.h
#pragma once
#include <SDL.h>
#include "Header.h"
#include "Footer.h"
#include "Grid.h"
class UI {
public:
void Render(SDL_Surface* Surface) const {
TopMenu.Render(Surface);
Rectangles.Render(Surface);
BottomMenu.Render(Surface);
}
void HandleEvent(SDL_Event& E) {
TopMenu.HandleEvent(E);
Rectangles.HandleEvent(E);
BottomMenu.HandleEvent(E);
}
private:
Header TopMenu;
Grid Rectangles;
Footer BottomMenu;
};
In this example program, the Header
and Footer
class both render a simple gray rectangle, whilst the Grid
renders a grid of red rectangles:
For reference, the Header
, Grid
, and Footer
classes used for this example are provided below:
// Header.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class Header {
public:
Header() {
Background.SetColor({
100, 100, 100, 255
});
}
void Render(SDL_Surface* Surface) const {
Background.Render(Surface);
}
void HandleEvent(SDL_Event& E) {
Background.HandleEvent(E);
}
private:
Rectangle Background{SDL_Rect{
0, 0, 700, 50
}};
};
#pragma once
#include <SDL.h>
#include <vector>
#include <memory>
#include "Rectangle.h"
class Grid {
public:
Grid() {
int VerticalPosition{65};
int RowCount{3}, ColCount{12};
Rectangles.reserve(RowCount * ColCount);
for (int Row{0}; Row < RowCount; ++Row) {
for (int Col{0}; Col < ColCount; ++Col) {
Rectangles.emplace_back(
std::make_unique<Rectangle>(
SDL_Rect{
60 * Col,
60 * Row + VerticalPosition,
50, 50
}
)
);
}
}
}
void Render(SDL_Surface* Surface) const {
for (auto& Rectangle : Rectangles) {
Rectangle->Render(Surface);
}
}
void HandleEvent(SDL_Event& E) {
for (auto& Rectangle : Rectangles) {
Rectangle->HandleEvent(E);
}
}
private:
std::vector<std::unique_ptr<
Rectangle>> Rectangles;
};
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class Footer {
public:
Footer() {
Background.SetColor({
100, 100, 100, 255
});
}
void Render(SDL_Surface* Surface) const {
Background.Render(Surface);
}
void HandleEvent(SDL_Event& E) {
Background.HandleEvent(E);
}
private:
Rectangle Background{SDL_Rect{
0, 250, 700, 50
}};
};
If we think about our application design as layers in a hierarchy: main
is the highest level, followed by UI
, then section managers like Grid
, and finally individual components like Rectangle
.
Conceptually, we can think of code that is placed in classes or functions near the top of this hierarchy as being more impactful than code near the bottom. This is because code placed higher up should generally have broader scope and impact.
Good design practice suggests pushing functionality as far down this hierarchy as is reasonable. If a behaviour relates only to how a rectangle draws itself or responds to input, that logic should live within the Rectangle
class. It shouldn't clutter the more important Grid
or UI
classes, and least of all the main()
function.
This concept, often related to encapsulation and cohesion, promotes locality. Keeping behaviour close to the relevant data makes classes more independent, easier to test, and simpler to reason about. High-level classes stay focused on coordination, leading to a cleaner overall architecture.
Beyond managing complexity, building UIs from components enables code reuse.
For example, let’s imagine we wanted to add the ability to load and render an image in our Header
class. Even if we can add the required logic without making the Header
code excessively complex, it’s probably a good idea to create a separate Image
component anyway.
This is because the ability to render an image is likely to be useful in many parts of our application, not just the Header
class. By creating a separate Image
component for this purpose, our work can be reused across other parts of our application that require an image.
We’ll learn how to render images in the next chapter.
We’ll reuse the concepts we covered in this lesson later in the course but, for now, we’ll go back to a minimalist UI
class that just renders two rectangles.
In the next lesson, we’ll continue developing on this framework as we learn techniques that allow our components to communicate with their parents, and each other.
#include <SDL.h>
#include "Window.h"
#include "UI.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
UI UIManager;
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
UIManager.HandleEvent(E);
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Render();
UIManager.Render(GameWindow.GetSurface());
GameWindow.Update();
}
return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>
class Window {
public:
Window() {
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
700, 300, 0
);
}
void Render() {
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() const {
return SDL_GetWindowSurface(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class UI {
public:
void Render(SDL_Surface* Surface) const {
A.Render(Surface);
B.Render(Surface);
}
void HandleEvent(SDL_Event& E) {
A.HandleEvent(E);
B.HandleEvent(E);
}
private:
Rectangle A{SDL_Rect{50, 50, 50, 50}};
Rectangle B{SDL_Rect{150, 50, 50, 50}};
};
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(const SDL_Rect& Rect)
: Rect{Rect} {}
void Render(SDL_Surface* Surface) const {
auto [r, g, b, a]{
isPointerHovering ? HoverColor : Color
};
SDL_FillRect(
Surface, &Rect,
SDL_MapRGB(Surface->format, r, g, b)
);
}
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
if (isPointerHovering &&
E.button.button == SDL_BUTTON_LEFT
) {
std::cout << "A left-click happened "
"on me!\n";
}
}
}
void SetColor(const SDL_Color& NewColor) {
Color = NewColor;
}
SDL_Color GetColor() const {
return Color;
}
void SetHoverColor(const SDL_Color& NewColor) {
HoverColor = NewColor;
}
SDL_Color GetHoverColor() const {
return HoverColor;
}
private:
SDL_Rect Rect;
SDL_Color Color{255, 0, 0};
SDL_Color HoverColor{0, 0, 255};
bool isPointerHovering{false};
bool isWithinRect(int x, int y) {
if (x < Rect.x) return false;
if (x > Rect.x + Rect.w) return false;
if (y < Rect.y) return false;
if (y > Rect.y + Rect.h) return false;
return true;
}
};
This lesson focused on structuring SDL programs to help manage complexity. We introduced the UI
manager class to simplify main
, used std::vector
for dynamic component lists, created specialized components like GreenRectangle
via inheritance, and managed mixed types using polymorphism and std::unique_ptr
.
Key Takeaways:
std::vector
to manage variable numbers of components.Discover how to organize SDL components using manager classes, inheritance, and polymorphism for cleaner code.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games