Pull to refresh

Unity: What is a Coroutine and why is there an IEnumerator

Reading time5 min
Views14K
Original author: Vladislav Lazarev

The title of the article is a question I was asked in an interview for a Middle position. In this article, we will look at Unity coroutines, what they are, and at the same time we will capture the topic of Enumerator \ Enumerable in C # and a little secret of foreach. The article should be very useful for beginners.

And so, as everyone knows, the method that represents Coroutine in Unity looks like this:

IEnumerator Coroutine()
{
  yield return null;
}
Some information about coroutines in Unity and IEnumerator

As a return object after yield return can be:

  • new WaitForEndOfFrame() - stops execution until the end of the next frame

  • new WaitForFixedUpdate() - stops execution until the next physics engine frame.

  • new WaitForSeconds(float x) - stops execution for x seconds of game time (can be changed via Time.timeScale )

  • new WaitForSecondsRealtime(float x) - stops execution for x seconds of real time

  • new WaitUntil(Func<bool>) - suspends the coroutine execution until the supplied delegate evaluates to true.

  • new WaitWhile(Func<bool>) - inverse of WaitUntil, continues execution when Func returns false

  • null - Same as WaitForEndOfFrame(), but execution continues at the beginning of the next. frame

  • break - ends the coroutine

  • StartCoroutine() - execution stops until the moment when the newly started coroutine ends.

You can start a coroutine via StartCoroutine(Coroutine()).

Coroutines are not asynchronous, they run on the main thread of the application, the same as drawing frames, instantiating objects, etc., if you block the thread in the coroutine, the whole application will stop, coroutines with asynchrony would use "IAsyncEnumerator", which Unity does not support. The coroutine allows you to stretch the execution over several frames, so as not to load 1 frame with large calculations. Unity provides the UnityWebRequest type for Http requests that can be made "asynchronously" in multiple frames, which may appear to be "asynchronous", in fact it is a wrapper over a native asynchronous HttpClient that provides some information synchronously, by the type of the isDone field, which renders - whether the request has ended or the response is still pending, but the request itself is asynchronous.

IEnumerator is C#'s standard implementation of the iterator pattern, which contains syntactic sugar for storing state. 

It returns an IEnumerator and has an unusual return with yield.
yield return is a component of IEnumerator, this bundle is converted, at compilation, into a state machine that saves the position in the code, waits for the MoveNext command from IEnumerator and continues execution until the next yield return or the end of the method, more details can be found on the microsoft site.

The IEnumerator interface contains the following elements:

public interface IEnumerator
{
  object Current { get; }

  bool MoveNext();
  void Reset();
}

Under the hood of Unity, this is handled something like this: Unity receives an IEnumerator, which is passed through StartCoroutine(IEnumerator), immediately calls MoveNext, in order for the code to reach the first yield return, it is worth clarifying here that when such a method is called, the execution of the code inside the method does not start independently, and you need to call MoveNext, this can be checked with a simple script, which is presented under this paragraph, and then if Unity receives an object of type YieldInstruction in Current, then it executes the instruction and calls MoveNext again, that is, the method can return any type, and if it not a YieldInstruction, then Unity will treat it as yield return null.

private IEnumerator _coroutine;

// Start is called before the first frame update
void Start()
{
  _coroutine = Coroutine();
}

// Update is called once per frame
void Update()
{
  if (Time.time > 5)
    _coroutine.MoveNext();
}

IEnumerator Coroutine()
{
  while (true)
  {
    Debug.Log(Time.time);
    yield return null;
  }
}
 The log shows that the method was called for the first time at the 5th second, according to the condition in Update()
The log shows that the method was called for the first time at the 5th second, according to the condition in Update()

Great, we covered the main point, namely what is IEnumerator and how it works. Now let's look at this case:

Let's describe a class that inherits the IEnumerator interface

class TestEnumerator : IEnumerator
{
    public object Current => new WaitForSeconds(1);
		
    public bool MoveNext()
    {
        Debug.Log(Time.time);
        return true;
    }

    public void Reset()
    {
    }
    
    /// This class is equivalent to the following coroutine:
    /// IEnumerator Coroutine()
    /// {
    /// 	while(true){
    ///			Debug.Log(Time.time);
    ///			yield return new WaitForSeconds(1);
    /// 	}
    ///	}
}

And now we can use it in the following way:

void Start()
{
  StartCoroutine(new TestEnumerator());
}
Performs the same as a coroutine method
Performs the same as a coroutine method

And so, we have considered IEnumerator and coroutines, here you can consider different use cases for a long time, but at the root there remains the transfer of IEnumerator in any form to the StartCoroutine method.

Now I propose to consider IEnumerable, this interface is inherited by the native C# array, List from System.Generic and other similar types, its whole essence lies in the fact that it contains the GetEnumerator method, which returns an IEnumerator:

public interface IEnumerable
{
  [DispId(-4)]
  IEnumerator GetEnumerator();
}

Let's implement a simple example:

class TestEnumerable : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        return new TestEnumerator();
    }
}

And now, we can do the following:

IEnumerator Coroutine()
{
  foreach (var delay in new TestEnumerable())
  {
    Debug.Log($"{Time.time}");
    yield return delay;
  }
}
You can see that the time is displayed twice in the log, this is due to the fact that we still have Debug in the TestEnumerator in the MoveNext method.
You can see that the time is displayed twice in the log, this is due to the fact that we still have Debug in the TestEnumerator in the MoveNext method.

There are many usages for this, for example, you can add a random delay time to the TestEnumerator:

class TestEnumerator : IEnumerator
{
    public object Current => new WaitForSeconds(_currDelay);
    private float _currDelay;

    public bool MoveNext()
    {
        _currDelay = Random.Range(1.0f, 3.0f);
        return true;
    }

    public void Reset()
    {
    }
}
The time between logs is not the same
The time between logs is not the same

And some magic for beginners: foreach does not require that the object returned by GetEnumerator implement IEnumerable, most importantly, that the type after "in" has GetEnumerator() method, and returns a type with Current property and MoveNext() method, that is, we can do so:

class TestEnumerator // Here was inheritance from IEnumerator
{
    public object Current => new WaitForSeconds(_currDelay);
    private float _currDelay;

    public bool MoveNext()
    {
        _currDelay = Random.Range(1.0f, 3.0f);
        return true;
    }
  
  	// There was the Reset method from IEnumerator, it's no longer needed :)
}

class TestEnumerable // Here was inheritance from IEnumerable
{
  	// The return type was IEnumerator
    public TestEnumerator GetEnumerator()
    {
        return new TestEnumerator();
    }
}

As you can see, there is no inheritance anywhere and no mention of IEnumerable and IEnumerator, but we can also use the following code:

IEnumerator Coroutine()
{
  foreach (var delay in new TestEnumerable())
  {
    Debug.Log($"{Time.time}");
    yield return delay;
  }
}
Everything works the same and without errors
Everything works the same and without errors

And so, having analyzed the coroutines, IEnumerator, IEnumerable and foreach, you should see an example of using this knowledge in practice:

More convenient to use Coroutine

Here I wanted to describe the implementation of a coroutine with a CancelationToken and start / end events with the ability to pause execution, but I was late, and there is a ready-made solution on github, I advise you to study, although I do not completely agree with the implementation:

unity-task-manager/TaskManager.cs at master AdamRamberg/unity-task-manager (github.com)

I would be grateful for criticism and comments, I also advise you to look at my other articles.

Translated for TechNation GlobalTalent Visa.

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments0

Articles