When listeners order is important, wich way can I go?

Hey friends, looking for some talk about an often problem that I need to face. I try to code following some events based workflow, but sometimes the order of how things will happen matter, for things read listeners, and I can’t find a “good” way to go*.* Only to clarify I’ll insert an example, say for example that I had an event called PrepareStage that will be called at the start of each stage and 3 listeners ShowHUD, SpawnEnemies, and FadeOut, in this scenario things can’t just happen in any order, because if fadeOut came first maybe things can don’t show properly for some frames. So for sake of mind, I need to make sure that ShowHud and SpawnEnemies are finished before we fade. So I’ll try to say some ideas that come into my mind to solve this.

1- (naive one) Give some frames before fadeOut to give time for other methods to run

2- Create a custom class to control execution, this works fine, but only if the methods are called by this function, and this approach creates coupled code between this class and listeners, not mention that I need to create some code if want to add a new listener. If this class only call other events we go back to step 1

3- (worst IMO) Create some kind of queue making sure that every listener calls the next event. This makes it hard to rearrange and create new events

This is what I got, can someone suggest some another possibility, or say how do you face this kind of problem?

Thanks for help guys, N

IMO, the best approach to temporal coupling is to be explicit. In this case, just add more events.

public event EventHandler PrepareStage;
public event EventHandler StageReady;
1 Like

I see two good solutions:

  1. Decentralized (scalable in terms of not having to think of the whole picture but not the most efficient)
private async void Start() {
            while (Action trigger conditions aren't right)
            {
                Debug.Log("Awaiting for right conditions");
                await Task.Delay(Constants.pollingInterval);
            }
        Do what I am supposed to do
}
  1. Domino-like
    Add abstraction (C# properties or wrapper methods) on every important event in your cycle and check if the conditions are right. If yes, do the thing.

I think the second approach is basically like using Events.

1 Like

This pattern also comes in super-handy for additive scene loading designs: I fire all the scenes to load async but for example the player requires there to be a valid spawnpoint, and that won’t be true until the content scene finishes loading, so meanwhile the player just keeps checking for spawnpoints but doing nothing else until it has one.

I think that I don’t get it, how you’re supposed to know when all PrepareStage listeners finished their execution to run StageReady?

Sounds nice, but how do u do these “action triggers” verify? I mean, do u suggest the creation of some kind of global states to verify if I can fade out already? Or create some communications between the fade-out scripts and spawn enemies and show hud script?

Do you mean creating some kind of layer that will control the flow, like a handle manager class?

Thanks for share info buddies \o/

Events are synchronous, just like function calls:

PrepareStage.Invoke();

Once that statement is done, 100% of the listeners in there have been called, you know you can now call:

StageReady.Invoke();

I see, I think that I’ve expressed myself wrong, I supposing that some listeners can call coroutines

The Observer pattern (events/listeners) expects that you have the explicit intent that each of the components do not have a dependency on each other. the listeners should not have to worry about each other, nor should it ever be expected that listeners can “race-condition” each other.

If these are happening, its far better to just bite the bullet and have an explicit manager/controller directing these flows of logic. Instead of event have the controller explicitly reference the other scripts (possibly via an interface) and run the coroutines on itself.

public interface IRoutineEnumerable
{
    IEnumerator GetRoutine();
}

public class ExampleController : MonoBehaviour
{
    // the collection of coroutines to run in sequence, as an array of monobehaviours so as to be serializable
    public MonoBehaviour[] routineBehaviours;
    private Coroutine routine;

    public void RunSequence()
    {
        if(routine != null)
            StopCoroutine(routine);

        routine = StartCoroutine(RoutineSequence());
    }

    private IEnumerator RoutineSequence()
    {
        for(int i = 0; i< routineBehaviours.Length; i++)
        {
            IRoutineEnumerable handler = routineBehaviours[i] as IRoutineEnumerable;

            if(handler != null)
                yield return handler.GetRoutine();
        }

        routine = null;
    }
}

public class ExampleRoutine: IRoutineEnumerable
{
    IEnumerator IRoutineEnumerable.GetRoutine()
    {
        return MyRoutine(2);
    }

    private IEnumerator MyRoutine(float duration)
    {
        var wait = new WaitForSeconds(duration);

        while (true)
        {
            Debug.Log($"Waiting at least {duration} seconds");
            yield return wait;
        }
    }
}
1 Like

Yeah, this ^

If you have dependencies, express them explicitly because if you do so with some generic ordering mechanism, you run the risk of not understanding it in the future since you cannot put meaningful documentation inside of a generic mechanism.

// Warning: always be sure the FooBar is initialized before the DingBat!
InitializeTheFooBar();
InitializeTheDingBat();
1 Like

Good solution for me, we create some dependencies between this flow manager and listeners, but as you say “bite the bullet”. Thanks for help buddies :stuck_out_tongue_winking_eye:

1 Like

I use something like option 2 where I don’t care about coupling and instead just write the code I want to write:

IEnumerator StageSequence(Action onCompleted)
{
    yield return StartCoroutine(enemySpawner.SpawnEnemies(onCompleted: () => Debug.Log("Enemies finished spawning")));
    yield return StartCoroutine(guiManager.ShowHUD());
    yield return StartCoroutine(fader.FadeToBlack());
    onCompleted?.Invoke();
}

It makes perfect sense when reading it. It will be run inside an FSM state, and will trigger a transition to another state when onCompleted is called.

If you don’t like the coupling, you can make it some kind of sequencer that you add actions/commands to, but they will always need access to the things they require to function. Then it just loops through the actions and calls Execute on them and waits for completion. Each command can be its own class, like so:

public class AnimatorTrigger : SequenceAction // MonoBehaviour version setup in hierarchy
{
   public Animator Animator;

   public override void OnBegin()
   {
       base.OnBegin();
       Animator.SetTrigger(TriggerName);
       Status = ActionUpdateStatus.Success;
   }

   public override void OnUpdate() { ... }

   public override void OnComplete()  { ... }
}