Skip to content

Instantly share code, notes, and snippets.

@dbechrd
Last active May 25, 2020 16:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dbechrd/bb131ed8f6b4f8e66f1307018594c9b3 to your computer and use it in GitHub Desktop.
Save dbechrd/bb131ed8f6b4f8e66f1307018594c9b3 to your computer and use it in GitHub Desktop.
Pseudocode for a very basic event system in a game engine
At a high level, events work as follows:
- Each type of event must have a unique name or ID (e.g. "user_joined_game")
- Each type of event may have additional metadata (e.g. the "user_joined_game" event will likely have a "user_id" in the metadata)
- Zero or more event handlers (in the form of callback function, also sometimes called "listeners" and owned by "subscribers") can be registered for each type of event. This can be done in many different ways, one way would be to have a central event manager that keeps track by having a list of subscribers for each type of event. In a more advanced implementation, you could also have filters (e.g. only send me "user_position_changed" events for "user_id = 5"). I would recommend starting with a simple boolean "handled" flag as a filter to start (see below).
- Zero or more places where an event is fired (also called "triggered", done by a "publisher").
- When an even is fired/triggered by a publisher, it goes into an event queue. Generally you'd have one portion of a frame where everything is firing events and you're collecting them in the event queue, then a second portion of the frame where the event manager pops things off the queue (in the same order they were pushed, hence "queue") and calls all of the registered callbacks for each event (generally in the order they were registered, though there could be some priority levels to determine which subscribers are allowed to handle an event first).
- When an event callback is called (i.e. the "listener" receives the event), it generally has side effects (e.g. "player_shoot" might queue a sound effect to be played), and it may also mark the event as "handled" (via a handled flag on the event). Once an event has been marked handled, the event manager will stop calling callbacks for that event, and move on to the next event in the event queue.
- Event handlers may or may not queue additional events. This is up to the implementation and your needs. You have to be careful if you allow this, because it could create an infinite loop.
// Event.h
enum EventType {
EVENT_TYPE_PLAYER_JOIN_GAME,
EVENT_TYPE_PLAYER_SHOOT,
EVENT_TYPE_COUNT
};
struct EventPlayerJoinGameData {
int player_id;
};
struct EventPlayerShootData {
int player_id;
int weapon_id;
vec3 player_position;
float player_orientation;
};
// NOTE: In C++ you'd probably use inheritance for this, where every event data type inherits from an "EventData" base class
union EventData {
EventPlayerJoinGameData player_join_game;
EventPlayerShootData player_shoot;
};
struct Event {
EventType type;
EventData data;
bool handled;
};
// NOTE: This should probably be an interface in C++. If you not familiar with this syntax, this
// creates a function pointer type called "EventHandler" which takes a reference to an event and
// returns void. This allows us to store pointers to handlers in the subscribers vector, iterate
// over them and call them for each event (see: PublishEvents()).
typedef void (*EventHandler)(Event &event);
// EventManager.h
class EventManager
{
void Subscribe(EventType type, EventHandler *handler);
Event &QueueEvent(EventType type);
private:
std::queue<Event> event_queue;
std::vector<EventHandler *> subscribers[EVENT_TYPE_COUNT];
};
// EventManager.cpp
void EventManager::Subscribe(EventType type, EventHandler *handler)
{
this->subscribers[type].push_back(handler);
}
// Push a new event and return it so that the caller can fill out the metadata if they need to
Event &EventManager::QueueEvent(EventType type)
{
Event event = { 0 };
event.type = type;
event_queue.push(event);
return event_queue.back();
}
// Call this every frame
void EventManager::PublishEvents()
{
// For each event queued this frame
while (!this->event_queue.empty()) {
Event &event = this->event_queue.front();
// Notify all subscribers (if any) that this event occurred
for (auto it = std::begin(this->subscribers[event->type]); it != std::end(this->subscribers[event->type]); ++it) {
EventHandler *handler = *it;
handler(event);
// If this subscriber marked this event as handled, we're done processing and can move on. Not all subscribers
// mark events as handled. AudioSystem doesn't, for example, because you might still need to deal damage, etc.
// This flag may not even be necessary in your engine, perhaps you can just let every subscriber receive every
// event and that's fine.
if (event->handled) {
break;
}
}
this->event_queue.pop();
}
}
// Player.cpp
// Rather than doing damage and playing audio in the Player::Shoot() method, this method just queues an event. Other systems
// can handle that event how they see fit. E.g. AudioSystem can subscribe to this event and play a sound effect, while
// ProjectileSystem can subscribe to this event and manage e.g. bullets. Later, if the bullet hits something, BulletSystem
// can queue a ProjectileCollisionEvent. AudioSystem can use that event to play an on-hit sound effect, ParticleSystem can
// use it to display smoke/sparks, DecalSystem can use it to paint a bullet hole on the wall, and DamageSystem can use it
// to deal damage to any entities that were within the damage radius.
Player::Shoot()
{
Event &event = EventManager.QueueEvent(EVENT_TYPE_PLAYER_SHOOT);
event->data.player_shoot.player_id = this->id;
event->data.player_shoot.weapon_id = this->equipped_weapon_id;
event->data.player_shoot.player_position = this->position;
event->data.player_shoot.player_orientation = this->orientation;
}
// AudioSystem.cpp
// For now this is just a static function, you could have AudioSystem inherit from
// an interface (e.g. IEventHandler) and implement a general HandleEvent() method
// which has a switch statement that handles each event type that the audio system
// cares about. There are infinitely many ways to impelement these things.
static void PlayerShootHandler(Event &event)
{
EventPlayerShootData *data = &event->data.player_shoot;
// Find sound effect for the weapon that was fire
int sound_effect_id = Weapon::Find(data->weapon_id).sound_effect_id;
vec3 position = data->player_position;
// Play sound effect at position where the weapon was fired from
AudioSystem::QueueSoundEffect(sound_effect_id, position);
}
// When systems are initialized, they subscribe to all of the events they care about
AudioSystem::Init()
{
EventManager.Subscribe(EVENT_TYPE_PLAYER_SHOOT, AudioSystem::PlayerShootHandler);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment