How do you understand IEnumerators?

What are some good uses for them? What makes them appropriate?

Share your challenge solutions here.

Im so pleased to see others using food for analogys. People at work are real sick of my food analogies :smiley:

thankfully IEnumorators have meant something to me for a long time, along with the whoile yield return and so on.

5 Likes

I laughed a lot during that lesson. :slight_smile:
I haven’t used IEnumerators or Coroutines much before, but a few of the applications of them I can think of are:
pausing an NPC’s behavior until a criteria is fulfilled, or timer (Replacing dwellTimeAtWaypoint?)
Hmmm… I’m having trouble thinking of examples that couldn’t also be solved by using a float that counts up/down in the Update function. How do IEnumerators/Coroutines differ from that functionality?

2 Likes

One good way to use IEnumerators is to read in data from one area which takes a time to generate them, such as finding prime numbers, and you can return them as you find them

3 Likes

Would an IEnumerator be used in a card game scenario, as in to take the top card from a deck? similar to the top chip in your analogy? or am i missing the point on this?

It could be. Even more useful. If you have an array of cards and you want to take one out randomly repeatedly you could create an IEnumerator that would lazily pick a card at random and remove it from the array. This could be used to simulate a shuffled deck.

1 Like

I understand that they are a type of array which we can have access to any part of it and can store unique values(This opposed to a Stack where its generic and you only can take the top). They are a bit heavier on the system to deal with correct? I was looking at using one of these for the deck of the card game I am making. I figure a deck load out screen will have to be editing where the player will be able to access a view of their database which would be manipulating/building their IEnumerator, which would be stored in the database, under their profile data correct? Is an asset bundle the best way of passing this data from one scene to the next? Another use of IEnumerator would be for storing states of things? Such as if I want those cards to go through an upgradable process? or would this be better suited under a different way?
I would imagine the T-Stack to be more edible… :thinking:.

The IEnumerator is more like a stack in that you can only access it one at a time. It cannot be expensive in itself because it is just an interface. You can implement it with an array or with a list or even calculate the values as you go (like the fibonacci example from above).

I was thinking of the combat system in Knights of the Old Republic. You could queue actions like attacks or using items, and they would happen in order, pausing for a a certain amount of time elapsed.

6 Likes

Any kind of cool down for example. Weapon reload, mana charge, explosive timer, sprint cool down, health regeneration, time between attacks like if the player had a heavy weapon like an axe.

Changing gears in a racing game, Maybe at the start of a race when the counter is going down 3, 2, 1.

7 Likes

I have used coroutines often before, but did not really get into the theory of how they worked and when using them would be most effective, I used them mainly when they were required by another function or class. If I am following the logic here, there are a lot of reasons we could want to use coroutines. Here are a few situations where I am thinking they would be useful:

  • Games with continuous waves need new enemies to spawn only after previous ones have been defeated or after a time interval.
  • A player can only use a special item like a potion or a spell after a cooldown period from the last time they used/cast it.
  • Many phone-games now have stamina-bars or energy-points that regenerate every few minutes and/or buildings/workers that generate income/resources every so often.
  • Asset loading/preloading could be another place to do this if you want the player to be able to play the game while things are loading in the background in preparation of the next gameplay moment.
13 Likes

If I am not mistaken the IEnumerator is a part of a Design Pattern, and needs to be used in conjunction with a IEnumeratable. The idea being that you create some sort of Data Structure that you would like to be able to iterate over in a meaningful way. You user the IEnumerator to make create that meaning. This is a huge simplification of the topic, and I am not doing it justice.

I think the yield return is just a build in construct that uses the Enumerator Pattern to unroll a loop for asynchronous access.

The best explanation I have ever heard was from a guy called Nelson LaQuet from the site 3dBuzz.com. To bad the guy running that site got sick because I dont think they keep that site up any more. However, the C# 102 - Spring 2012 and the C# 103 are awesome, he stresses good design and architechure and gives his examples via that spotlight. The bad it is a recorded online lecture format so it is long and not well sectioned but some of the best info I have ever heard on C#.

2 Likes

IEnumerator reminds me of a stack or it is part of a stack. Stacks are used to setup a list of instruction in assembly if my memory serves me right. So anything that is stack and can be pulled and called. Example might be a dialog with a stack of strings that you are going over or voice files. Maybe something like a magic deck of cards, that one is drawn and destroyed and another is called until empty. Ammo types. Any kind of limited special container types like the example with the Pringles. Because it can yield something can be drawn wait for use and a call for another item.

1 Like

Good example. One more thing that IEnumerators can do is lazily produce the value. So unlike a stack that requires everything to be placed on it already. So Stacks are IEnumerators but not all IEnumerators are Stacks.

1 Like

I’ve used coroutines for boss battles before, special for generic behaviour, like wait until certain condition (current health, time, player position) to start next phase or to perform certain attack/animation/eatAPringle/whatever.

4 Likes

Love it. Great example.

2 Likes

