Event - Action system

I would like to get some feedback or alternative solution to following problem.
There are various events in game, for example: DamageEvent, LocationEnterEvent, ItemSpawnEvent.
I would like to create some “actions” to react to those events, for instance:

  1. Give status effect to damaged unit
  2. Modify amount of spawned items
  3. Increase stats of player when enemy dies

I want to make those things to be pluggable inside inspector, not constructed entirely via code.
The problem is that event data must be transformed to fit those actions - simple example:
There is action that gives status effect to target character, this could be method to call:

public void ApplyStatusEffect(Character target);

In my case DamageEvent provides info about attacker and damage target, so one of those can be used to feed that method above, however ItemSpawnEvent does not provide that data, or I might need to feed it with custom reference.

My current implementation:
There are EventListener components (for instance DamageEventListener), they subscribe to given event to call various instructions.

public class DamageEventListener
{
    [SerializeReference]
    public IDamageEventReceiver[] instructions;

    // [...]

    public void OnDamage(DamageEvent damageEvent)
    {
        for (...)
        {
            instructions[i].HandleDamageEvent(damageEvent);
        }
    }
}

Instructions (processors) are serializable classes using interfaces to make them available to call and serialize.

public interface IDamageEventReceiver
{
    public void HandleDamageEvent(DamageEvent damageEvent);
}

[System.Serializable]
public class DamageEventToCharacter : IDamageEventReceiver
{
    public Action action; // This must implement certain interface taking Character as param

    public void HandleDamageEvent(DamageEvent damageEvent)
    {
        action.NameOfMethod(damageEvent.target); // This method would depend on interface
    }
}

Advantage of this system is that when something implements given interfaces I can plug anything I want, no GC, simple, modular, fast. The downside is that first I need to check/cast action to given interface (warning in inspector is needed too) and things get complicated if there are made variants of the same function. I would need to code DamageEventToCharacter, DeathEventToCharacter, DamageEventToFloat, DamageEventToSomeFlags, DeathEventToFloat, SpawnEventToFloat, SpawnEventToCharacter and all other variants for various interfaces/events.

Second downside is that Action might need to listen two events with the same parameters, for example changing stats and status effect action both have got the same signature with target Character as parameter. In this case it would be fair solution if I would need to code that thing with separate script, but if I wanted to make that work with interfaces (system above), then I would need different interfaces for each action (at this point I could just reference concrete classes…) and this make everything even worse (more variants).

I would be grateful for any tips.

Good to see SerializeReference starting to get more attention!

One solution that first comes to mind is to have a general ‘context’ object that you pass through to every action. Like what I suggested here: Help with building block ability system - #15 by spiney199

You can build it in such a way that data, targets, etc, can all be composed through it. Whatever is sending the event can build up this data before actually firing the event, with individual actions looking for specific data and doing their thing if present, and either do nothing or log a warning if required data isn’t present.

I guess the question is, do you always want all the receivers to be doing something? Or is their execution conditional or perhaps optional?

Another thought is something akin to UI Toolkit’s event system. It’s a bit complicated code-wise, but you can plug through the C# source code to get an idea of how it works. I used this in a current project for dispatching events for what happens in a procedurally generated tile-based world; and has so far proven to be the correct choice. This is entirely code based hooking-in though, but I do imagine that it should be possible to make it work via the inspector as well.

Do you mean InputActionAsset or something else? Not sure what you mean by UI Toolkit’s event system.

You mean something like Event with all possible properties? I had this in my head, this is quite acceptable solution, but well, it’s set in stone. For one project it might be good idea, but if possible I would go with flexible system.

I didn’t mention it, but actually there is EventCondition system, it works with SerializeReference too, but because there is limited amount of conditions per event or some of them can implement several interfaces at once, it is way easier to code. Most of them are one liners anyway.
In any case this is not big problem, it can be checked inside listener or instruction, or even action.

UI Toolkit, as in Unity’s newer UI system. It has a callback system where you can register events with strongly typed parameters: Unity - Manual: Handle event callbacks and value changes

I found using a similar system useful in my context, as the event parameters are pooled, so even if you’re firing them off every frame you’re avoiding GC still.

Which is why I mentioned ‘composing’ your data into it. Scroll further up in that thread and I have a simple example of doing just that. If we’re composing our actions, we might as well also compose our data!

Ok, I see what system you meant, but I am not sure how this can help. I mean even in code you still need to dig/“transform” data by doing for example instance.property. I also avoid GC by pooling event params.

Yeah, that would reduce number of variants, action could take context and grab what is needed, but there are two problems, first it does not solve problem with two arguments of the same type, as dictionary is based on types I can’t put two characters there. Second it’s easier to construct something invalid because there is no filtering for actions - I don’t know if action is compatible with provided context until called.

Also I wanted actions to be not dependent on events and abilities if possible, now it assumes there is function that takes some context. It’s not the most important thing, but I wonder if those things can be overcome.