4

Recently I saw a tutorial of spawning objects using C# in Unity. In that the teacher used a function like this

   public IEnumerator CallSpawner()
   {
      yield return new WaitForSeconds(0.5f);
      SpawnObstacles();
   }

I wanna ask what is the use of IEnumerator function. Can't we do this by this process

float diffTime = 0f;
private void Update()
{
    if(Time.time - diffTime == 0.5f)
    {
        diffTime = Time.time;
        SpawnObstacles();
    }
}

I read the documentation but couldn't understand it..

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
I-am-developer-9
  • 434
  • 1
  • 5
  • 13
  • 5
    Are you sure it wasn't either `IEnumerable` or `IEnumerator`? – Jon Skeet Oct 08 '20 at 08:26
  • Never heard of `IEnumerate` ... there is [`IEnumerator`](https://learn.microsoft.com/dotnet/api/system.collections.ienumerator) and [`IEnumerable`](https://learn.microsoft.com/dotnet/api/system.collections.ienumerable) .. do you mean one of these? – derHugo Oct 08 '20 at 08:26
  • 2
    You most probably mean an `IEnumerator` in the usage for [Coroutines](https://docs.unity3d.com/Manual/Coroutines.html) . please read that first .. there are also tons of tutorials about Coroutines ;) – derHugo Oct 08 '20 at 08:28
  • I am sorry its IEnumerator – I-am-developer-9 Oct 08 '20 at 08:29
  • Looks like the important part is that Unity usually needs to have its logic perform fast, and the trick is not IEnumerator but the [yield keyword](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield) – Cleptus Oct 08 '20 at 08:33
  • @Cleptus `yield` is just a mandatory part of any `IEnumerator` ... it has also nothing to do with performance ;) Code inside an `IEnumerator` using `yield` can still be slow and freeze your app. – derHugo Oct 08 '20 at 08:35
  • 1
    @derHugo *yield is a mandatory part of any IEnumerator* - this isn't true. Plenty of enumerators exist that do not use yield; the one in [the sample in the fine manual](https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerator?view=netcore-3.1) for example – Caius Jard Oct 08 '20 at 08:39
  • @CaiusJard what I ment is anything that looks like `private IEnumerator XYZ` .. ofcourse if you implement the `IEnumerator` interface yourself then no you don't ... because then you implement `MoveNext` with a proper `return` statement sure ... but what `yield` does in the end is call and execute exactly that `MoveNext` of the `IEnuemrator` you give it. In the case of Unity that's e.g. `yield return WaitForSeconds` which ofcourse inside implements the `IEnumerator` interface and just executes a method ... – derHugo Oct 08 '20 at 08:47

3 Answers3

8

what is the use of IEnumerator function

IEnumerator there isn't a function, it's a return type. C# doesn't have functions either (but I know what you mean) - in C# we call them methods.

IEnumerator being so called implies it is an interface, so any class that implements the IEnumerator interface can be returned by this method

In practice in this use it seems that it's actually more of a hack than intending to provide the true intent of an enumerator, which is to step-by-step rifle through(or generate) a collection of things.

When you use a yield return statement within a method "some magic happens" whereby it's not a return in the classic sense, but creates a facility whereby the code can resume from where it left off (calling for the next item out of the returned enumerator will cause the code to resume from after the yield, with all the state it had before, rather than starting over).

If you look at the MSDN example for yield:

public class PowersOf2
{
    static void Main()
    {
        // Display powers of 2 up to the exponent of 8:
        foreach (int i in Power(2, 8))
        {
            Console.Write("{0} ", i);
        }
    }

    public static System.Collections.Generic.IEnumerable<int> Power(int number, int exponent)
    {
        int result = 1;

        for (int i = 0; i < exponent; i++)
        {
            result = result * number;
            yield return result;
        }
    }

    // Output: 2 4 8 16 32 64 128 256
}

The loop is controlled by i; if this wasn't a yield return then this wouldn't function as intended (it couldn't return an enumerator for a start but we'll leave that out). Suppose it was just a normal return, the loop would never loop at all; the code would enter, start the loop, hit the return, and just return a number one time and all memory of where the loop was would be forgotten.

By making it a yield return, an enumerator is returned instead, and a small set of "saved state" is set up whereby the loop can remember the current value of i - each time you ask for the next value, the code resumes where it left off from (ie just after the yield), the loop goes round again and a different value is yielded. This continues up to the max of course.. at which point the returned enumerator says it has no more items

You could yield forever, too.. If the code can never escape the loop then it will yield/generate forever


In this case you have to use yield return new WaitForSeconds because that's how WaitForSeconds is intended to work. Yielding gives up an enumerator to the calling method, which is then free to enumerate it. From the docs it looks like this is deliberately done on the next frame, so using yield (perhaps repeatedly) is a way of arranging a block of code that occurs across several frames without having some sort of external state management that remembers where the process is up to and a wordy

  • if state = 1 then close the door and add 1 to the state,
  • else if state = 2 then light the torch and add 1
  • else if state = 3 ...".

You can just

  • yield,
  • close the door,
  • yield,
  • light the torch,
  • yield ..

Can't we do this by this process

Sure, looks reasonable; look at the clock 100 times a second and if 0.5 seconds have passed since you first looked at the clock, spawn the obstacles

I'd imagine (never used Unity; don't profess to know anything about it other than having read the docs for this one function) that your Update loop has a lot more to be getting on with, so handing a process off to a dedicated wait-then-do is more efficient than spending all your time looking at a clock and carrying out a potentially complicated calc to work out if you should do something; most things in life that start out as poll-every-x-milliseconds benefit from being switched to an "if the event occurs, react to it" way of working

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • ok thank you. It means that the use IEnumerator is for a delay with less code and less memory. – I-am-developer-9 Oct 08 '20 at 09:04
  • 1
    @I-am-developer-9 This pattern was the pre-cursor for what we in dotnet call `async` `await`. The idea isn't about reducing code or memory. It is about keeping the UI thread responsive by using async code, whilst avoiding callback hell. – Aron Oct 08 '20 at 09:52
  • 1
    "C# doesn't have functions either (but I know what you mean) - in C# we call them methods" Just for clarity; methods are member functions. technically C# does not allow functions to be outside of a class, however you could debate that static methods of a static class are functions and not methods. This is a good explanation here: https://stackoverflow.com/a/155655/2146952 – Oria Nov 16 '21 at 23:44
  • 1
    Objection: argumentative – Caius Jard Nov 16 '21 at 23:58
0

The code you are looking at only exists in Unity3d and DotNet Framework < 4.0.

The reason you can't find any documentation on this is:

  1. It is an evil hack (Pioneered by Jeffrey Richter) that abuses the IEnumerable state-machine generator feature of the C# compiler. See https://www.codeproject.com/Articles/39124/Learn-How-to-Simplify-the-Asynchronous-Programming for an example.
  2. The Microsoft guys have long since added this hack into the dot net compiler, and they called this feature async/await.

Unfortunately, Unity3d is based on an old version of Mono, which does not support async/await.

Luckily most of the features of async/await can be done using this approach

There are a few reasons to go with Jeff's Power Threading Library approach.

  1. Flow. You have a single method that sets up the wait, defines the wait time and executes the payload, and in that order.
  2. Performance. Your suggested code will run many times per second, slowing down the system as you poll for the timeout to complete. Instead, the PTL approach supplied a callback event, which is scheduled in the future.
  3. Clean up. Your code will continue to run, checking for 0.5 long after SpawnObstacles() ran once.
  4. Fuzzy timing. If your PC is skipping frames due to lag, performance etc. You might have skipped the 0.5 frame completely. More-over, it is possible, that 0.5d will never occur (see Jon Skeet's Pony Fail post, you are doing a double to double comparison). The timer/callback would definitely fire at sometime after 0.5 seconds.

But the main reason would have to be Flow. In this very simple example, we won't easily be confused. However imagine the following effect.

  • New enemies will spawn every 10 seconds
  • Each spawning will follow the fibonacci sequence
  • At 10s 1 enemy will spawn
  • At 20s 1 enemy
  • At 30s 2 enemy
  • At 40s 3 enemy
  • At 50s 5 enemy
  • At 60s 8 enemy
  • etc

This would be pretty difficult to write with the update method.

However you can do it with the IEnumerable easily.

public IEnumerator CallSpawner()
{
  int current = 1;
  int last = 0;
  while(true)
  {
      yield return new WaitForSeconds(10f);
      SpawnEnemies(current);
      var next = current + last;
      last = current;
      current = next;
   }
}

As a bonus, you will notice that all the variables are nicely encapsulated in the Method, as opposed to in free floating fields.

Meaning you can run multiple CallSpawner() without them interfering.

Aron
  • 15,464
  • 3
  • 31
  • 64
  • Unity most definitely does support Async and await. I've been using them myself –  Oct 09 '20 at 05:18
0

Coroutines are ways to write code that says "wait at line for a little."

To be more precise, WaitForSeconds and "wait until the next frame" (yield return null) are the commands that make code wait at one spot. They can only be used inside Coroutines.