#3: Damage Mutations! & the Publish-Subscribe Pattern

Noah

1. Describing the problem of the damage lifecycle

In the chaos of battle, the arc of a sword-slash slices against its target. The warrior taking the hit is wearing armor, but will it do anything to mitigate the plunging blade’s terrible impact?

The scene I just described is a dramatic retelling of a common exchange seen in action games: an attack connects, but the target might have some protection from the resulting damage.

If the only variables we are accounting for are the sword’s damage value and the defender’s armor rating, a simple calculation might tell us whether the blow deals full damage or if that damage is reduced.

However, like a raging battlefield of soldiers, the code being executed at the height of the action might be a confusing, jumbled mess!

With additional variables in the mix such as custom equipment, troop-specific attributes, status effect debuffs, and so on, the calculation for damage may happen across many different component scripts the instant an attack lands.

2. Complex value handling and clean code

In a game like Project Medstrat, potentially dozens of factors might mutate the damage value of an attack, and manually checking all components involved in the process would tightly couple these scripts in a brittle, inflexible way – spaghetti code for days!

My solution, one that I’ve honed over years of developing role-playing action games such as Turk’s Crawling Dungeon, was to make use of the publish-subscribe pattern (an evolution of the observer pattern) to build a loosely-coupled network of event streams and their subscribers.

Events, in this case, serve as buffers for data values – like damage from an attack – which can then be passed to subscribers who modify the data without caring where the value is being sent from.

Another game that uses a publish-subscribe system that’s similar to this is Caves of Qud, a game made with the Unity game engine. I know this because since it’s made with Unity, I was able to use a decompiler to hack open the game’s code and have a peek (sorry, Freehold Games).

It makes sense that a game as complex and immersive as Caves of Qud would require an architecture that handles damage values and other stat mutations in a loosely-coupled way – hand-coding all of these interactions would be an unimaginably painstaking process otherwise.

3. An overview of a solution in Unity

In the hypothetical scenario we’re using as an example, a damage event (in code, a DamageEvent object) is sent to a “defender” game object.

We then retrieve all the components of the defender object that inherit an interface (for instance, one called IEventHandler<DamageEvent>) which flags the component as a handler for the damage event, and store these matching components in a list.

The DamageEvent buffer is then passed to each handler component in the list, which modify the damage value accordingly. Once all the handling is done, we can then extract the final damage value from our DamageEvent and apply that our defender (ouch!).

4. Publishing damage events in Medstrat, in detail

While architecting Project Medstrat’s (rapidly-swelling) codebase, the issue of applying damage values without terrible fuss was foremost in my mind while implementing components for troop equipment, invincibility, and so forth.

If you’ll indulge me, let’s have a brief look at the way I’ve handled the problem.

For a component to subscribe to events, it must inherit IWorldEventHandler<E>, a generic interface with a single method: HandleEvent, which takes an event object of type E.

In the abstract base class WorldComponent, we have the SendEvent method, which takes an event of type E and sends it to any IWorldEventHandler components found in its cache (it isn’t important to know how the cache is populated for the purpose of this blog post).

Our combat scenario is exemplified in the AutoAttack script, which sends a few events to the attacker and defender objects over the course of the attack lifecycle. Of interest to us is the BeforeDamageEvent, which is sent before a damage value is applied to the stats of the defender (reducing their health).

TroopEquipment is a WorldComponent that also inherits IWorldEventHandler<BeforeDamageEvent>, which means that before the defender takes damage, the equipment they’re wearing can modify the damage based on the total equipment armor value.

<==