Observer & Event Queue

Software design patterns

Problem statement

For example this achievement:

center

How would you implement this?

void GameActor::Die()
{
  PlayDieSound();
  PlayDieAnim();

  if(NobodyDiedYet())  
    UnlockFirstBloodAchievement();

  FadeOut();
}
Programming 4 - Observer & Event Queue
Software design patterns

Problem statement

Or, even worse:

center

Going Down? is an achievement which is completed by falling 65 yards without dying. You have to end the fall on a "hard surface".

Goal is again: no tight coupling.

Programming 4 - Observer & Event Queue
Software design patterns

Remember: no coupling

center

Programming 4 - Observer & Event Queue
Observer

Observer

Notify interested parties that a certain event took place. They have to decide what to do with that.

In our example we notify interested parties that a certain actor in the game died:

void GameActor::Die()
{
  PlayDieSound();
  PlayDieAnim();

  NotifyObservers(GameEvent::ActorDied);

  FadeOut();
}

These interested parties are called "observers" or "event handlers". In our example they implement an interface like this:

class Observer
{
public:
  virtual ~Observer() = default;
  virtual void Notify(Event event, GameActor* actor) = 0;
}
Programming 4 - Observer & Event Queue
Observer

Observer

An achievement system will typically be an observer. It should not interfere with the game code at all.

class Achievement : public Observer
{
public:
  void Notify(Event event, GameActor* actor) override
  {
    switch(event) {
      case GameEvent::ActorDied:
        if(CheckIfNobodyElseDiedYet(actor)) {
          Unlock(Achievement::FirstBlood);
        }
        break;
      case GameEvent::ActorFell:
        // etc...
    }
  }

private:
  void Unlock(Achievement achievement) {
    // code to unlock an achievement...
  }
}
Programming 4 - Observer & Event Queue
Observer

Subject

The observed class gets some extra state – it needs to track who’s observing (but not what!). This observed class is called the subject.

class GameActor
{
public:
  void AddObserver(Observer* observer) {
    // code to add an observer
  }
  void RemoveObserver(Observer* observer) {
    // code to remove an observer
  }

protected:
  void NotifyObservers(Event event) {
    for(auto observer : m_observers)
      observer->Notify(event, this);
  }

private:
  std::vector<Observer*> m_observers;
}
Programming 4 - Observer & Event Queue
Observer

Considerations

  • The observers in the subject state: can we afford the extra memory?

    • Should they be kept in an extra buffer as in the example?
  • When we delete the subject: what do we do with the observers?

    • Nothing?
    • Delete them?
    • Notify them?
    • In GC languages this is certainly an issue!
  • When we Notify the observers, what data do we send them?

    • Nothing – they can just get the data (= pull model)
    • Everything = push model
    • Only the altered state
Programming 4 - Observer & Event Queue
Observer

Implementations

To alleviate the memory cost, you could keep a pointer to a Subject class

class GameActor
{
public:
  void Die() {
    PlayDieSound();
    PlayDieAnim();
    m_actorDiedEvent->NotifyObservers();
    FadeOut(); 
  }

private:
  std::unique_ptr<Subject> m_actorDiedEvent;
}
  • This is exactly what events are in C#
  • This is exactly what UnityEvents are in Unity
  • This is exactly what multicast delegates are in Unreal
  • Java has a similar system and even an Observer and Observable class.
Programming 4 - Observer & Event Queue
Observer

Java

In java a subject class can inherit from “Observable”

center

https://docs.oracle.com/javase/7/docs/api/java/util/Observable.html

Programming 4 - Observer & Event Queue
Observer

Java

And the Observer implements the interface "Observer"

center

https://docs.oracle.com/javase/7/docs/api/java/util/Observer.html

This may suffer from the lapsed listener problem.

In C++, where we have no GC we can make use of shared and weak pointers to avoid this issue, or use the destructor to unsubscribe.

Programming 4 - Observer & Event Queue
Observer

Unreal

In Unreal you'd add an event (which is a multicast delegate) in your header file:

public:
  DECLARE_MULTICAST_DELEGATE(FMyEvent)
  FMyEvent& OnSwitched() { return SwitchedEvent; }
private:
  FMyEvent SwitchedEvent;

Which you then can broadcast

void AMyActor::MyFunction()
{
    SwitchedEvent.Broadcast();
}

And subscribe to

Actor->OnSwitched().AddUFunction(this, "ToggleDoor");

https://docs.unrealengine.com/5.3/en-US/multicast-delegates-in-unreal-engine/

Programming 4 - Observer & Event Queue
Observer

Implementations

It’s often not interesting or possible to know the source of an event.

  • If there are many actors and you need to know if one died but you don’t care which one
  • You don’t want to couple the observer to the subject
  • Or the same event can be issued by multiple subjects
    • For example, “DialogEnded” can be issued by all dialogs – do you want to track them all?

In that case: use an event queue / message queue instead.

Programming 4 - Observer & Event Queue
Event queue

Event queue

We've already seen one:

