In module 1, we implemented some event-driven mechanics as per the game brief. Mainly, these were a “Switch” type object that you can dash past and it activates some sort of remote object and a “Pressure plate” which activates a remote object. These could trigger things like gates to open and remote platforms to start moving.
Obviously, one needs to know about the other. In our module 1 framework, I connected them directly by letting the trigger source know what to activate on creation via a generic “trigger target” interface. Going into module 2, we needed to link up the objects using the OGMO editor, and drag-dropping one object into a property of another one (like you can in Unity) isn’t that easy. Instead, I figured that the best way to achieve this linkage is by using an event system (which we had assigned to us as a homework task). And so I made a quick rough first pass at the homework, then took and plugged that into our game.
I chose to make it a member variable of CGCGameLayerPlatformer, which is the main layer of any given level. Since the layer is a singleton, you can fetch the event system from it and use it. This also means that the event system gets destroyed and re-created at each level. However, thinking about it, it’s more or less what we want, since the event-driven objects in each level might be different and use different messages, so we kind of want to clean up anyway.
The Event system registers messages (and errors if some object wants to use a message that’s not registered – this is a way of explicitly telling the client code that they might not be doing what they want and it’s also good defence against things like typos in event names). Objects can then send the registered message. Other object can register to listen to that message. I used inheritance to make this work, and created an interface which a listener needs to implement to be able to listen to events. This is mainly to abstract the object type from the event system, as it does not need to know that.
However, only after implementing this did I realise that there was potentially a better solution to the problem. Instead of registering listener objects, maybe I could have registered callback functions. That way, the listening objects don’t need to implement the interface and it’s slightly less prep-work for an object to start listening to events. Furthermore, if I wanted the game layer to listen to some types of events, I would need it to implement the interface. But the main game layer class is massive and over-burdened with responsibilities as it is, so implementing another interface just seems like adding even more responsibilities on its shoulders. Therefore, if I did it again, I would have went with callbacks, as the only thing that misses is some of the cleanness and structured-ness that you get when implementing OOP code.
Knowing about the basic premise of the event system, we can have a closer look at it now:
The implementation detail that springs to mind is that it uses two STL containers: a map and a vector. It uses a map because I originally had a hashmap implementation in my homework assignment, only to find that it’s a C++11 feature, so I couldn’t take advantage of the blazing fast access times of hashmaps. So I used the closest thing, which is the std::map, a Red-Black tree structure. I also used vectors to be able to manage the entries more easily. However, I made sure to protect the code from run-time stretching of the data structure by reserving size at the start and inserting assertions to make sure that it never goes beyond its designated boundary:
So registering the events creates these vectors for each event and stores pointers to the interested listeners. This is what you would call an initialisation phase – all events should be registered before the game starts running. I probably could have enforced this even more by setting a Boolean flag at the end of the layer initialisation code which would bar any new registrations of events, as this would inform other programmers to do the initialisation before that (if they don’t realise it to begin with).
Since the requirements for our events were incredibly basic, we decided to not enable the sending of additional “data” besides the actual event name. After all, the only thing we needed is to connect two objects for the most part. We could have defined an event name system such as “GATE_OPEN” and then included additional data to specify the gate index. Instead, we just relied on the event name. This choice had two implications, both of which are good for a small game such as ours, I think:
- This means that objects only get the messages they are actually interested in. With the previous example of message name + data to identify the gate, all gates would need to receive it and sift through to see if it’s their turn to open/close. Using specific messages saves that little bit of processing effort (but the event system uses more memory to store the larger number of event names).
- The level designer was really pleased with the way to connect objects. All he needed to do, is enter the same string for the gate and the switch, and that’s him done. This is as simple a system as you can get, more or less.
We managed to use the event system not only in OGMO, but also for things like the level exit opening after the player collects the first piece of mail in the game, which is a nice added bonus.
If I did it again
Going forward and doing it again in the PS4 project, I think as much forethought as possible would be useful to decide whether we should use such a simple system again, or go for more complex classified message types and extra data.
The choice really depends on what you want to send – if there is any state data of the sender object that affects the outcome of the event, then you surely want to send that state data through, the question is whether we will have that kind of usage scenario. Furthermore, I think it’s better encapsulation and less coupling if the sender figures out precisely when to send the message, rather than the receiver getting the sender’s state data and figuring out what to do.
Rather, I think it’s useful to send additional data when the receiver needs it to do its job, rather than figuring out when to do its job. A good example of this is anything that needs to render in different colours based on the event (VFX – blue flash on character when powered up, white when invulnerable, red when hurt?), or, taking the example of the Win32 API (which uses events for all windowing), when entry in a text box is sent over, when the message indicates a radio button state, etc. We just need to consider if we are going to have things like that, and if it’s practical to expose extra complexity to the level editor. Then again, expanding the event system to include additional data does not seem to be that hard either, so maybe the choice does not matter too early on.
Finally, if I were to do the project again, I would probably not use a map structure, as we found that it actually fragments the memory (events are getting registered all over the place by all sorts of objects). This caused problems to us when loading in dynamic background assets, as they can get quite large. However, we tackled the issue by loading the biggest assets before any of the event system stuff happens, and that solved it. However, even if it’s slightly slower to index an array and check each and every event name, for the amount of events we have, I think it would be totally worth doing it, as memory fragmentation is something we definitely want to avoid.
GCEventListener.h – https://drive.google.com/open?id=0B_BnvnFZLH7mTVRLRWl6ZkhiTm8
GCEventRegistry.h – https://drive.google.com/open?id=0B_BnvnFZLH7mbGppYU1Iblp2ZVU
GCEventRegistry.cpp – https://drive.google.com/open?id=0B_BnvnFZLH7mbnRCRC1nSlB5b0E