ShaderOp.com

ShaderOp.com

Hi there. I have moved to a new website at Mhmmd.org. I'm no longer updating this one, but I'm keeping it around so the Internet wouldn't break. See you there.

A Simple, Intra-Process Event Bus in C++, Part I

A previous post introduced a way of assigning a unique ID number to arbitrary C++ types. As already mentioned in that post, my motivation for doing this was to lay the foundation for a simple, intra-process messaging system suitable for use in games (particularly the examples presented in Programming Game AI by Example).

This post presents a possible implementation for such a messaging system. To avoid confusion with inter-process or network messaging, I’ll refer to “messages” as “events” from now on, which is a less loaded and more accurate term.

But first, let’s introduce the hero.

Saving the world from nine to five

We’ll be building a console-based, non-interactive game that depicts a normal day in the life of a superhero. And please don’t take the phrase “normal day in the life of a superhero” to be some sort of cynical commentary about the state of contemporary story telling or the human condition on my part.

Our hero’s day consists of six main events: waking up, driving to work, checking in, checking out, driving back, arriving home, and going to bed. Here’s one possible way to present his entire miserable existence in code:

class Hero
{
private:
    int m_clockTicks;

public:
    Hero(): m_clockTicks(0) {}

    void Update()
    {
        switch (m_clockTicks++ % 6)
        {
        case 0:
            std::cout << "Hero: Yaaaaawn!" << std::endl;
            break;
        case 1:
            std::cout << "Hero: Getting into the Hero Mobile(tm)."
                      << std::endl;
            break;
        case 2:
            std::cout << "Hero: Punching into the Hero Cave(tm)."
                      << std::endl;
            break;
        case 3:
            std::cout << "Hero: Phew! Finally going home."
                      << std::endl;
            break;
        case 4:
            std::cout << "Hero: Honey! I'm home!"
                      << std::endl;
            break;
        case 5:
            std::cout << "Hero: Zzzzzzz..." << std::endl;
            break;
        default:
            break;
        }
    }
};

int main(int argc, char** argv)
{
    Hero theHero;

    for (int i = 0; i < 12; ++i)
    {
        theHero.Update();
    }
    return 0;
}

The program iterates over just two days of the hero’s life (hence the 12 in the for loop), and produces the following output:

Hero: Yaaaaawn!
Hero: Getting into the Hero Mobile(tm).
Hero: Punching into the Hero Cave(tm).
Hero: Phew! Finally going home.
Hero: Honey! I'm home!
Hero: Zzzzzzz...
Hero: Yaaaaawn!
Hero: Getting into the Hero Mobile(tm).
Hero: Punching into the Hero Cave(tm).
Hero: Phew! Finally going home.
Hero: Honey! I'm home!
Hero: Zzzzzzz...

Certain species of plankton probably lead more exciting lives, but that’s beside the point.

This game, as awesome as it is already, is missing something. Something that would turn it into a classic. Something that would make it stand shoulder to shoulder next to Fallout 3, Fable II, and Torchlight. It needs a pet.

Like any good, faithful companion, the pet will greet our hero every time he comes back from work. So, we’ll need a way to communicate that event to the pet and any other interested parties (like, say, the Mad Scientist’s advanced abduction and extraction team).

There are many ways to go about implementing this, but for the purpose of maximum decoupling between components it would probably be best to use some sort of event bus, where any object can send an event to the bus, which in turn takes care of relaying the events to all objects interested in receiving them.

After some false starts and dead ends, I ended up with a solution composed of the following components:

  • TypeIdentifer<T>: discussed at length in a previous post.
  • Event: The base case for all events sent and received in the system.
  • IReceiver: An interface to be implemented by event receivers.
  • IEventBus: The interface for the event bus.
  • EventBus: A concrete implementation of IEventBus.

What follows is a closer look at some of these components.

The Event class

This one is ridiculously simple:

namespace Events
{
    class Event
    {
    public:
        virtual ~Event() {}
    };
}

It’s just a marker interface that does nothing on its own.

Our game will have just one event, HeroMovedEvent, defined as follows:

class HeroMovedEvent : public Events::Event
{
public:
    enum Locations { HOME, WORK, ROAD };
private:
    Locations m_location;
public:
    explicit HeroMovedEvent(Locations location)
        : m_location(location)
    {
    }

    Locations Location() const
    {
        return m_location;
    }
};

We’ll see how to use it a bit later.

The IReceiver interface

Which declares a single virtual method:

namespace Events
{
    class IReceiver
    {
    public:
        virtual void Receive(unsigned int eventId, Event* e) = 0;
        virtual ~IReceiver() {};
    };
}

A partial implementation of the Hero’s pet would look like this:

class HerosPet: public Events::IReceiver
{
    /* Some detailed omitted */
public:

    void Receive(unsigned int eventId, Events::Event* e)
    {
        if (eventId == Events::TypeIdentifier<HeroMovedEvent>::id())
        {
            HeroMovedEvent* e_ = static_cast<HeroMovedEvent*>(e);
            OnHeroMoved(e_);
        }
    }

    void OnHeroMoved(HeroMovedEvent* e)
    {
        if (e->Location() == HeroMovedEvent::HOME)
        {
            std::cout << "Pet:  Woof! Woof!" << std::endl;
        }
    }
};

We’ll add the missing details in just a bit.

The IEventBus interface

Defined thusly:

