University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Event System

Overview

This lab builds an EventManager that can broadcast several different kinds of information to a set of independent listeners, without the manager or the listeners knowing about each other's concrete types. Every event is a small object derived from a shared Event base class, so an EnemyDefeatEvent and a NewAreaExploredEvent can travel through the same DispatchEvent call even though they carry completely different data. You will complete EventManager's listener bookkeeping and dispatch logic, then build two listeners, LootManager and ExperienceManager, that each use dynamic_cast to work out which event just arrived and react only to the ones they are written to handle.

Learning Objectives

By the end of this lab you will be able to:

Concept

A listener interface with one fixed update(float temperature) signature, like the one built in the Observer Pattern lab, only works while every notification carries the same kind of data. Once a system needs to announce unrelated things, an enemy being defeated in one case and a new area being explored in another, forcing every notification through one fixed parameter stops working, since those two situations do not share a payload.

An Event hierarchy solves this by giving every kind of notification its own class instead of its own function signature. Event is a small polymorphic base class that declares nothing but a virtual destructor, and concrete event types such as EnemyDefeatEvent and NewAreaExploredEvent derive from it and add whatever fields that particular event needs. EventManager keeps a single std::vector<EventListener*>, exactly like the Observer Pattern lab's collection of observers, and its DispatchEvent(Event* event) loops over that vector calling OnEvent(event) once per listener, regardless of which concrete event type event actually points at. Every listener receives the same Event* and has to work out for itself what it is actually looking at.

That is what dynamic_cast is for. dynamic_cast<T*>(basePointer) returns a valid T* when basePointer genuinely points at a T, and nullptr otherwise, so if (T* derived = dynamic_cast<T*>(event)) works as both the test and the cast in a single line. A listener's OnEvent chains one dynamic_cast per event type it cares about, reacting inside each successful branch and doing nothing when every branch fails.

Tasks

Task 1 - Create the project and the Event hierarchy

In Visual Studio, create a new Console App (C++) project the same way as the Environment Setup lab, called EventDemo. Every notification in this lab is going to be an object derived from one shared base class. In Solution Explorer, use Add > New Item... to create a header file named Event.h. Replace its generated content with the code below.

Success condition: the project still builds once Event.h is in place. Event and EnemyDefeatEvent are just data at this point, so there is nothing to run yet.

Event.h
#pragma once

class Event
{
public:
    virtual ~Event() = default;
};

class EnemyDefeatEvent : public Event
{
public:
    int enemyId;
    float x, y, z;

    EnemyDefeatEvent(int id, float x, float y, float z) : enemyId(id), x(x), y(y), z(z) {}
};

Task 2 - Create the EventListener interface

Any class that wants to be told about events can implement this same interface. Use Add > New Item... to create a header file named EventListener.h. Replace its generated content with the code below.

Success condition: the project still builds with EventListener.h in place. Nothing implements EventListener yet, so there is still nothing to run.

EventListener.h
#pragma once

#include "Event.h"

class EventListener
{
public:
    virtual void OnEvent(Event* event) = 0;
    virtual ~EventListener() = default;
};

Task 3 - Implement EventManager

Use Add > New Item... to create a header file named EventManager.h. The diagram below shows the shape DispatchEvent needs to end up with, one manager looping over however many listeners are currently attached and calling OnEvent on each. Replace the generated content with the code below, then complete the three marked functions.

  1. In AddListener(), add listener to the listeners vector with push_back.
  2. In RemoveListener(), remove listener from the listeners vector using the erase-remove idiom, listeners.erase(std::remove(listeners.begin(), listeners.end(), listener), listeners.end()).
  3. In DispatchEvent(), loop over listeners and call listener->OnEvent(event) on each one.

Success condition: the project still builds with all three functions completed. There is still nothing to run yet, since nothing has attached itself as a listener until a later task.

EventManager.h
#pragma once

#include <algorithm>
#include <vector>

#include "EventListener.h"

class EventManager
{
public:
    void AddListener(EventListener* listener)
    {
        // TODO: add listener to the listeners vector
    }

    void RemoveListener(EventListener* listener)
    {
        // TODO: remove listener from the listeners vector using the erase-remove idiom
    }