bool dae::InputManager::ProcessInput()
{
  SDL_Event e;
  while (SDL_PollEvent(&e)) {
    if (e.type == SDL_QUIT) {
      return false;
    }
    if (e.type == SDL_KEYDOWN) {
      
    }
    if (e.type == SDL_MOUSEBUTTONDOWN) {
      
    }
    //process event for IMGUI
    ImGui_ImplSDL2_ProcessEvent(&e);
  }
}
Programming 4 - Observer & Event Queue
Event queue

Event queue

Observer decouples the sender from the receiver

  • Sender does not know who’s listening
  • But the observer knows who it is listening to

Event Queue also decouples the receiver from the sender

  • Sender does not know who’s listening
  • Receiver does not need to know who issued the event/message
  • Receiver doesn't have to handle the message/request immediatly

Unsurprisingly, this works with a queue

  • (ideally implemented as a ring buffer)
Programming 4 - Observer & Event Queue
Event queue

Usage example

When can we use this? For example in our GameActor::Die function we needed to play a sound. That function could call this function:

void PlaySound(SoundId id, int volume)
{
  auto sound = LoadSound(id);
  int channel = FindOpenChannel();
  if(channel == -1) return;
  StartPlayingSound(sound, channel, volume);
}

This has 3 issues:

Programming 4 - Observer & Event Queue
Event queue

Usage example

When can we use this? For example in our GameActor::Die function we needed to play a sound. That function could call this function:

void PlaySound(SoundId id, int volume)
{
  auto sound = LoadSound(id);
  int channel = FindOpenChannel();
  if(channel == -1) return;
  StartPlayingSound(sound, channel, volume);
}

This has 3 issues:

  1. The call is blocking/synchronous and can take a while
  2. The call is not thread safe
  3. The call is executed by the wrong thread.

So instead we could implement an event queue in the Audio API, that will process audio play requests on a separate thread.

Programming 4 - Observer & Event Queue
Event queue