I liked the pringles example - sausage pringles sounds awesome! We don’t have those here.

This sounds exactly like a linux fifo pipe and/or a queue to me. It is kinda like a poor mans threading model where you can unblock the main routine to let the producer produce more data.

2 Likes

True, very similar to pipes. My unix foo isn’t what it used to be so I’m not sure if the two processes in a pipe excecute in parallel but I think they might.

With IEnumerators it’s important to note there is no parallelism just asynchronous execution. All the code can run in a single thread.

3 Likes

IEnumerators can be good if you don’t know at create time what all to put in your “Pringles tube” or don’t want to spend the time at create time to fill the tube. Maybe the next thing in the queue needs some calculation. Maybe the next thing in the tube doesn’t even exist when you create the tube; maybe it’s created in runtime.

An IEnumerator study.
In a turn based Grid game I am working on, I want my character to follow an A* calculated path on a Hexagonal grid. I want the move to happen over time an smoothly. Neither a sudden shift to the location or a step step step.

I could write a complex series of Update If/thens to determine the character’s location along the path and it’s location between tiles, but we’d waste a lot of time each update branching through states.

Enter the IEnumerator. Here’s a few fragments of code from my GridMover.cs script. We’ll start with the call to the Mover in the first place:

public void BeginMoveAction(List<Vector2Int> pathToFollow, System.Action callback)
        {
            callbackAction = callback;
            path = pathToFollow;
            StopAllCoroutines();
            StartCoroutine(ProcessMovement());
        }

In my particular scheme, the controller calls the Mover, giving the function a path to follow as a list of Vector2Int, and a callback function to call when the Mover has reached the destination. It then starts a coroutine ProcessMovement()

IEnumerator ProcessMovement()
        {
            Vector3 destination=transform.position;
            Queue<Vector3> queue = new Queue<Vector3>();
            if (path.Count > 0)
            {
                for (int i = 0; i < path.Count; i++)
                {
                    if(i<MaxStepsPerTurn+1)
                    {
                        destination = TileUtilities.IdealWorldPosition(path[i]);
                    }
                    queue.Enqueue(destination);
                }
            }
            queue.Dequeue(); //cook off the tile we're standing on.
            int currentSteps = 0;
            yield return new WaitForEndOfFrame();
            while (queue.Count > 0 && currentSteps < MaxStepsPerTurn)
            {
                Vector3 currentWaypoint = queue.Dequeue();
                yield return MoveToNextLocation(currentWaypoint);
                currentSteps++;
            }
            anim.SetTrigger("Idle");
            transform.position = destination;
            callbackAction?.Invoke();
        }

Here’s where the bulk of the magic happens. The list that was set in path is a list of Vector2Int, representing the tile in question. Since it’s a tile based game, we don’t need “precise” 3d coordinates, we can calculate these easily. In this case there is a utility class that manages this for us. So the first thing we need to do is convert the path from a list of Vector2Int to a List with our list of real world coordinates.
Once this conversion is complete, we yield back some time to the game, essentially waiting until the next update frame. We don’t strictly speaking have to do this yet, but it’s best for any running animations that we keep the timeslices as short as possible and we’ve already spent a bit of time converting the coordinates.
Once we’ve done this it’s time to loop over the queue. I’ve chosen a while loop for this, but you could just as easily do it with a foreach. I stop it at the maximum steps per turn so even if the game sends me a path with 20, we’re only going to move maxStepsPerTurn (maybe 3 or 4?)
The loop ultimately calls yet another Coroutine which we’ll explore in a moment. That routine will handle the actual movement from one step to another.
Once the queue is empty or the character has moved his max, the character’s animator is set to idle, and just in case the MoveToNextLocation() coroutine missed it’s mark by a few, the position is fixed.
Then the callback is sent, letting the controller know that the mover has finished it’s move.
Why a callback? Because this way it doesn’t matter if it was the Player Controller, an AI Controller, or perhaps a Fighter component making the call, it just issues it’s own callback and it will always be right.

IEnumerator MoveToNextLocation(Vector3 newLocation)
        {
            transform.LookAt(newLocation);
            anim.ResetTrigger("Walking");
            anim.SetTrigger("Walking");
            while (Vector3.Distance(transform.position, newLocation) > .2f)
            {
                transform.LookAt(newLocation);
                yield return new WaitForEndOfFrame();
            }
            transform.position = newLocation;
        }

The final IEnumerator MoveToNextLocation() handles the actual moving. The example I’ve posted is for an animator that utilizes Root Motion. This simplifies things, all the mover has to do is tell the Animator to walk and make sure each frame that the character faces the target. Then it’s a matter of checking the distance. If it’s close enough, we end the coroutine and control returns automatically to the first coroutine.
And here you can see the practical example of our Coroutines in action. The GridMoverScript kicks in whenever an AI or the player determines a move, and returns control when it’s all over, and there are no Updates in PlayerController, AIController, or Mover.cs. It’s all managed with Coroutines and callback routines.

13 Likes

Privacy & Terms