Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question How Does StartCoroutine() Accept A Delegate With Multiple Signatures?

Discussion in 'Scripting' started by Bmco, Dec 1, 2023.

  1. Bmco

    Bmco

    Joined:
    Mar 11, 2020
    Posts:
    50
    I use delegates (a method acting as a variable) everywhere. In C#, you cannot set/pass a delegate with different arguments.

    However, it seems that StartCoroutine(IEnumerator routine) gets around this somehow since the "IEnumerator routine" argument can accept any coroutine that can have any combination of arguments.

    Does anyone know why this works? Is it something special/specific about coroutines? Is it finding the method by its name via Reflection or something like that?
     
  2. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,831
    No, you're not passing your method along but an IEnumerator object that is returned by your generator method. You don't do
    StartCoroutine(MyMethod)
    but you do
    StartCoroutine(MyMethod())
    . That's a big difference. When you call a generator method (a method with a yield statement in it) you actually get back a statemachine object that represents the code inside the original "method".

    A long time ago I've written a coroutine crash course on Unity Answers. Though since the site has degraded over time and was ultimatively replaced by Discussions, I do have a mirror on github. Over there I explain what an IEnumerator actually is and how Unity uses those iterators to implement coroutines.

    So this has nothing to do with signatures of methods. This is just an interface that is implemented by a class. All the magical transformation of your code is done by the C# compiler that turns your linear code into a statemachine. You can use ILSpy or any other .NET reflector to see the hidden class that was generated. Though ILSpy is clever enough to actually decompile iterators, so look at the settings to turn it off ^^.
     
    CodeSmile and Nad_B like this.
  3. Bmco

    Bmco

    Joined:
    Mar 11, 2020
    Posts:
    50
    Thanks for the quick response. I'll start to digest your answer. It was more than I bargained for. LOL.
     
  4. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,242
    The main thing is that:
    1. StartCoroutine simply accepts a parameter of type IEnumerator.
    2. When you execute MyMethod(), it returns an object of type IEnumerator.
    So you are actually not passing just a reference of MyMethod to StartCoroutine, you are actually immediately executing MyMethod, and then passing the value that was returned by the method to StartCoroutine.
    Code (CSharp):
    1. IEnumerator enumerator = MyMethod(1, 2); // My method gets executed immediately here
    2. StartCoroutine(enumerator); // You pass the object returned by it to StartCoroutine

    The confusing part is that when you write an IEnumerator method in C#, the compiler actually does a bunch of magic behind-the-scenes and generates a whole new class.

    Given a coroutine like this:
    Code (CSharp):
    1. public IEnumerator MyMethod(int parameter1, int parameter2)
    2. {
    3.     Debug.Log("Step " + parameter1);
    4.  
    5.     yield return new WaitForSeconds(1f);
    6.  
    7.     Debug.Log("Step " + parameter2);
    8. }
    The compiler would generate something like this:
    Code (CSharp):
    1. class MyMethodEnumerator : IEnumerator
    2. {
    3.     int parameter1;
    4.     int parameter2;
    5.  
    6.     int step;
    7.  
    8.     public object Current { get; private set; }
    9.  
    10.     public bool MoveNext()
    11.     {
    12.         if(step == 0)
    13.         {
    14.             Console.WriteLine(string.Concat("Step ", parameter1.ToString()));
    15.             Current = new WaitForSeconds(1f);
    16.             step = 1;
    17.             return true;
    18.         }
    19.  
    20.         if(step == 1)
    21.         {
    22.             step = -1;
    23.             Console.WriteLine(string.Concat("Step ", parameter2.ToString()));
    24.             return false;
    25.         }
    26.  
    27.         return false;
    28.     }
    29. }
    And when you execute MyMethod(), actually, an instance of this compiler-generated type gets returned.
    Code (CSharp):
    1. public IEnumerator MyMethod(int parameter1, int parameter2)
    2. {
    3.     return MyMethodEnumerator() { parameter1: parameter1, parameter2: parameter2 };
    4. }
    It is with the help of this compiler generated type that coroutines are able to do their thing, where all their code does not get executed at once, but in smaller chunks, separated by
    yield
    statements.

    Unity can use the generated object to do something like this essentially:
    Code (CSharp):
    1. public static void StartCoroutine(IEnumerator routine)
    2. {
    3.     while(routine.MoveNext())
    4.     {
    5.         if(routine.Current is WaitForSeconds waitForSeconds)
    6.         {
    7.             Debug.Log("Waiting for "+waitForSeconds.seconds+ " seconds...");
    8.         }
    9.     }
    10. }
    SharpLab example.
     
    Last edited: Dec 1, 2023
    Bmco and Nad_B like this.
  5. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    560
    Yes the difference all lie in the parenthesis ()

    MethodName is a delegate, it means "get a reference to that method without executing it"
    MethodName() is a method call, it means execute the method and get the result.
     
    halley, Bmco and SisusCo like this.
  6. Bmco

    Bmco

    Joined:
    Mar 11, 2020
    Posts:
    50
    Yes. I see it now. The syntax was too sugary for me.
     
    Bunny83 and SisusCo like this.