All interactive game architectures are, by their nature, forced to be somewhat event-driven. There are various hardware-based events for input, audio and network systems, etc.
Furthermore, in modern game systems events are often used to drive many other aspects, such as:
- handling abstracted input from Gui controls, such as ButtonClicked and MouseEnter
- producer/consumer models for long-running tasks such as path-finding
- collision events emitted from the physics simulator
- processing abstracted network events above the raw hardware layer
So there is a requirement for an efficient, flexible and expressive event system for C++, which is unfortunately lacking from the standard library.
Of course, there are mechanisms in the Boost libraries that can be used to implement such event-based systems, but game developers are generally wary of adding a dependency on boost-related code to their source base.
This blog entry will introduce a multi-cast event model that consists of exactly one header file which has exactly two external dependencies. These are the standard C++ <list> and <memory> headers. The library resides entirely in the header; there are no associated source files or libraries to link with.
Events from this system can delegate to functions, methods, or other events. The delegate methods can be const or non-const, and the arguments can be any mix of value or reference types. Events are copy-constructable and assignable.
The maximum number of arguments in an event signature is fixed to eight for this implementation, but that can be increased by changing a constant and rebuilding the header.
So here’s some example use, given the one header file:
using namespace std;
using namespace Schladetsch::Events;
void Method(int num, const string &str);
void Fun1(int num, const string &str);
void Fun2(int num, const string &str);
// make an event that has two parameters
Event<int, const string&> event;
// add a delegate method
// add a delegate function
// fire the event: the foo.Method will be called,
// as well as Fun1
event(42, string("Hello, Events"));
// remove Fun1 from the event
event(123, string("Fun1 not called"));
// it is perfectly safe to copy events
Event<int, const string&> other(event);
other(456, "Both Foo::Method and Fun1 called");
// we can also 'chain' events: by adding one event to another,
// the added event will be fired when the parent event is fired
Event<int, const string&> chained;
event(789, "Foo::Method called, as well as firing chained, which will call Fun2");
Events are templates that build the signature of supported delegates from their template type parameters.
The interface to the system is minimal, with just two methods ‘Add’ and ‘Remove’, to add and remove delegates from events.
Invoking, or firing, the event looks just like a function call. When the event is fired, all delegates that are stored in the event are invoked in order that they were added.
I could have added operator overloading for += and -= to add and remove delegates, as used in C#, but I considered that a little too twee.
The system is based on the idea of decoupling the actual delegate from the way that it is invoked. The event object itself stores a list of pointers to generalised delegates. When the event is fired, it iterates through its delegates, passing the arguments to each. As such, please use reference types for large objects, including strings – but you do that anyway.
Along with the base Invoker arity-type (see below) stored in event instances, there are arity-types for delegated functions, const methods, non-const methods, and events. ‘Delegated events’ in this sense are used for chaining events together, such that when one event is fired, the next chained event is also fired. Syntactically, chaining events is exactly like adding a new delegate to an event.
I used the term ‘arity-type’ above to describe a collection of C++ types that vary only by the arity (number of meaningful type parameters) that they support, but are otherwise semantically equivalent. The general pattern used to implement an arity-type is:
// forward declare the general case
// specialise for the case of no arguments
template <class T0, class T1, ..., class Tn>
// implementation for arity-0
// specialise for the case of m arguments
template <class T0, class T1, ..., class Tn>
// implementation for arity-m, using type arguments T0...Tm
If you think this gets tedious for arities up to eight arguments, with four different delegate types (function, method, const method, chained event), plus the base invoker type – you’re absolutely correct. That’s why I pulled out the big guns to help with the implementation.
The system was made using the EventP.h, is created by running the source headers through the C++ pre-processor and manually editing the result.
Alternatively, one can use the underlying headers directly by including Event.h instead, but this will require that you have all the library headers, as well as Boost.Preprocessor header files available.
A benefit of using the post-processed headers is that compilation time is kept to a minimum as only two external headers are included: the standard <list> used to store delegates within an event, and <memory> for tr1::shared_ptr.
Add EventP.h to a location in your include path.
This is not meant to be a human-readable file, but it is useful when debugging, as it contains the post-processed output from the source.
The default fully-qualified type name for an event is Schladetsch::Events::Event. You probably don’t want to use that name, so set the SCHLADETSCH_NAMESPACE pre-processor symbol to something else before including EventP.h.
There is a single virtual method call required to invoke each delegate within an event when the event is fired.
This implementation is not thread-safe.
This post has described what and where, but not much of why or how. More work is required to describe how to use these types of systems well, and why they are useful to modern game development practices.
As pointed out in the comments below, there are other similar libraries available, such as those supplied by Don Clugston.
This blog post introduced a mechanism that you can use to implement a Subscriber/Publisher pattern (also known as Signals/Slots) to your system architecture, by including just one header file.
Thanks to Chris Regnier for the excellent comments.
I hope you find this system useful, and I appreciate all feedback.
/* ]]> */