    void DispatchEvent(Event* event)
    {
        // TODO: call OnEvent(event) on every listener in listeners
    }

private:
    std::vector<EventListener*> listeners;
};

Task 4 - Create LootManager

A listener does not have to react to every event it is handed, it only has to check for the ones it cares about. Use Add > New Item... to create a header file named LootManager.h, implementing EventListener. Replace its generated content with the code below, then complete OnEvent() so it checks whether event is actually an EnemyDefeatEvent using dynamic_cast, and if it is, prints a message that includes enemyId.

Success condition: the project still builds with LootManager.h in place. There is still nothing to run yet, since nothing has constructed a LootManager or dispatched an event until the next task.

LootManager.h
#pragma once

#include <iostream>

#include "EventListener.h"

class LootManager : public EventListener
{
public:
    void OnEvent(Event* event) override
    {
        // TODO: use dynamic_cast to check whether event is an EnemyDefeatEvent
        // TODO: if it is, print a message that includes its enemyId
    }
};

Task 5 - Attach LootManager and dispatch an event

Open EventDemo.cpp, the file the project wizard created in Task 1, and complete main() so it attaches lootManager to eventManager and dispatches one EnemyDefeatEvent.

  1. Call eventManager.AddListener(&lootManager).
  2. Call eventManager.DispatchEvent(&defeatEvent).

Success condition: build and run. LootManager's message prints once, showing the enemyId of 7 that defeatEvent was constructed with.

EventDemo.cpp
#include "EventManager.h"
#include "LootManager.h"

int main()
{
    EventManager eventManager;
    LootManager lootManager;

    // TODO: add lootManager as a listener on eventManager

    EnemyDefeatEvent defeatEvent(7, 12.0f, 0.0f, 4.0f);

    // TODO: dispatch defeatEvent through eventManager

    return 0;
}

Task 6 - Create ExperienceManager

The same EnemyDefeatEvent can reach more than one listener, and neither listener has to know the other exists. Use Add > New Item... to create a header file named ExperienceManager.h, implementing EventListener. Replace its generated content with the code below, then complete OnEvent() the same way as LootManager, checking for EnemyDefeatEvent with dynamic_cast and printing a message that includes enemyId.

Success condition: the project still builds with ExperienceManager.h in place. There is still nothing to run yet, since ExperienceManager has not been attached to eventManager until the next task.

ExperienceManager.h
#pragma once

#include <iostream>

#include "EventListener.h"

class ExperienceManager : public EventListener
{
public:
    void OnEvent(Event* event) override
    {
        // TODO: use dynamic_cast to check whether event is an EnemyDefeatEvent
        // TODO: if it is, print a message that includes its enemyId
    }
};

Task 7 - Attach ExperienceManager and observe both listeners react

Extend main() in EventDemo.cpp to attach experienceManager as a second listener before defeatEvent is dispatched.

  1. Include ExperienceManager.h alongside the existing includes.
  2. Construct an ExperienceManager experienceManager; next to lootManager.
  3. Call eventManager.AddListener(&experienceManager).

Success condition: build and run. Both LootManager's message and ExperienceManager's message print for the single DispatchEvent call, since DispatchEvent loops over every attached listener and both of them recognise defeatEvent as an EnemyDefeatEvent.

EventDemo.cpp
#include "EventManager.h"
#include "LootManager.h"
#include "ExperienceManager.h"

int main()
{
    EventManager eventManager;
    LootManager lootManager;
    ExperienceManager experienceManager;

    eventManager.AddListener(&lootManager);
    // TODO: add experienceManager as a listener on eventManager

    EnemyDefeatEvent defeatEvent(7, 12.0f, 0.0f, 4.0f);
    eventManager.DispatchEvent(&defeatEvent);

    return 0;
}

Task 8 - Add a second event type

Every event so far has been an EnemyDefeatEvent. Open Event.h and add a second class, NewAreaExploredEvent, below EnemyDefeatEvent, carrying an area name instead of an enemy id and a position.

Success condition: the project still builds with NewAreaExploredEvent in place. Neither EventManager nor EventListener needs to change to support it, since both already work in terms of the Event base class.

Event.h
#pragma once

#include <string>

class Event
{
public:
    virtual ~Event() = default;
};

