In the previous section, we introduced the application loop and event loop, which sets the foundation of our program. As our program and interactions get more complex, we need to build additional systems on top of this foundation to help manage the complexity.
In this lesson, we'll dive deeper into managing complex user interfaces by implementing a modular UI system. We'll cover key topics such as:
By the end of this lesson, you'll have a solid foundation for building scalable and maintainable UI systems.
We’ll start by creating a UI manager, which has two methods that will be called from our application loop.
HandleEvent()
method, giving our UI visibility of the events flowing through our application, and the opportunity to react to themRender()
at the appropriate time, passing the SDL_Surface
the UI should render to// UI.h
#pragma once
class UI {
public:
void HandleEvent(const SDL_Event& E){
// ...
};
void Render(SDL_Surface* Surface){
// ...
}
};
Adding this clas to our program and connecting it to our application loop might look something like this:
#include <SDL.h>
#include "UI.h"
class Window {/*...*/};
int main(int argc, char** argv){
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
UI GameUI;
SDL_Event Event;
bool shouldQuit{false};
while (!shouldQuit) {
while (SDL_PollEvent(&Event)) {
GameUI.HandleEvent(Event);
if (Event.type == SDL_QUIT) {
shouldQuit = true;
}
}
GameWindow.Render();
GameUI.Render(GameWindow.GetSurface());
GameWindow.Update();
}
SDL_Quit();
return 0;
}
Our UI manager is now removing UI-related responsibility from the main loop, helping to reduce its complexity. However, in large applications, our UI can get very complex, so we need ways to defer responsibility away from the UI manager.
A common way to design this is to imagine our UI existing in a hierarchy. For example, our main UI class might have three children, each responsible for a specific part of the UI such as a main menu, a sidebar, and a footer.
We’d create classes for each of these and, just like the UI manager itself, we can give them Render()
and HandleEvent()
methods:
// MainMenu.h
#pragma once
class MainMenu {
public:
void HandleEvent(const SDL_Event& E){];
void Render(SDL_Surface* Surface){};
};
// Sidebar.h
#pragma once
class Sidebar {
public:
void HandleEvent(const SDL_Event& E){};
void Render(SDL_Surface* Surface){};
};
// Footer.h
#pragma once
class Footer {
public:
void HandleEvent(const SDL_Event& E){};
void Render(SDL_Surface* Surface){};
};
Our UI manager can now create these three children, and forward events and render requests to them:
// UI.h
#pragma once
#include "MainMenu.h"
#include "Sidebar.h"
#include "Footer.h"
class UI {
public:
void HandleEvent(const SDL_Event& E){
MenuComponent.HandleEvent(E);
SidebarComponent.HandleEvent(E);
FooterComponent.HandleEvent(E);
};
void Render(SDL_Surface* Surface){
MenuComponent.Render(Surface);
SidebarComponent.Render(Surface);
FooterComponent.Render(Surface);
}
private:
MainMenu MenuComponent;
Sidebar SidebarComponent;
Footer FooterComponent;
};
This process can continue for deeper levels of nesting. For example, our MainMenu
component might be comprised of a series of Button
components. Each Button
would take some responsibility away from theTMainMenu
class, preventing it from becoming excessively large.
Within this framework, components can also have a dynamic number of children. For example, one of the children of our MainMenu
could be a std::vector
, containing a variable number of UI elements:
// MainMenu.h
#pragma once
#include <vector>
#include "Button.h"
class MainMenu{
public:
MainMenu(){
// Construct Children
// ...
};
void HandleEvent(const SDL_Event& E){
for (Button& Child : Children) {
Child.HandleEvent(E);
}
};
void Render(SDL_Surface* Surface){
for (Button& Child : Children) {
Child.Render(Surface);
}
}
private:
std::vector<Button> Children;
};
Remember, we can also use inheritance when building out our collection of UI components. For example, a typical component we’ll need is one that draws a simple rectangle onto the surface:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(int x, int y, int w, int h)
: Rect{x, y, w, h}{}
void Render(SDL_Surface* Surface){
SDL_FillRect(
Surface, &Rect, SDL_MapRGB(
Surface->format, 255, 0, 0
)
);
}
private:
SDL_Rect Rect{0, 0, 0, 0};
};
Aside from rendering this component directly, we can also use it as the base of more complex components. For example, we might have a component that expands on this to create rectangular buttons, or a class that expands on it to create text boxes.
We’ll implement both of these later in the course, but the basic idea is outlined below.
Our Text
class inherits from Rectangle
, and replaces the Render()
method. The Text::Render()
function will call inherited the Rectangle::Render()
method to draw the rectangle, and will then implement additional logic to draw the text on top of it:
// Text.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class Text : public Rectangle {
public:
Text(int x, int y, int w, int h)
: Rect{x, y, w, h}{}
void Render(SDL_Surface* Surface){
Rectangle::Render(Surface);
// Render Text...
}
};
Finally, we can use polymorphic patterns within this design. For example, imagine we have a Grid
component that is responsible for positioning its children in a specific way. For this class to be flexible, it shouldn’t need to know in advance what type its children should be, not should it require all the children have the same type.
This is where polymorphism can help us. Our grid could simply require its children have appropriate Render()
and HandleEvent()
methods. So, we create some type that meets these requirements, remembering to add the virtual
keywords to make it polymorphic:
// Component.h
#pragma once
#include <SDL.h>
class Component {
public:
virtual void HandleEvent(const SDL_Event& E){}
virtual void Render(SDL_Surface* Surface){}
};
Our Grid
class will store pointers to objects that have this type. Ideally, we’d want to give our Grid
the capability construct children of an unknown type and assume unique ownership of them, but this approach requires some advanced topics we haven’t covered yet.
So for now, we’ll just construct the objects elsewhere and pass them to our Grid
as a std::shared_ptr
through the AddChild()
method. This lets our grid at least share ownership of its children, and keep them alive in memory:
// Grid.h
#pragma once
#include <SDL.h>
#include <memory>
#include "Component.h"
class Grid {
public:
void AddChild(
std::shared_ptr<Component> Child){
Children.push_back(Child);
}
void Render(SDL_Surface* Surface){
for (auto& Child : Children) {
Child->Render(Surface);
}
}
void HandleEvent(const SDL_Event& E){
for (auto& Child : Children) {
Child->HandleEvent(E);
}
}
std::vector<std::shared_ptr<Component>> Children;
};
We covered memory ownership and std::shared_ptr
in more detail here:
Next, let’s create some types that inherit from Component
and override the Render()
method:
// Shapes.h
#pragma once
#include <SDL.h>
#include <iostream>
#include "Component.h"
class Rectangle : public Component {
void Render(SDL_Surface* Surface) override{
std::cout << "Rendering Rectangle\n";
}
};
class Circle : public Component {
void Render(SDL_Surface* Surface) override{
std::cout << "Rendering Circle\n";
}
};
Finally, let’s create some shapes and push them to our grid. For the sake of this example, we’ll do it from our UI manager:
// Shapes.h
#pragma once
#include <SDL.h>
#include "Shapes.h"
#include "Grid.h"
class UI {
public:
UI(){
GridComponent.AddChild(
std::make_shared<Rectangle>());
GridComponent.AddChild(
std::make_shared<Circle>());
}
void Render(SDL_Surface* Surface){
GridComponent.Render(Surface);
}
void HandleEvent(const SDL_Event& E){
GridComponent.HandleEvent(E);
}
private:
Grid GridComponent;
};
Now, our Grid
renders a rectangle and a circle on every frame, without needing to know the Rectangle
or Circle
types even exist:
Rendering Rectangle
Rendering Circle
Rendering Rectangle
Rendering Circle
// ...
In this lesson, we've explored the fundamentals of building a modular UI system in C++ using SDL. We started by creating a UI manager to handle events and rendering, then introduced nested components to manage complexity.
We also discussed how to use inheritance for creating reusable UI elements and leveraged polymorphism for flexible component management.
In the next lesson, we'll build upon this foundation by adding interactive buttons to our UI. We'll implement hover and click functionality, allowing users to interact with our interface. This will involve handling mouse events, updating button states, and providing visual feedback to the user. Get ready to bring your UI to life with dynamic, responsive elements!
Learn how to create a flexible and extensible UI system using C++ and SDL, focusing on component hierarchy and polymorphism.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games