8
\$\begingroup\$

I'm new to Unity. I was learning coroutines and I wrote this.

private void Fire()
{
    if(Input.GetButtonDown("Fire1"))
    {
        StartCoroutine(FireContinuously());
    }
    if(Input.GetButtonUp("Fire1"))
    {
        StopAllCoroutines();
    }
}

IEnumerator FireContinuously()
{
    while(true)
    {
        GameObject laser = Instantiate(LaserPrefab, transform.position, Quaternion.identity) as GameObject;
        laser.GetComponent<Rigidbody2D>().velocity = new Vector2(0, 10f);
        yield return new WaitForSeconds(firetime);
    }
}

When the button is pressed the coroutine is called and it enters the 'while' loop. When I leave the button it stops the coroutine. Shouldn't it get stuck in the 'while' loop as it is an infinite loop ? Why?

\$\endgroup\$
  • \$\begingroup\$ I just recently got back into Unity myself, I notice that the Input methods are taking in a string "Fire1", is that something you can set up in the engine to allow for key remappings rather than typing out Keycode.Foo? \$\endgroup\$ – Mkalafut Aug 6 at 17:11
  • 1
    \$\begingroup\$ It might help to realize that yield is effectively short for "Yield control to the caller until the next item in the Enumerable is requested." \$\endgroup\$ – 3Dave Aug 6 at 17:35
  • \$\begingroup\$ @Mkalafut that sounds like something to ask in a new Question post if you're unable to find the answer in the Unity documentation pages, tutorials, or your own experiments. \$\endgroup\$ – DMGregory Aug 6 at 23:43
  • \$\begingroup\$ I don't recommend StopAllCoroutines() in this case. It's fine when you're only ever using one coroutine, but if you ever planned to have more than one, this would have undesired effects. Instead you should use StopCoroutine() and just stop the one that's relevant instead of all of them. (StopAllCoroutines() would be useful e.g. when ending the level or loading a new area, etc., but not for specific stuff like "I'm not shooting anymore".) \$\endgroup\$ – Darrel Hoffman Aug 7 at 13:52
14
\$\begingroup\$

The reason is the keyword yield which has a specific meaning in C#.

On encountering the words yield return a function in C# returns, as one would expect.

Using yield to define an iterator removes the need for an explicit extra class

[...]

When a yield return statement is reached in the iterator method, expression is returned, and the current location in code is retained. Execution is restarted from that location the next time that the iterator function is called.

So there is no infinite loop. There is a function/iterator that can be called an infinite number of times.

The Unity function StartCoroutine() makes the Unity framework call the function/iterator once per frame.

The Unity function StopAllCoroutines makes the Unity framework stop calling the function/iterator.

And returning WaitForSeconds(time) from the iterator makes the Unity framework suspend calling the function/iterator for time.


A confused comment and an equally confused upvote on that comment encouraged me to elaborate further on what the keyword yield does and doesn't do.

If you write this:

IEnumerable<int> Count()
{
   int i = 0;
   yield return i++;
}

You can instead also write this:

IEnumerator<int> Count() {
    return new CountEnumerator ();
}
class CountEnumerator : IEnumerator<int> {
    int i = 0;
    bool IEnumerator<int>.MoveNext() { i++; return true; }
    int IEnumerator<int>.Current { get { return i; }
    void IEnumerator<int>.Reset() { throw new NotSupportedException(); }
}

It follows that the keyword yield is not related to multi-threading and does absolutely not call System.Threading.Thread.Yield().

\$\endgroup\$
  • 1
    \$\begingroup\$ "On encountering the words yield return a function in C# returns". No it does not. The text which you quote explains it, as does Wikipedia - "In computer science, yield is an action that occurs in a computer program during multithreading, of forcing a processor to relinquish control of the current running thread, and sending it to the end of the running queue, of the same scheduling priority.". Basically, "`please pause me where I am and let someone else run for a while". \$\endgroup\$ – Mawg Aug 7 at 12:04
  • 2
    \$\begingroup\$ @Mawg I added a second part to the answer to address your concern. \$\endgroup\$ – Peter - Unban Robert Harvey Aug 7 at 16:42
  • \$\begingroup\$ Thank you very much for clarifying (upvoted). I certainly learned something new today :-) \$\endgroup\$ – Mawg Aug 8 at 8:37
8
\$\begingroup\$

When the fire button is lifted, the second if statement is entered, and StopAllCoroutines is run. This means that the Coroutine that the while loop is running in is ended, thus there is no more infinite loop. The coroutine is like a container for the code to execute in.

I can recommend the Unity Manual and the Unity Scripting API to get a better grasp on what coroutines are and how powerful they can be.

This blog and searching youtube post was also helpful for me to better use coroutines.

\$\endgroup\$
3
\$\begingroup\$

Coroutines are a strange beast. Yield return causes the method to suspend execution until it is later stepped. Behind the scenes, it might look something like this:

class FireContinuouslyData {
    int state;
    bool shouldBreak;
}

object FireContinuously(FireContinuouslyData data) {
    switch (data.state) {
        case 0:
            goto State_0;
    }
    while (true) {
        GameObject laser = ...;
        laser.GetComponent...
        //the next three lines handle the yield return
        data.state = 0;
        return new WaitForSeconds(fireTime);
        State_0:
    }
}

And internal to Unity/C# (since yield return is a native c# feature), when you call StartCoroutine, it creates a FireContinuouslyData object, and pass it in to the method. Based on the return value, it determines when to call it again later, simply storing the FireContinuouslyData object to pass it in next time.

If you ever did a yield break, it could internally just set data.shouldBreak = true and then Unity would simply throw away the data and not schedule it again.