Another example (C#)

Messenger.Broadcast(Messages.DialogStarted, new DialogArgs {
  dialogId = DialogueManager.instance.LastConversationID,
  title = DialogueManager.instance.lastConversationStarted
});
private void Start()
{
  _fpController = GetComponent<FPController>();
  Messenger.AddListener<DialogArgs>(Messages.DialogStarted, OnDialogStarted);
  Messenger.AddListener<DialogArgs>(Messages.DialogEnded, OnDialogEnded);
}
private void OnDialogStarted(DialogArgs args)
{
  bool rileyWalks = DialogueLua.GetActorField("Riley", "CanWalk").AsBool;
  if (!rileyWalks) {
      _fpController.DisableWalking();
  }
}
Programming 4 - Observer & Event Queue
Event queue

Another example (C++)

void dae::GameOverChecker::DisplayGameOver(const std::string& text) {
  EventManager::GetInstance().SendEvent(GameOverEventID);
  auto textComponent = GetOwner()->GetComponent<TextComponent>();
  textComponent->SetText(text, true);
  SetEnabled(false);
}
void dae::Ghost::Init() {
  EventManager::GetInstance().AttachEvent(GameOverEventID, handler);
  EventManager::GetInstance().AttachEvent(BoostStartEventID, handler);
  EventManager::GetInstance().AttachEvent(BoostEndEventID, handler);
}
void dae::Ghost::HandleEvent(const Event* pEvent) {
  switch(pEvent->GetID())
  {
  case GameOverEventID:
    ChangeToState(std::make_unique<GameOverState>(this));
    break;
  case BoostStartEventID:
    // etc...
  }
}
Programming 4 - Observer & Event Queue
Event queue

Considerations

  • Central Event Queues (like my Messenger system) are Global Variables
  • What happens with events that became obsolete?
  • Infinite loops
  • One to One, One to Many, Many to One, Many to Many?
    • Many to One
      • like with the audio
    • xx to Many
      • All listeners get the event/message?
      • Or only the first? Like a worker that takes the next task from a queue?
    • One to xx
      • Observer pattern
  • What about ownership of the objects in the queue?
Programming 4 - Observer & Event Queue
Event queue

Event Id

An event has two parts

  • Type (or Name, or Id)
  • Arguments

“EventId” like this? What are the pros and cons?

struct EventArg{};

struct Event {
  const std::string id;

  static const uint8_t MAX_ARGS = 8;
  uint8_t nbArgs;
  EventArg args[MAX_ARGS];

  explicit Event(const char* _id) : id{_id} {}
};
Event e("PlayerDied");
if(e.id == "PlayerDied") {
  // handle player death event
}
Programming 4 - Observer & Event Queue
Event queue

Event Id

An event has two parts

  • Type (or Name, or Id)
  • Arguments

“EventId” like this? What are the pros and cons?

struct EventArg{};

enum EventId {
  LEVEL_STARTED,
  BOMB_EXPLODED,
  PLAYER_DIED,
  //...
};

struct Event {
  const EventId id;  

  static const uint8_t MAX_ARGS = 8;
  uint8_t nbArgs;
  EventArg args[MAX_ARGS];

  explicit Event(EventId _id) : id{_id} {}
}
Event e(PLAYER_DIED);
if(e.id == PLAYER_DIED) {
  // handle player death event
}
Programming 4 - Observer & Event Queue
Event queue

Event Id

An event has two parts

  • Type (or Name, or Id)
  • Arguments

“EventId” like this? What are the pros and cons?

Yes! Only use scoped enums.

struct EventArg{};

enum class EventId {
  LEVEL_STARTED,
  BOMB_EXPLODED,
  PLAYER_DIED,
  //...
};

struct Event {
  const EventId id;  

  static const uint8_t MAX_ARGS = 8;
  uint8_t nbArgs;
  EventArg args[MAX_ARGS];

  explicit Event(EventId _id) : id{_id} {}
}
Event e(EventId::PLAYER_DIED);
if(e.id == EventId::PLAYER_DIED) {
  // handle player death event
}
Programming 4 - Observer & Event Queue
Event queue

Event Id

  • With strings
    • Id's don't need to be known in advance 😄
    • String comparisons 😭
  • With a scoped enum
    • Fast comparison/indexing 😄
    • All events are declared in one place 😢
    • Can’t be extended 😭

Applicable for small projects where either perfomance or extendability are not that important.

Any other options?

Programming 4 - Observer & Event Queue
Event queue

Event Id

We could make use of an SDBM hash

unsigned int sdbm_hash(const char *str) {
    unsigned int hash = 0;
    int c;

    while ((c = *str++)) {
        hash = c + (hash << 6) + (hash << 16) - hash;
    }

    return hash;
}

This function generates a simple (non-secure) hash of a given string in O(n) time.

Instead of EventId as a string or an enum we simply use an unsigned int and we generate id's with text:

Event e(sdbm_hash("PlayerDied"));

But what is the downside now?

http://www.cse.yorku.ca/~oz/hash.html

Programming 4 - Observer & Event Queue
Event queue

Event Id

template <int length> struct sdbm_hash 
{
    consteval static unsigned int _calculate(const char* const text, unsigned int& value) {
        const unsigned int character = sdbm_hash<length - 1>::_calculate(text, value);
        value = character + (value << 6) + (value << 16) - value;
        return text[length - 1];
    }

    consteval static unsigned int calculate(const char* const text) {
        unsigned int value = 0;
        const auto character = _calculate(text, value);
        return character + (value << 6) + (value << 16) - value;
    }
};

template <> struct sdbm_hash<1> {
    consteval static int _calculate(const char* const text, unsigned int& ) { return text[0]; }
};

template <size_t N> consteval unsigned int make_sdbm_hash(const char (&text)[N]) {
    return sdbm_hash<N - 1>::calculate(text);
}

Inspired by: "Learn C++ For Game Development"

Programming 4 - Observer & Event Queue
Event queue

Event Id

And now we can write:

struct EventArg{};

using EventId = unsigned int;

struct Event {
  const EventId id;

  static const uint8_t MAX_ARGS = 8;
  uint8_t nbArgs;
  EventArg args[MAX_ARGS];

  explicit Event(EventId _id) : id{_id} {}
}
Event e(make_sdbm_hash("PlayerDied"));
if(e.id == make_sdbm_hash("PlayerDied")) {
  // handle player death event
}
Programming 4 - Observer & Event Queue

Achievement from Team Fortress 2 Not very portable, right? Ask for alternatives. Let’s see what they come up with. Challenges: What about 100 kills. What about 20 kills with a flamethrower, etc...

Achievement from world of warcraft You need to keep track of the height when you jumped, you compare it with the height when you land, you cannot die of it + it needs to be on a hard surface...

This decouples code. The Physics engine can issue an event that an Entity fell, but whether or not there should be an achievement for that, or lives should be subtracted, or a level should be restarted is totally not up to the Physics engine. Note: the observer interface does not have to be written like this, the Notify method can have whatever parameters you want and you can have multiple Notify methods too. Often you have an event and eventargs.

Keep in mind: the parameters of the Notify function don’t have to be the ones as we present here. That’s why it is a pattern – adapt it to your needs.

In GC languages, if you remove an observer, you’ll need some Destroy() method that unregisters them from the subject, you don’t have a destructor to do that. = lapsed listener problem

Check the book for extra examples, even something in there with a ring buffer which is good for memory and data locality.

2nd bullet: By the time an event gets processed, it might not be necessary anymore, how do you handle that? 3rd bullet: If an eventhandler invokes an event that triggers the same handler you’re stuck. In a single threaded environment that’s fine, you’ll get a stackoverflow, but in a multithreaded context you’ll be keeping your CPU’s busy. 5th bullet: Pass ownership: caller to queue to receiver, or share ownership (with shared ptr) or the queue owns.

string comparisons!

enums/ints are easy to compare, but list needs to be known in advance no scoped enums :)

Downside is that this needs to be calculated at runtime.

The last method I've added myself: In C++, string literals like "QuitEvent" are of type const char[N], where N includes the null terminator ('\0'). When a function template takes an array by reference (e.g., const char (&text)[N]), the compiler deduces the size of the array (N) automatically.