Hooks Ahoy!
Full post at
https://anapest.digital/hooks-ahoy-event-triggered-effects/I wanted to have a system that handled interesting, event-based effects (like on-hit or on-block procs). One COULD hardcode things like critical hits and ripostes, but I wanted to try a much more flexible and generalized solution.
After some thought, I modelled these after a piece of software that I know pretty well: WordPress. In WordPress, “hooks” are called at key moments during core, theme and plugin functionality. Developers are able to hook functions — any functions — to these, and they’ll be called. This allows us to stick code into the middle of things without having to break plugins. It’s also one of the reasons that WordPress is a dream to extend.
I’m not sure if this is typical — or good practice — for game developers. The usual disclaimer applies: I do not know what I’m doing. Never follow along.
…WHEN HARD CODING DOESN’T MAKE YOU HARD ANYMORE.
Sorry. Anyways:
Pseudo-code for a simple but unflexible system might look something like this:
AttackMethod(Unit target, int damage) {
if (Critical()) {
damage = damage * 2;
}
if(SomeOtherEffect()){
//do something
}
… etc.
target.Attacked(this, damage);
}
Attacked(Unit attacker, int damage) {
if(Block()){
damage = damage * 0.5f;
}
if(Thorns()){
attacker.TakeDamage(2);
}
TakeDamage(damage);
etc...
}
This doesn’t seem to arduous with only two or three possible on-hit effects. Actually, it’s probably perfect for a small game. Which is... I guess... what I'm making. But I'm ambitious (and lazy). What if we want to make twenty or thirty effects? Or three hundred? And what if we don’t want every single unit to constantly be checking if they can do every possible thing? And what if I don’t want to be cracking into my unit code whenever I want to do this?
INTRODUCING HOOKS FOR EVENT-TRIGGERED ACTIONS!
My solution was to create a master list of functions that can be assigned at specific points, with dynamic chances to be called. This involves getting our hands messy with delegates, which are a lot of fun (I think). Actually, I had to figure out how to implement C# delegates for this task, so I hope I’m not way off base.
The first thing to create was a series of hooks. I set these up as an enum with values like HIT, BLOCK, DAMAGED etc. These would be used to identify the points at which an effect would have a chance to trigger.
Next, I created a data structure for the effects themselves. These would be “EffectChances,” and they would take in a name(“Critical Hit”), an action (CriticalHit()), a chance (out of 1) to trigger (0.05), a power level, an optional hook for beneficiaries (“all enemies”, “all friends”), and a hook (Hit, Take Damage, Block etc.)
public EffectChance(string _name, int _chance, Action<IEffectHookable, Unit, int> _action, int _power, EffectBeneficiary _beneficiary, EffectHook _hook) {
effect_name = _name;
chance = _chance;
action = _action;
power = _power;
beneficiary = _beneficiary;
hook = _hook;
}
Now that we had something to hook and an enum of hooks to use, we needed something to hook it to. I wrote an interface called “IHookable” which is currently only implemented in Units. This contains the meat of the hook script: AddEffect(), ApplyEffects() and RemoveEffect(). These manage a Dictionary<EffectHook, EffectChance[]> (that is to say, each dictionary key is a hook (HIT, DAMAGED etc.), and the value is an array of effect chances (10% chance to crit, 5% chance to poison etc.).
Here’s how they look in the Unit.cs script:
public EffectChance AddEffect(EffectHook _hook, EffectChance _effect) {
if (!hooked_effects.ContainsKey(_hook)) {
hooked_effects[_hook] = new List();
}
hooked_effects[_hook].Add(_effect);
return _effect;
}
public void ApplyEffects(EffectHook _hook, IEffectHookable _subject, Unit _target = null) {
if (hooked_effects.ContainsKey(_hook)) {
foreach (EffectChance effect in hooked_effects[_hook]) {
int roll = Random.Range(0, 100);
if (roll <= effect.chance) {
effect.action.Invoke(this, _target, effect.power);
}
}
}
}
public void RemoveEffect(EffectHook _hook, EffectChance _effect) {
hooked_effects[_hook].Remove(_effect);
}
Finally, the actual effect actions themselves are contained in a script called effects list, which keeps a Dictionary<string, Action> of actions. The actions are delegates that take in an IEffectHookable (unit), a target unit, and an integer for power. Here are a few examples:
This effect drains mana from a target and gives it to the protagonist.
public Action<IEffectHookable, Unit, int> DrainMana = (IEffectHookable subj, Unit target, int power) => {
subj.GetUnit().CalculateDamage(power);
FindObjectOfType().GetProtagonist().ChangeMPAmount(power);
};
This action triggers when a unit is hit and gives them a small speed buff.
public Action<IEffectHookable, Unit, int> Fury = (IEffectHookable subj, Unit target, int power) => {
subj.GetUnit().AddCondition(new CndSpurOn(power, 5));
};
It can get more complicated, too. Intercept is a temporary condition bestowed on allied units that allows your hero to take damage for them.
public Action<IEffectHookable, Unit, int> Intercept = (IEffectHookable subj, Unit target, int power) => {
int total = subj.GetUnit().damage_to_be_resolved;
//Find INTERCEPT condition on unit
Unit u = subj.GetUnit();
CndIntercept cnd = u.FindCondition("Intercept") as CndIntercept;
//Find INTERCEPT caster from condition
Unit caster = cnd.GetCaster();
//Split damage
float percent = power / 100f;
int damage_to_caster = Mathf.FloorToInt(total * percent);
int damage_to_unit = total - damage_to_caster;
//Damage units:
u.damage_to_be_resolved = damage_to_unit;
caster.CalculateDamage(damage_to_caster, "Pure");
};
Fun fact: Since the last one was hooked to the damage hook, it crashed Unity (since it kept trying to intercept its own damage) until I made it check for “Pure” damage to escape from the effect hook! It also made me create a “damage_to_be_resolved” value for units, and a way to track back a condition’s caster (in order to apply the intercepted damage to her!).
Now, in my Unit.cs’s attack method, I can include:
unit_target.ApplyEffects(EffectHook.HIT, this, unit_target);
…and every single effect “hooked” to the “HIT” hook will have a chance to be called.
It took a while to figure this all out, but it seems to work quite well … as long as I remember to use the feature.
(That’s another reason I’m devlogging… so I don’t forget how to use my own code).
WAS IT WORTH IT?
Yes! I definitely think of this system as a bit of up front work for a lot of down-the-road time savings. It’s also one of the more complicated systems that I’ve authored without much help, so I hope that it’s not embarrassingly useless (or utterly obvious) for other people.
This hook system means that, in order to make a unit prefab that has unique special effects, I can just specify a hook, an effect, a chance and a power, rather than have to muck around with specific scripts for units. It’s a powerful way to create a lot of strategic diversity among different units. And I love power.