class EnemyDefeatEvent : public Event
{
public:
    int enemyId;
    float x, y, z;

    EnemyDefeatEvent(int id, float x, float y, float z) : enemyId(id), x(x), y(y), z(z) {}
};

class NewAreaExploredEvent : public Event
{
public:
    std::string areaName;

    NewAreaExploredEvent(std::string name) : areaName(name) {}
};

Task 9 - Extend LootManager to handle the new event type

One listener can care about more than one event type by chaining another dynamic_cast check. Open LootManager.h and add a second check inside OnEvent(), below the existing EnemyDefeatEvent check, for NewAreaExploredEvent. When it matches, print a message that includes areaName.

The diagram below shows the shape OnEvent() will have once both checks are in place. The two dynamic_cast checks are independent if statements, not an if/else chain, so the second check still runs even when the first one matches, and an event that matches neither check simply falls through to return without printing anything.

LootManager.h
#pragma once

#include <iostream>

#include "EventListener.h"

class LootManager : public EventListener
{
public:
    void OnEvent(Event* event) override
    {
        if (EnemyDefeatEvent* defeat = dynamic_cast<EnemyDefeatEvent*>(event))
        {
            std::cout << "Loot dropped for enemy " << defeat->enemyId << std::endl;
        }

        // TODO: use dynamic_cast to check whether event is a NewAreaExploredEvent
        // TODO: if it is, print a message that includes its areaName
    }
};

Task 10 - Dispatch the new event type

Extend main() in EventDemo.cpp to construct and dispatch a NewAreaExploredEvent after the existing defeatEvent dispatch.

  1. Construct NewAreaExploredEvent exploreEvent("Riverside Camp");.
  2. Call eventManager.DispatchEvent(&exploreEvent).

Success condition: build and run. LootManager's area message prints for exploreEvent, but ExperienceManager prints nothing for it, since ExperienceManager::OnEvent only ever checks for EnemyDefeatEvent. The same eventManager is now carrying two unrelated event types, and each listener decides for itself which ones matter to it.

EventDemo.cpp
#include "EventManager.h"
#include "LootManager.h"
#include "ExperienceManager.h"

int main()
{
    EventManager eventManager;
    LootManager lootManager;
    ExperienceManager experienceManager;

    eventManager.AddListener(&lootManager);
    eventManager.AddListener(&experienceManager);

    EnemyDefeatEvent defeatEvent(7, 12.0f, 0.0f, 4.0f);
    eventManager.DispatchEvent(&defeatEvent);

    // TODO: construct exploreEvent as a NewAreaExploredEvent named "Riverside Camp"

    // TODO: dispatch exploreEvent through eventManager

    return 0;
}

Task 11 - Remove a listener dynamically

Listeners can be removed while the program is still running, exactly like the Observer Pattern lab's detach(). Extend main() in EventDemo.cpp to remove experienceManager and dispatch one more EnemyDefeatEvent.

  1. Call eventManager.RemoveListener(&experienceManager).
  2. Construct EnemyDefeatEvent secondDefeatEvent(3, 5.0f, 1.0f, 2.0f);.
  3. Call eventManager.DispatchEvent(&secondDefeatEvent).

Success condition: build and run. Only LootManager's message prints for secondDefeatEvent, since experienceManager was removed from eventManager's listeners before that dispatch, even though secondDefeatEvent is exactly the kind of event ExperienceManager would otherwise react to.

EventDemo.cpp
#include "EventManager.h"
#include "LootManager.h"
#include "ExperienceManager.h"

int main()
{
    EventManager eventManager;
    LootManager lootManager;
    ExperienceManager experienceManager;

    eventManager.AddListener(&lootManager);
    eventManager.AddListener(&experienceManager);

    EnemyDefeatEvent defeatEvent(7, 12.0f, 0.0f, 4.0f);
    eventManager.DispatchEvent(&defeatEvent);

    NewAreaExploredEvent exploreEvent("Riverside Camp");
    eventManager.DispatchEvent(&exploreEvent);

    // TODO: remove experienceManager from eventManager's listeners

    // TODO: construct secondDefeatEvent as an EnemyDefeatEvent with id 3

    // TODO: dispatch secondDefeatEvent through eventManager

    return 0;
}

Solution

Try the tasks above before revealing the solution.