namespace Events
{
    class IEventBus
    {
    public:
        virtual void Register(unsigned int eventId, IReceiver* receiver) = 0;
        virtual void Unregister(unsigned int eventId, IReceiver* receiver) = 0;
        virtual void Unregister(IReceiver* receiver) = 0;
        virtual void Broadcast(unsigned int eventId, Event* event) = 0;
        virtual ~IEventBus() {}
    };
}

Any class that wants to be notified of events registers itself with the bus by passing the event id (obtained through TypeIdentifier<T>::id method) and itself to the Register method. The reverse operation is done by passing the same arguments to the Unregister method. The second version of Unregister is just a convenience method that unsubscribes a receiver from all events.

I provided an implementation for this interface in a class named EventBus, the details of which aren’t very interesting, so I won’t go over them here.

The Revised Game

Using the components above, the Hero class changes to this:

class Hero
{
private:
    int m_clockTicks;
    Events::IEventBus* m_eventBus;
public:
    explicit Hero(Events::IEventBus* eventBus)
        : m_clockTicks(0), m_eventBus(eventBus) {}

    void Update()
    {
        switch (m_clockTicks++ % 6)
        {
        case 0:
            {
                std::cout << "Hero: Yaaaaawn!" << std::endl;
                break;
            }
        case 1:
            {
                std::cout << "Hero: Getting into the Hero Mobile(tm)."
                          << std::endl;
                HeroMovedEvent e(HeroMovedEvent::ROAD);
                m_eventBus->Broadcast
                    ( Events::TypeIdentifier<HeroMovedEvent>::id()
                    , &e
                    );
                break;
            }
        case 2:
            {
                std::cout << "Hero: Punching into the Hero Cave(tm)."
                          << std::endl;
                HeroMovedEvent e(HeroMovedEvent::WORK);
                m_eventBus->Broadcast
                    ( Events::TypeIdentifier<HeroMovedEvent>::id()
                    , &e
                    );
                break;
            }
        case 3:
            {
                std::cout << "Hero: Phew! Finally going home."
                          << std::endl;
                HeroMovedEvent e(HeroMovedEvent::ROAD);
                m_eventBus->Broadcast
                    ( Events::TypeIdentifier<HeroMovedEvent>::id()
                    , &e
                    );
                break;
            }
        case 4:
            {
                std::cout << "Hero: Honey! I'm home!"
                          << std::endl;
                HeroMovedEvent e(HeroMovedEvent::HOME);
                m_eventBus->Broadcast
                    ( Events::TypeIdentifier<HeroMovedEvent>::id()
                    , &e
                    );
                break;
            }
        case 5:
            {
                std::cout << "Hero: Zzzzzzz..." << std::endl;
                break;
            }
        default:
            break;
        }
    }
};

Nothing fancy here. The Hero class raises a HeroMovedEvent whenever the Hero changes locations.

And here’s the full implementation of the HerosPet class presented earlier:

class HerosPet: public Events::IReceiver
{
private:
    Events::IEventBus* m_eventBus;
public:
    explicit HerosPet(Events::IEventBus* eventBus)
        : m_eventBus(eventBus)
    {
        m_eventBus->Register
            ( Events::TypeIdentifier<HeroMovedEvent>::id()
            , this
            );
    }

    ~HerosPet()
    {
        m_eventBus->Unregister(this);
    }

    void Receive(unsigned int eventId, Events::Event* e)
    {
        if (eventId == Events::TypeIdentifier<HeroMovedEvent>::id())
        {
            HeroMovedEvent* e_ = static_cast<HeroMovedEvent*>(e);
            OnHeroMoved(e_);
        }
    }

    void OnHeroMoved(HeroMovedEvent* e)
    {
        if (e->Location() == HeroMovedEvent::HOME)
        {
            std::cout << "Pet:  Woof! Woof!" << std::endl;
        }
    }
};

And here’s the main method that wires all the components together:

int main(int argc, char** argv)
{
    Events::EventBus theBus;
    Hero theHero(&theBus);
    HerosPet thePet(&theBus);

    for (int i = 0; i < 12; ++i)
    {
        theHero.Update();
    }
    return 0;
}

And here’s the output:

Hero: Yaaaaawn!
Hero: Getting into the Hero Mobile(tm).
Hero: Punching into the Hero Cave(tm).
Hero: Phew! Finally going home.
Hero: Honey! I'm home!
Pet:  Woof! Woof!
Hero: Zzzzzzz...
Hero: Yaaaaawn!
Hero: Getting into the Hero Mobile(tm).
Hero: Punching into the Hero Cave(tm).
Hero: Phew! Finally going home.
Hero: Honey! I'm home!
Pet:  Woof! Woof!
Hero: Zzzzzzz...

This works nicely. The sender and receiver only need to know about the IEventBus interface and the type of the events they wants to exchange. But there’s one problem, and that is the Receive method in HerosPet:

    void Receive(unsigned int eventId, Events::Event* e)
    {
        if (eventId == Events::TypeIdentifier<HeroMovedEvent>::id())
        {
            HeroMovedEvent* e_ = static_cast<HeroMovedEvent*>(e);
            OnHeroMoved(e_);
        }
    }

This is fine for now, but as the number of events increases, the Receive method will grow into a series of ugly if-else statements, reminiscent of the Windows message loop.

Ideally, this should be made easier by using something like event tables in wxWidgets or message maps in WTL, preferably without using any pre-processor hacks. And I’ll try do just that in the next post. So I’ll defer providing the full source code until then.

No Comments

Powered by: