Game Engine Architecture Spring 2017 03. Event systems for game engines
Juha Vihavainen University of Helsinki [ [ McShaffry,McShaffry , Ch. 6: Game actors and component architecture ,, Ch. 11: Game event management ]] [ Gregory, Ch. 5.4: Strings, unique ids ,, localization , etc., 274, Ch.15.2: Runtime object model architectures , 873 Ch. 15.7: Events and message passing, 933 ]] [Meyers (2015): Effective Modern C ++++]]
Lecture outline
On events and event handling using Observer (MVC) and Command design patterns
Updates are not enough for game software
Typed Message design pattern (Vlissides, 1997) breaking dependencies between senders & receivers
Queueing of messages for later delivery
Using unique object ids for game entities and types
24.2.2017 Juha Vihavainen / University of Helsinki 22 The Observer design pattern
Problem certain objects need to be informed about the changes occurring in in other objects a subject has to be observed by one or more observers decouple as much as possible and reduce the dependencies
Solution define a oneone--toto--manymany dependency between objects so that when one object changes its state,state , all its dependents are automatically notified and updated
A cornerstone of the ModelModel--ViewView--ControllerController architectural design, where the Model implements the mechanics of the program, and the Views are implemented as Observers that are kept uncoupled from the Model components Modified from Joey Paquet, 20072007--20142014
24.2.2017 University of Helsinki 33
Erich Gamma et al., Design Patterns ( 1994) Observer pattern: class design
""View "" classes
""ModelModel""classesclasses
Updates multiple ( seems Notifies all its observers complicated..) observers on changes
24.2.2017 University of Helsinki 44 The participants
Subject --abstractabstract class defining the operations for attaching and de--de attaching observers to the clientclient;; often referred to as " Observable ""
ConcreteSubject -- concrete Subject class. It maintains the state of the observed object and when a change in its state occurs it notifies the attached Observers. If used as part of MVC, the ConcreteSubjectConcreteSubject classes are the Model classes that have Views attached to themthem
Observer --interfaceinterface or abstract class defining the operations to be used to notify the registered Observer objects
ConcreteObserver -- concrete observer subclasses that are attached to a particular subject class
There may be multiple different concrete observers attached to a single subject that will provide a different view of that subject
24.2.2017 University of Helsinki 55
On behaviour of Observer pattern
The client class instantiates the ConcreteObservable object
Then it instantiates and attaches the concrete observers to it usingusing the methods defined in the Observable interface
Each time the (observable) state of the subject is changing, it notifies all the attached observers using the methods defined in the Observer interface
When a new Observer is added to the application, all we need to do is to instantiate it in the client class and to attach it to the Observable object
The classes already created (framework) will remain mostly unchanged
24.2.2017 University of Helsinki 66 77 Observer pattern: interface classes
// Observable.h #pragma once // Visual Studio include guard class Observer ; // name stub needed only // Observer.h class Observable { // an abstract class #pragma once public : virtual void attach ( Observer *); class Observer { // an abstract class virtual void detach ( Observe r *); public : virtual void notify (); virtual void update () = 0; virtual ~Observable (); virtual void detach () = 0; // added Observable (Observable const&) = delete; virtual ~Observer (); ... Observer (Observer const&) = delete; protected : ... Observable (); // called by subclass protected : private : Observer (); // called by subclass struct Observers ; // name stub only }; Observers * observers_; }; Note . If a destructor is declared, the generation of default copy operations is deprecated (to be removed from standard). Pimpl idiom hides std::list
Meyers S. (2015): On C++ technicalities Effective Modern C++, p. 115 The special member functions are those compilers may generate on their own (if needed, i.e., called): default constructor , destructor , copy operations , and move operations Move operations (T &&) are generated only for classes lacking explicitly declared move operations, copy operations, and a destructor The copy constructor (T const &) is generated only for classes lacking an explicitly declared copy constructor, and it’s deleted if a move operation is declared The copy assignment operator == (T const &) ) is generated only for classes lacking an explicitly declared copy assignment operator, and it’s deleted if a move operation is declared Generation of the copy operations in classes with an explicitly declared destructor is deprecated (i.e., to be removed from C++ standard) Member function templates never suppress generation of special member functions (even if templates could produce these copy operations)
24.2.2017 University of Helsinki 88 99 Observer PatternPattern:: Concrete Subject
// ClockTimer.cpp #include "ClockTimer.h " // ClockTimer.h ClockTimer :: ClockTimer () #pragma once : hour_( 0), minute_(0 ), second_(0 ) {} #include "Observable.h " void ClockTimer :: start ( int time ) { class ClockTimer : public Observable for (int i = 1; i <= time ; ++i) { tick (); public : } ClockTimer (); void ClockTimer :: tick () { // by one sec int hour () const { return hour_; } ++second_; int minute () const { return minute_; } if (second_>= 60 ) { int second () const { return second_; } ++minute_; second_= 0; void start ( int time); if (minute_>= 60 ) { void tick (); ++hour_; minute_= 0; private : if (hour_>= 24 ) int hour_; hour_= 0; int minute_; } int second_; } }; // the Observable object notifies // all its registered observers notify (); }
1010 Observer PatternPattern:: // DigitalClock.cpp Concrete Observer #include "DigitalClock.h " #include "ClockTimer.h " // DigitalClock.h #include
// ClockDriver.cpp #include "ClockTimer.h " #include "DigitalClock.h " #include
// create a DigitalClock that is connected to the ClockTimer DigitalClock digitalClock (&timer );
// advancing the ClockTimer updates the DigitalClock // as tick() calls Update() after it changed its state int secs ; std::cout << "Enter number of seconds to count: " ; std::cin >> secs ; timer.start ( secs );
int j; std::cin >> j; // pause at end of program }
24.2.2017 University of Helsinki 1111
Implementation of the Observer pattern ( Solution ))
// Observable.cpp #include "Observable.h " Note . Should #include "Observer.h " additionally enclose #include // only here, not in Observable .h file everything in our custom namespace.. struct Observable::Observers : std::list < Observer *> {}; // define implementation class
Observable :: Observable () : observers_(new Observers) { } Observable ::~ Observable () { for (auto& x : *observers_) detach (x); delete observers_; } // Observer.cpp void Observable ::attach ( Observer * o) { #include "Observer.h " observers_ ->push_back (o); Observer :: Observer () {} } Observer ::~ Observer () {} void Observable ::detach ( Observer * o) { observers_ ->remove (o); o->detach (); // will break link to subject } void Observable ::notify () { for (auto& x : *observers_) x ->update (); } Background: updates are not enough
Games are inherently very much eventevent--drivendriven an explosion goes off the player is sighted by an enemy, or enters a trigger area a health pack is getting picked up
Games need to notify interested game objects when an event occurs arrange those objects to respond to significant events
Different kinds of game objects will respond in different ways to an event; for example, a Pong ball is governed/affected both by updates in its physical "state", determined by its velocity hitting another object (paddle/wall) and changing its velocity ((raterate and direction of change))change being missed by one of the players --andand hitting the back wall
24.2.2017 Juha Vihavainen / University of Helsinki 1313
Common game events [McShaffry, p. 325325--326]326]
1 1 General game events a game object has moved the event system is ready a collision has occurred the sound system is ready a character has changed states the network system is ready player character has changed a human view has been attached a new game object is created the game is paused a game object is destroyed the game is resumed player character is dead the game is about to be saved (presave) player death animation is over (game over) the game has been saved (postsave)
2 2 Map/mission events 4 Animation and sound effects a new level is about to be loaded (preload) an animation has begun a new level has finished loading (loaded) an animation has looped to beginning a character entered a trigger volume a timing signal from animation to sound a character exited a trigger volume an animation has ended the player has been teleported a new sound effect has started a sound effect has looped back 3 3 Game startup events a sound effect has completed the graphics system is ready a cinematic (video) has started the physics system is ready a cinematic has ended 24.2.2017 1414 The normal object interaction doesn't work
Consider calling a method of an object to handle an event void Explosion ::doDamage() { // this is pseudocode var damagedObjects = theGame.theGame.getObjectsgetObjects(getDamagesSphere ());
for each object in damagedObjects do Gregory, p.934 object.object.onExplosiononExplosion ((**this) }}
This kind of object interaction style does not scale up Here , game objects need to be inherited from a base class that defines onExplosion() -- even if not all objects respond to explosions Such virtual functions would be needed for all events in the game
Better to register for only interesting events and handle those ones
24.2.2017 Juha Vihavainen / University of Helsinki 1515
Registering interest in events
We don't want to unnecessarily send events / call event handlers
can maintain a list of interested listeners ( observers ) for each distinct type of event, and/or each "potential" receiver could keep data to show the particular events this receiver is interested in (say, as a bit array)..
The classic version is The Observer design pattern: define 11--toto--manymany dependency (Gamma et al., 1994), described earlier
Instead, we could use callbacks (function pointers), or Build up special event records to send around
24.2.2017 1616 Use of an event system
E.g., a subsystem (say, physics) cannot keep track of all other subsystems that need to know about moving objects (e.g., the game renderer) they can have different APIs and different parameter lists (messy)
An event system centralizes and uniforms all interactions supports modularization and separation of concerns enables central tracing and debugging of all activities
In a wellwell--designeddesigned game engine an event system is a part of the higher support layer each subsystem is responsible for subscribing to and handling game events as they pass through the system an event system manages all communications between the game logic and game views (e.g., display, network interactions)
24.2.2017 Juha Vihavainen / University of Helsinki 1717
Encapsulating an event in an object
An event ~ a command ~ a message event signaling is similar to sending a message or a command
An event may consist of two parts type : explosion , picking up a thing , player being spotted , etc. arguments : specifics about the event (where, how large, who/what)
struct Event { // often a base class EventKind kind; // meaning of this message EventArgs arguments ;;// linked list or array, giving . . . . . // . . arguments of various types };}; Gregory, p.935 Usually, different kinds of events can be derived from an base event class -- depending on the platform and its restrictions (memory, etc.)
24.2.2017 Juha Vihavainen / University of Helsinki 1818 Event handlers (one version) (Gregory, p. 940)
An event handler is some piece of code that handles a received event: virtual void SomeObject::onEvent (Event& event) { switch (event.kind) { // illustrative only case EventKind::Attack: respondToAttack (event.getInfo ()); break; case EventKind::HealthPack: addHealth (event.getInfo ()); break; . . . }} }} MMayay need to downcast (for "type recovery")
One single method vsvs . many handlers: OnAttack (), etc. ? For strings or hashed string IDs, use cascaded ifif //elseelse--ifif statements .. Other approach: function pointers ("(" delegates ") to virtual or nonnon-- virtual functions, mapped from event types (used, e.g. with old MFC ))
24.2.2017 Juha Vihavainen / University of Helsinki 1919
Notes about an implementation (McShaffry, 2009)
(McShaffry) uses a separate EventManager class ((--reallyreally necessary?) Again, smart pointers can automate garbage collection (mostly)
Note the "loosely typed" events: handlers need to inspect type info vsvs . Typed Message that uses clean typetype--specificspecific event handlers but static typing not always possible: e.g., msgs from network..
The implemention can use custom meta--objectsmeta objects ( EventType )) identify different event kinds for specific processing in a simple prototype system could well use a C++ enumeration enum Kind class { ObjMove, ObjCollision, PlayerDeath.. };
however, this strategy scales up badly in practice (recompilations) better to use unique hashed ID s or something similar ( GUID )) possibly could augment/integrate with the C++ builtbuilt--inin RTTI (..?)
24.2.2017 Juha Vihavainen / University of Helsinki 2020 Typed Message design pattern
In the Observer design pattern, the participants are still tightly coupled the subject ""knows"knows" its observers (any number of them) an observer maintains a ref to the (concrete) subject to get its state
John Vlissides, ""MulticastMulticast --ObserverObserver = Typed Message",Message ", 1997 in C++ Report , Nov/Dec 1997; also later described in the book by J. Vlissides: Pattern Hatching . AddisonAddison--Wesley,Wesley, 1998
A minimal type--safe type safe solution; can be used to implement other versions..
Described also in the master's thesis of M. Väänänen: Olioarkkitehtuurit peliohjelmistoissa (Univ. of Helsinki, Dept. of CS, 2008) the original version uses immediate dispatching (as usual for GUI) M.V. added a queue per handler; handleBufferedEvents () delivered messages that were earlier sent to a specific handler
24.2.2017 Juha Vihavainen / University of Helsinki 2121
Features of Typed Message
Actually minimizes coupling : senders don't know about handlers, and handlers don't know about senders handlers are associated with events TT, not with subjects/senders
A TT handler is derived from TypedMsgHandler <> a kind of mixin class (providing selected limited capabilities) no extra registration is required (enabled by default) a TT message is received by an operation " handle (T const& msg) "" can later remove a handler from the handler list (to disable it) and later insert the handler back into the handler list (to enable it)
A TT message is sent by a separate: " DeliverTypedMsg (myMsg) "" template
24.2.2017 Juha Vihavainen / University of Helsinki 2222 TypedMsgHandler < T > (one version)
template
24.2.2017 Juha Vihavainen / University of Helsinki 2323
Use of Typed Message To receive a message need only to derive a subclass: class GameEntity: public TypedMsgHandler <
24.2.2017 Juha Vihavainen / University of Helsinki 2424 Implementing delivery of typed messages
A list of message handlers is kept perper a message type
To send a message, get its handlers and call their handle function template
Note that handlers register and unregister themselves automatically but still, handler objects need to be destroyed in proper manner e.g., first mark then as dead, then clear them out later
24.2.2017 Juha Vihavainen / University of Helsinki 2525
Event sending
In GUI system, events are (usually) immediately processed
sometimes events cause sending new events to other objects/parts immediate event sending can cause deep call chains object AA sends to object BB that sends to object CC (that sends to AA..)..) in the worst case, immediate sending may cause an eternal loop
Often, event handlers need to be rere--entrantentrant (= no harmful extra global updates/sideupdates/side--effects)effects) --if they can be called multiple times..
In games, events can be handled
immediately, or later during the same frame (animation blending), during the next frame (game loop cycle), or even at some arbitrary future time, say, after n seconds (or frames)
24.2.2017 Juha Vihavainen / University of Helsinki 2626 Event queuing Queuing helps to manage when events are handled Can post events into the future: each Event would have a delivery time to indicate the dispatch time; consider how to process such events void EventQueue::dispatchTimedEvents (float currTime) { EventPtr pEvent = peekNextEvent (); while (pEvent && pEvent -->>deliveryTime <=<= currTime) { removeNextEvent (); // got one to be triggered pEvent -->>dispatch (); // DeliverTypedMsg (pEvent( pEvent)) pEvent = peekNextEvent (); }} (GEA, p. 945 ) }} also, could prioritize events to control the order during a frame drawbacks: complexity, and need for dyn. mem. allocations
19.2.2013 Juha Vihavainen / University of Helsinki 2727
Extra services TThehe queued events need to bebe dynamically allocated : should ensurethis
EEventvent kindscould bebe validated bybyrequiringrequiringthat event types must bebe registered before useuse=> could guard against mismatches and typos
IIf event processing loop takes tootoo much time ((given a system --dependent time limit), limit ), cancan trytryto push spilled--over spilled overevents onto the other queue but what ififqueues just keep growing and staying full..? perhaps need some optimization and/or down--grade down grade effect ss
(McShaffry) proposesa specialevent kind ("("AnyAny")") such handler could convenientlytrace allallevents and activities
AAhandlerhandlercould somehow indicate that the message hashasbeen processed (to implement Chain of Command : input is not propagated anyany moremore)) TTimedimed events (kept in a separate queue) resemble scheduling coroutines
24.2.2017 Juha Vihavainen / University of Helsinki 2828 Summary: distinguishing events from tasks
AA task is an activitythat goes on overmultiple frames e.ge.g.,., animationsand soundeffects, effects , control of gameflow, and so on AAnn event is a notificationof a relevantchange in the gameworld that requires processing an AI character is spawn,spawn , a gametrigger fires, fires , etc. often causes changes in onon--goinggoingactivities ((taskstasks)) Further study : how to receive with any functions / to use closures ??
(Gregory) implies that events can be send to and are processed by almost any game entities, to specify all possible interactions.. In constrast, (McShaffry) advises that we don't want giant lists of listeners to go through for each event events should be mostly used between subsystems (managers) game objects are in turn handled by these subsystems 24.2.2017 Juha Vihavainen / University of Helsinki 2929
DataData--drivendriven event systems (Gregory, p. 950950--954)954) Event types may need to be defined externally, e.g., in a textual scripting language (or similar) to generate, send, and handle events need some symbolic form to name them uniformly the whole game logic could then be expressed by scripting only Event objects may need to be saved and loaded must support serialization of events ( object IO ) in online multiplayer games , need to send them out there anyway Note that we are kind of wiring up a game with events and handlers At extreme, graphical scripting systems provide components (operations/blocks) with "ports" to be manually connected (wired) can be configured and combined to express new interactions events are used to trigger (start) sequences of actions Unreal Engine and Blender Game Engine provide such facilities 24.2.2017 Juha Vihavainen / University of Helsinki 3030 How to define types ( tags ) for events
A simple and efficient --butbut does not scale well for large games enum class EventKind { (Gregory) suggests using hashed LevelStarted, strings ids as global identifiers (names) EnemySpawned, PlayerSpotted, In C++, we have built-in RTTI BulletHit . . (Run-Time Type Info) but its };}; implementation is partly undefined.. Drawbacks of enum class : info about all events kinds is centralized and hard --coded => changes cause systemsystem--widewide recompilations event codes are implementationimplementation--dependentdependent ordered integers => does not support datadata--drivendriven event definitions (in scripts)
24.2.2017 Juha Vihavainen / University3131 of Helsinki
EEncodingncoding event types via string name ss
A very freefree--formform and easy to use: just name a new event type to add it
Some problems: increased memory requirements (need char arrays/buffers) highhigh--costcost of comparing actual strings (takes linear time) can have name conflicts (e.g., when introducing new event types) possibility for aliases , and mismatches or typographical errors
Some remedies: use a central repository to register ("declare") event type names can store additional info: documentation, use of arguments.. useuse hashed string IDs : generate unique integer IDs for strings faster comparison, uses less memory (to pass around) when needed can (easily) recover the original name
24.2.2017 Juha Vihavainen / University of Helsinki 3232 Unique ids for game entities [Greg, Ch.5.4, 277277--279]279]
Say, in PacMan we might encounter game entities named "pacman", "blinky", "pinky", "inky", and "clyde"
Also, the assets from which our game objects are constructed -- meshes, materials, textures, audio clips, animations, etc. -- need unique identifiers of their own
We want to hash strings into string ids (~ unique integer codes) Interning a string means hashing it and adding it to a global string table; the original string can be recovered from the hash code later
Another idea used by the Unreal Engine is to wrap the string id and a pointer to the corresponding CC--stylestyle char array in a tiny class in the Unreal Engine , a "name table" of FNames maps unique strings to indices; provides a lightweight system for strings, where a given string is stored once only in a table ---- eevenven if reused
24.2.2017 Juha Vihavainen / University of Helsinki 3333
using SId = unsigned32; // modified from (Gregory, 278) using StringIdTable = std::unordered_map
SId internString (char const ** str) { Must ensure uniqueness.. SId sid = hashCrc32 (str); // custom hash creates unique ids if (stringIdTable.find (sid) == stringIdTable.end ()) // not found stringIdTable [sid] = std::strdup (str);
return sid; See also: Gregory, Section 5.4.3.2 } } . . . Some implementation ideas, p.277 static SId sidFoo = internString ("foo"); // to use SIdSId as a name static SId sidBar = internString ("bar"); . . . if (sid == sidFoo) . . . // then handle the ""foofoo "" case ..
24.2.2017 Juha Vihavainen / University of Helsinki 3434