Structuring SDL Programs

Learn how to organize SDL components using manager classes, inheritance, and polymorphism for cleaner code.

Ryan McCombe
Updated

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 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 program, keeping our main code clean and organized.

We will cover:

  • Creating a UI manager class.
  • Using std::vector to manage multiple components dynamically.
  • Applying inheritance to create specialized components.
  • Leveraging polymorphism to handle different component types uniformly.
  • Building hierarchies of managers for complex UIs.

Starting Point

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.

Files

src
Select a file to view its content

Manager Classes

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 unmanageable.

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:

src/UI.h

#pragma once
#include <SDL3/SDL.h>

class UI {
public:
  void Render(SDL_Surface* Surface) const {
    // ...
  }

  void HandleEvent(SDL_Event& E) {
    // ...
  }
};

Now, in our main function, managing the entire UI only requires three lines of code. We construct a UI manager, we notify it of events, and we render it:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"
#include "Rectangle.h"
#include "UI.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  Rectangle Rect{SDL_Rect{50, 50, 50, 50}};
  UI UIManager;

  bool IsRunning = true;
  SDL_Event Event;
  while (IsRunning) {
    while (SDL_PollEvent(&Event)) {
      Rect.HandleEvent(Event);
      UIManager.HandleEvent(Event);
      if (Event.type == SDL_EVENT_QUIT) {
        IsRunning = false;
      }
    }

    GameWindow.Render();
    Rect.Render(GameWindow.GetSurface());
    UIManager.Render(GameWindow.GetSurface());
    GameWindow.Update();
  }

  SDL_Quit();
  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:

src/UI.h

#pragma once
#include <SDL3/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}};
};

Dynamic Children

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:

src/UI.h

#pragma once
#include <SDL3/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 its array, forwarding the request to each of them:

src/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);
    }
  }

  // ...
};

Inheritance

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:

src/GreenRectangle.h

#pragma once
#include <SDL3/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:

src/UI.h

#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include "Rectangle.h"
#include "GreenRectangle.h"

class UI {
// ...

private:
  std::vector<GreenRectangle> Rectangles;
};

Polymorphism

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 to implement a model here, as the UI should conceptually "own" the objects that it is managing:

src/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 .:

src/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 :

src/UI.h

#pragma once
#include <SDL3/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})
        );
      }
    }
  }

  // ...
};

The virtual Keyword

In 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 is specifically pointing to a GreenRectangle. That worked in this case but, in more advanced inheritance examples, we might require a derived class like GreenRectangle to override the behavior of the Render() function entirely.

If we want derived classes to have the option to override the behavior of a function they're inheriting, we should add the virtual keyword to that function. We introduced virtual in our lesson on .

Applying it to Rectangle::Render(), for example, would look like this:

src/Rectangle.h

// ...

class Rectangle {
 public:
 // ...

  virtual void Render(
    SDL_Surface* Surface
  ) const {
} // ... };

Any derived class that overrides this function should add the override keyword:

src/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 class's destructor as virtual:

src/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;

Hierarchy

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 do this 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:

src/UI.h

#pragma once
#include <SDL3/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:

Files

src
Select a file to view its content

Component Reuse

Beyond managing complexity, building UIs from components also 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.

Complete Code

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.

Files

src
Select a file to view its content

Summary

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:

  • Manager classes improve organization by encapsulating UI sections.
  • Use std::vector to manage variable numbers of components.
  • Inheritance allows extending base component functionality.
  • Polymorphism provides flexibility in handling different component types via base pointers.
  • Build UI hierarchies to manage complexity by delegating responsibility.
  • Component reuse and locality are important design goals.
Next Lesson
Lesson 24 of 25

Creating SDL3 Buttons

Learn to create interactive buttons in SDL3 and manage communication between different UI components.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate