Creating a Simple UI Architecture

How to build a simple, scalable and layered UI architecture using an SDL2 and C++ event loop
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
aSDL9.jpg
Ryan McCombe
Ryan McCombe
Posted

The previous lesson covered how we could use an event loop to handle keyboard and mouse events from our users.

Having set up an application and event loop to handle user input, the next step is to build our next set of systems on top of it.

Managing all events directly in the event loop can get quite messy, so we should come up with a better way of managing it. In this lesson, we’ll establish a simple UI system, to handle all the complexity

The design pattern of event layers can be useful here.

Managing UI Complexity with Event Layers

Fundamentally, our goal is to transform an input event into some change in our application state. The change we need to make is based on the user intent that triggered that event.

In a game, for example, we might be rendering a 3D world, and have a series of UI elements layered on top.

If a user clicks their mouse, they might be trying to interact with the 3D world, like firing their weapon in a shooter game, or selecting a unit in a strategy game.

But, if the user was hovering over a button when they clicked their mouse, they were interacting with that button instead.

The input event hasn’t changed, but the user intent is completely different.

Creating an Event Layer System

Out of this, the concept of an event layer was created. We can think of the UI and the game world being two different layers. The UI layer sits on top of the world layer.

When a mouse event is received, the event loop first hands it to the top layer - the UI, in this case. The UI inspects the event and decides whether or not to handle it.

If the UI handles the event, the journey stops there - the lower layers never see it.

If the UI doesn’t handle the event, it then gets passed to the next layer, which will have the opportunity to handle it, and the process repeats.

Let's see a simple example. Note, Window.h is the same file we created in our previous lesson on creating an SDL2 window.

#include <SDL.h>
#include "Window.h"

class Layer {
public:
  bool HandleEvent(const SDL_Event* Event) {
    // Return true if the event was handled
    // Return false otherwise
    return false;
  }
};

int main() {
  Window GameWindow;
  Layer UI;
  Layer World;

  SDL_Event Event;
  while(true) {
    while(SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
      if (UI.HandleEvent(&Event)) {
        // The UI took care of this event
        // Continue to the next
        continue;
      }
      // The UI did not handle this event
      // Let the next layer see it
      if (World.HandleEvent(&Event)) {
        continue;
      }
    }
  }
}

Letting Objects Register with a Layer

First, we need a way for objects to register themselves with an event layer. To do that, we’ll create a new class: EventReceiver. Similar to the layers, they’ll have a function to receive events, and that function will return a boolean representing whether or not they handled that event:

class EventReceiver {
public:
  virtual bool HandleEvent(const SDL_Event* Event) {
    return false;
  }
};

Then, we’ll update our Layer class with a way to let EventReceivers subscribe to that layer’s events:

class Layer {
public:
  bool HandleEvent(const SDL_Event* Event) {
    // Return true if the event was handled
    // Return false otherwise
    return false;
  }

  void SubscribeToEvents(EventReceiver* Receiver) {
    Subscribers.push_back(Receiver);
  }

private:
  std::vector<EventReceiver*> Subscribers;
};

Finally, the HandleEvent function on our Layer needs to notify all of its subscribers of the event. It will return true if one of its subscribers handled it, and false otherwise:

class Layer {
public:
  bool HandleEvent(const SDL_Event* Event) {
    for (const auto Handler : Subscribers) {
      if (Handler->HandleEvent(Event)) {
        return true;
      }
    }
    return false;
  }
  // ...
}

Lets see everything we’ve got so far:

#include <SDL.h>
#include <vector>
#include <iostream>
#include "Window.h"

class EventReceiver {
public:
  virtual bool HandleEvent(const SDL_Event* Event) {
    std::cout << "It's working!!\n";
    return false;
  }
};

class Layer {
public:
  bool HandleEvent(const SDL_Event* Event) {
    for (const auto Handler : Subscribers) {
      if (Handler->HandleEvent(Event)) {
        return true;
      }
    }
    return false;
  }

  void SubscribeToEvents(EventReceiver* Receiver) {
    Subscribers.push_back(Receiver);
  }

private:
  std::vector<EventReceiver*> Subscribers;
};

int main() {
  Window GameWindow;
  Layer UI;
  Layer World;
  EventReceiver ExampleButton;
  UI.SubscribeToEvents(&ExampleButton);

  SDL_Event Event;
  while(true) {
    while(SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
      if (UI.HandleEvent(&Event)) {
        // The UI took care of this event
        // Continue to the next
        continue;
      }
      // The UI did not handle this event
      // Let the next layer see it
      if (World.HandleEvent(&Event)) {
        continue;
      }
    }
  }
}

Running this application, we should see our window pop up, and our ExampleButton receiving lots of events as we move our mouse around. Most importantly, our main event loop, and main function in general, is simple and clean.

Our example here is quite basic, but the core principles are what is important. Some enhancements we could make include:

  • Separating the classes into their own files
  • Adding a constructor to EventReceiver that accepts a Layer, then handling event subscription within the EventReceiver class.
  • Allowing EventReceivers to subscribe to only certain types of events. For example, SubscribeToEvents and Subscribers could be separated out, allowing distinction between keyboard events, mouse click events, mouse motion events

Typically, the architecture here will just evolve as our requirements develop.

In the next lesson, we’ll expand on this UI architecture to create buttons that can interact with our core application.

Was this lesson useful?

Next Lesson

Creating Buttons in SDL2

Expanding on our UI architecture to add a simple button to our application, complete with hover and click events.
aSDL9a.jpg
Ryan McCombe
Ryan McCombe
Posted
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
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:

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

Creating Buttons in SDL2

Expanding on our UI architecture to add a simple button to our application, complete with hover and click events.
aSDL9a.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved