My favorite part of the classic Roguelike gameplay loop is finding emergent behaviors that arise in the interactions between different gameplay systems. The challenges posed in the next area always seem insurmountable, until the player has the "aha" moment of learning to harness the new powers offered by the fruit of their latest effort. This is where the joy is in these types of games, for the author at least. In order to create a satisfying number of deep interactions between sources of character power, I need a scalable way to have one system share information about what it's doing with another system. The communication itself needs to be completely agnostic of the inner workings of both communicators, instead defining a message format that both of the other systems will agree on to pass data from one to the other.
I want this system to be simple to implement in blueprints, and not require careful shepherding of event broadcast code from individual spell or item blueprints. Core systems that use the data provided in items and spell definitions should handle the messaging, so I don't have to remember to send out "Spell Finished Casting" events at the end of every single spell Blueprint. For Example, this is how PreCast and PostCast abilities are triggered by the Spellcaster component:
I also need to have the capability to "listen" to separate streams of messages. Two quick illustrative examples taken from spell upgrades:
At the beginning of the month, these requirements existed only in scattered and nebulous thoughts, and it took me a bit of hacking away and iterating on the design to really discover what I wanted from this system. Coding at the beginning of the month was slow because of this, and I walked away from it with a deeper appreciation of how difficult it actually is to turn a high-level design objective like "The game has upgradable spells" into actionable technical requirements.
Now that we know exactly what this system needs to be, I'll go into the trial and error involved in getting there over the past month.
My first attempt at event handling involved creating an event handler interface, that every tier of the gameplay hierarchy would implement to pass messages up and down the ownership hierarchy. This approach had several severe issues:
My second attempt was an overreaction to the pain of having to maintain a tedious chain of event handlers. I tried collapsing all of the events for a pawn into a single message broadcaster on the pawn itself. This was far simpler to implement, and far, far simpler to debug. Once I had the basics working for this approach, I took a step back to determine if this was a viable path forward for the entirety of my design space. After poring over my list of planned spell upgrades, I realized that this would become awkward to work with as soon as I start adding events that only trigger off of damage from certain sources. I would have to add some sort of event filtering predicate to each triggered effect, and I couldn't think of a good way that didn't make the designer experience involve manually specifying these predicates at every turn. I wanted to bake the event triggering behavior into C++ code, and not require extra blueprint work to create the desired triggering behavior on every asset.
Third time's the charm, and the culmination of this month's work is a system that takes the benefits of both extremes (everything is an event handler | only one event handler), while having the drawbacks of neither, while making ease of use a first-class concern. To address the problems from the first attempt, I implemented all of this functionality in a new class named GameplayEventMessageBroadcaster. Pawns are given an instance of this object, and in addition each Spell and Attack object get an instance. When a spell or attack isn't active (i.e. not equipped), its MessageBroadcaster is deregistered and it doesn't receive or forward new gameplay events.
When this object broadcasts a message, it automatically repeats to the parent Message Broadcaster, if it exists. This convention allows me to avoid a whole class of infinite message loop bugs, which was a constant concern with the first approach. TODO: put this in a collapsible code snippet control Message Broadcaster Header file: https://gitlab.com/kozmary/hexhexhex/-/blob/master/Source/HexHexHex/Public/Events/GameplayEventMessageBroadcaster.h?ref_type=heads
This topology is much simpler to maintain than the "everything is an event handler" approach, while allowing for objects to subscribe to different message streams by specifying which message handler they'd like to listen to. The default forwarding behavior means that the Pawn's Message Broadcaster will be a superset of all gameplay events, while the "child" Message Broadcaster nodes will only see events that that system generated.
This is all well and good in theory, but how do designers actually subscribe to all of these abstract event streams? For that, I provide a suite of TriggeredEffect classes, one for each different category of events that can be subscribed to. The designer simply needs to pick one of the triggers below to specify when their event should trigger:
They must then pick a triggered effect that corresponds to that type of trigger:
The events implemented at the time of writing are all pointed at the Spell Object's Message Broadcaster. For events that want to listen to the Pawn's event center instead, it's a simple matter of adding a new trigger class for that event scope. Having these triggering events and their related effects separated by type allows me to make it incredibly easy for designers to use this system, since they can only choose from valid options when setting up data assets for spell upgrades, and the entire set of valid options is presented to them in a searchable dropdown box in the editor.