And if there were any data that needed to be saved between executions, it would also be stored in the data for later.

An example of how Unity/C# might implement the coroutine functionality:

//Internal to Unity/C#

class Coroutine {
    Action<object> method;
    object data;
}

Coroutine StartCoroutine(IEnumerator enumerator) {
    object data = CreateDataForEnumerator(method); //Very internal to C#
    Action<object> method = GetMethodForEnumerator(enumerator); //Also very internal to C#
    Coroutine coroutine = new Coroutine(method, data);
    RunCoroutine(coroutine);
    return coroutine;
}

//Called whenever this coroutine is scheduled to run
void RunCoroutine(Coroutine coroutine) {
    object yieldInstruction = coroutine.method(coroutine.data);
    if (!data.shouldBreak) {
        //Put this coroutine into a collection of coroutines to run later, by calling RunCoroutine on it again
        ScheduleForLater(yieldInstruction, coroutine);
    }
}
\$\endgroup\$
1
\$\begingroup\$

Another answer mentions that you are stopping co-routines when "Fire1" is up - this is completely correct, insofar as why the coroutine does not continue instantiating GameObjects after the first press of "Fire1".

In your case however this code will not become 'stuck' in an infinite loop, which is what it looks like you're looking for an answer to - i.e. the while(true) {} loop, even if you did not stop it externally.

It won't get stuck but your coroutine won't end (without calling StopCoroutine() or StopAllCoroutines()) either. This is because Unity coroutines yield control to their caller. yielding is different to returning:

  • a return statement will cease execution of a function, even if there is more code following it
  • a yield statement will pause the function, starting at the next line after yield when resumed.

Usually, coroutines will be resumed each frame but you're also returning a WaitForSeconds object.

The line yield return new WaitForSeconds(fireTime) roughly translates as "now suspend me, and don't come back until fireTime seconds have passed".

IEnumerator FireContinuously()
{
    // When started, this coroutine enters the below while loop...
    while(true)
    {
        // It does some things... (Infinite coroutine code goes here)

        // Then it yields control back to it's caller and pauses...
        yield return new WaitForSeconds(fireTime);
        // The next time it is called , it resumes here...
        // It finds the end of a loop, so will re-evaluate the loop condition...
        // Which passes, so control is returned to the top of the loop.
    }
}

Unless stopped, this is a coroutine which, once started, will perform the whole loop once every fireTime seconds.

\$\endgroup\$
1
\$\begingroup\$

A simple explanation: under the hood Unity is iterating over a collection (of YieldInstructions or nulls or whatever you yield return) using the IEnumerator that your function returns.

Since you use the yield keyword, your method is an iterator. It's not the Unity thing, it's a C# language feature. How does it work?

It is lazy and doesn't generate all the collection at once (and the collection may be infinite and impossible to be generated at once). Elements of the collection are generated as needed. Your function returns an iterator for Unity to work with. It calls its MoveNext method to generate a new element and Current property to access it.

So your loop isn't endless, it runs some code, returns an element and returns control back to Unity so it's not stuck and can do other work such as handling your input to stop the coroutine.

\$\endgroup\$
0
\$\begingroup\$

Think about how a foreach works:

foreach (var number in Enumerable.Range(1, 1000000))
{
  if (number > 10) break;
}

The control over the iteration is on the caller - if you stop the iteration (here with break), that's it.

The yield keyword is a simple way to make an enumerable in C#. The name hints at this - yield return yields control back to the caller (in this case, our foreach); it's the caller who decides when to continue to the next item. So you can make a method like this:

IEnumerable<int> ToInfinity()
{
  var i = 0;
  while (true) yield return i++;
}

This naively looks like it will run forever; but in reality, it depends entirely on the caller. You can do something like this:

var range = ToInfinity().Take(10).ToArray();

This can be a bit confusing if you're not used to this concept, but I hope it's also obvious that this is a very useful property. It was the simplest way that you could yield control to your caller, and when the caller decides to follow up, it can just do the next step (if Unity was made today, it would probably use await instead of yield; but await didn't exist back then).

All you need to implement your own coroutines (needless to say, the simplest stupidest coroutines) is this:

List<IEnumerable> continuations = new List<IEnumerable>();

void StartCoroutine(IEnumerable coroutine) => continuations.Add(coroutine);

void MainLoop()
{
  while (GameIsRunning)
  {
    foreach (var continuation in continuations.ToArray())
    {
      if (!continuation.MoveNext()) continuations.Remove(continuation);
    }

    foreach (var gameObject in updateableGameObjects)
    {
      gameObject.Update();
    }
  }
}

To add a very simple WaitForSeconds implementation, you just need something like this:

interface IDelayedCoroutine
{
  bool ShouldMove();
}

class Waiter: IDelayedCoroutine
{
  private readonly TimeSpan time;
  private readonly DateTime start;

  public Waiter(TimeSpan time)
  {
    this.start = DateTime.Now;
    this.time = time;
  }

  public bool ShouldMove() => start + time > DateTime.Now;
}

And the corresponding code in our main loop:

foreach (var continuation in continuations.ToArray())
{
  if (continuation.Current is IDelayedCoroutine dc)
  {
    if (!dc.ShouldMove()) continue;
  }

  if (!continuation.MoveNext()) continuations.Remove(continuation);
}

Ta-da - that's all a simple coroutine system needs. And by yielding control to the caller, the caller can decide on any number of things; they might have a sorted event table rather than iterating through all the coroutines on every frame; they might have priorities, or dependencies. It allows for very simple implementation of cooperative multi-tasking. And just look at how simple this is, thanks to yield :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.