Building a Modular UI System

Learn how to create a flexible and extensible UI system using C++ and SDL, focusing on component hierarchy and polymorphism.
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

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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:

  1. Creating a UI manager to handle events and rendering
  2. Implementing nested components for better organization
  3. Utilizing inheritance to create reusable UI elements
  4. Using polymorphism for flexible component management

By the end of this lesson, you'll have a solid foundation for building scalable and maintainable UI systems.

Creating a UI Manager

We’ll start by creating a UI manager, which has two methods that will be called from our application loop.

  • Events will be passed to the HandleEvent() method, giving our UI visibility of the events flowing through our application, and the opportunity to react to them
  • The application loop will call Render() 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; }

Nested Components

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

Inheritance

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

Polymorphic Children

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

Summary

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!

Was this lesson useful?

Next Lesson

Building Interactive Buttons

Explore techniques for building UI components that respond to user input
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Building a Modular UI System

Learn how to create a flexible and extensible UI system using C++ and SDL, focusing on component hierarchy and polymorphism.

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

Free, Unlimited Access
Implementing User Interaction
  • 41.GPUs and Rasterization
  • 42.SDL Renderers
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

Free, unlimited access

This course includes:

  • 40 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Building Interactive Buttons

Explore techniques for building UI components that respond to user input
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved