How to handle coroutine listeners in an EventManager

Hello,

I’ve watched this unity live session about how to write a small but powerful C# event manager and I found it very interesting because this event manager works in the same way of Node.js event system :smile: That said, I have a problem. I would like to be able to add a coroutine listener, basically do this:

EventManager.StartListening("AString", ACoroutine);

I know that I can write a wrapper in my class to pass a normal function instead of a coroutine, but this sounds like a dirty solution for me. And I can’t use anonimomus functions in C#, so… :frowning: But I feel like there is an easy way to wrap my function inside thr Event Manager, doesn’t it?

So you want that this code from the supplied link:

using UnityEngine;
using UnityEngine.Events;
using System.Collections;

public class SelfDestruct : MonoBehaviour {

    public GameObject explosion;

    private float shake = 0.2f;
    private AudioSource audioSource;
      
    void Awake ()
    {
        audioSource = GetComponent <AudioSource>();
    }
  
    void OnEnable ()
    {
        EventManager.StartListening ("Destroy", Destroy);
    }
  
    void OnDisable ()
    {
        EventManager.StopListening ("Destroy", Destroy);
    }
  
    void Destroy ()
    {
        EventManager.StopListening ("Destroy", Destroy);
        StartCoroutine (DestroyNow());
    }

    IEnumerator DestroyNow()
    {
        yield return new WaitForSeconds (Random.Range (0.0f, 1.0f));
        audioSource.pitch = Random.Range (0.75f, 1.75f);
        audioSource.Play ();
        float startTime = 0;
        float shakeTime = Random.Range (1.0f, 3.0f);
        while (startTime < shakeTime)
        {
            transform.Translate (Random.Range (-shake, shake), 0.0f, Random.Range (-shake, shake));
            transform.Rotate ( 0.0f, Random.Range (-shake * 100, shake * 100), 0.0f);
            startTime += Time.deltaTime;
            yield return null;
        }
        Instantiate (explosion, transform.position, transform.rotation);
        Destroy (gameObject);
    }
}

Could rather work as:

using UnityEngine;
using UnityEngine.Events;
using System.Collections;

public class SelfDestruct : MonoBehaviour {

    public GameObject explosion;

    private float shake = 0.2f;
    private AudioSource audioSource;
      
    void Awake ()
    {
        audioSource = GetComponent <AudioSource>();
    }
  
    void OnEnable ()
    {
        EventManager.StartListening ("Destroy", DestroyNow);
    }
  
    void OnDisable ()
    {
        EventManager.StopListening ("Destroy", DestroyNow);
    }
  
    IEnumerator DestroyNow()
    {
        EventManager.StopListening ("Destroy", DestroyNow);
      
        yield return new WaitForSeconds (Random.Range (0.0f, 1.0f));
        audioSource.pitch = Random.Range (0.75f, 1.75f);
        audioSource.Play ();
        float startTime = 0;
        float shakeTime = Random.Range (1.0f, 3.0f);
        while (startTime < shakeTime)
        {
            transform.Translate (Random.Range (-shake, shake), 0.0f, Random.Range (-shake, shake));
            transform.Rotate ( 0.0f, Random.Range (-shake * 100, shake * 100), 0.0f);
            startTime += Time.deltaTime;
            yield return null;
        }
        Instantiate (explosion, transform.position, transform.rotation);
        Destroy (gameObject);
    }
}

???

Well for starters, you’re going to also have to pass in the MonoBehaviour that will run the coroutine (less you want to use some global one).

Why does this sound dirty?

Why not? I use them all the time.

Sure add this to the EventManager for starting listening:

    public static void StartListening (string eventName, MonoBehaviour handle, System.Func<IEnumerator> coroutineListener)
    {
        StartListening(eventName, () => {
            handle.StartCoroutine(coroutineListener);
        });
    }

But since it uses an anonymous delegate, removing it isn’t as straight forward. You’ll need to track the an association between the coroutine and created delegate. Which if you really want to do I can go into further detail about…

but…

I personally think you should look for a different way of doing such things. This is honestly a very weak method of doing things. And I don’t see the need of adding coroutine support to it… if you want coroutines, just do it the way the example code from the link you supplied above does it.

It’s perfectly valid code, and doesn’t require a whole bunch of extra state tracking to handle StopListening.

@lordofduct , thanks for the comment!

Unfortunately you can’t pass a coroutine to StartListening, so your code won’t compile: IEnumerator GameOrchestrator.isLevelCompleted() has wrong return type

Because I basically want to execute a coroutine called IEnumerator isLevelCompleted() and to make it work I have to write another function called void isLevelCompletedWrapper(). This, for each coroutine I want to use in this way. That’s why it sounds dirty for me.

That’s not needed. I know what you’re talking about. I simply wanted to focus on my game instead of this script. But what I guess is that I would need to associate a string to a couple <UnityAction, delegate> to use anonymous functions. I don’t know if it’s worth the effort. Moreover I’m rather inexperienced with C#, so if you think that this is the way to go, I’ll trust you :stuck_out_tongue:

@lordofduct , to be honest, the problem I have is due to the fact that (talking about Object.Destroy)

so I have to write:

    IEnumerator isLevelCompleted()
    {
        // Destroy(gameObject) is delayed, so skipping this frame is needed
        yield return null;

        // If there are no more asteroid, start a new level
        if (!asteroidsExist())
        {
            StartCoroutine(generateLevel());
        }

And I can’t find another way to solve my issue. Maybe you have some tip for me :sweat_smile:

@Giovarco , note that the live session you linked to is kinda garbage.

They’ve got the right idea, but they’re using UnityEvent to wrap the methods, which makes no sense. UnityEvent is much more cumbersome and much slower than the native C# event system. It’s convenient for exposing events in the inspector, but that’s not what’s happening here, so they’re using the wrong tool for the job.
For what they’re doing, the C# Action is much better than UnityEvent.

@lordofduct has the right idea, if you want an event manager to run coroutines, you need an overload that handles Coroutines.

Before you implement that, though, your current problem is probably a lot easier to solve, though I need some more info about that. Where are you calling the TriggerEvent from?

@Baste

As far as I know, I could get the same behaviour using C# delegates. The fact is that the approach of the video was really “friendly” for me, since it remembers me Node.js

I’m doing an asteroids game. I call TriggerEvent every time an asteroid is hit, so I emit “AsteroidDestroyed” event and check if there are other asteroids in the map and start a new level when appropriate. For the record, before using the Event Mananger, I checked for this condition every X milliseconds using a looping coroutine, but I thought it wasn’t the best way to approach this problem.

Checking the condition every milliseconds is actually not that bad. I’m not saying that it’s better than an event system, but if you asked me “should I change from checking every millisecond to something event-based” I would’ve replied “eh, probably not worth the bother”.

That being said, there’s pretty straight forward solutions to fix this:

  1. emit AsteroidDestroyed in the Asteroid’s OnDestroy. I’m pretty sure that objects report that they’re destroyed (ie. == null) after their OnDestroy has been invoked. Not 100% sure - should check that, don’t have Unity open right now.
  2. put an IsDestroyed flag on the asteroid script, and check (asteroid == null || asteroid.IsDestroyed) when you check if there’s any left
  3. put all asteriods in a list in the manager. When it’s informed that an asteroid is destroyed, remove it from the list. If it’s the last element, then every asteroid is gone.
  4. Have a counter of how many asteroids there are. Decrement it whenever AsteroidDestroyed is called. When you’re at 0, then every one is gone.

You can probably come up with other ways to solve this, all easier than bending your pretty simple event manager into running coroutines. Having other objects run your coroutines is not something you should never do ™, but it’s something that you need to be carefull about as it becomes pretty messy pretty fast.

Thanks you all for the help :slight_smile:

@Baste I found this stackoverflow post that features another version of the code shown in the video that uses Action instead of UnityAction. Do you think?

I think a lot!

1 Like