Structuring SDL Programs

Discover how to organize SDL components using manager classes, inheritance, and polymorphism for cleaner code.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
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 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:

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

#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;
  }
};

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 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}};
};

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:

// 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);
    }
  }
  
  // ...
};
Screenshot showing the program rendering a grid of red rectangles

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:

// 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;
};
Screenshot showing the program rendering a grid of green 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 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})
        );
      }
    }
  }

  // ...
};
Screenshot showing the program rendering a grid of red and green rectangles

Reminder: 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 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;

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 repeat this pattern as many times as needed, thereby creating a hierarchy of managers.

Diagram showing a program hierarchy

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:

Screenshot showing a program rendering a header, grid, and footer

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
  }};
};

Where Does the Logic Go?

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.

Component Reuse

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.

Diagram showing an Image Component reused multiple times across a complex UI hierarchy

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.

#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;
  }
};

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.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Structuring SDL Programs

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

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Implementing User Interaction
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 118 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved