Problem with WaitForSeconds() in a nested loop

Hi, I’m creating a horizontal wave defense game and I’m stuck at the enemy spawning algorithm. Currently I want to spawn an enemy at a random location every X seconds. Every time a mob spawns it reduces the weightCounter variable which simply said controls how many enemies will spawn. Once it hits 0, the wave ends. Apart from spawning enemies at random locations, there will be different arrangements, that’s why I’ve put a main cycle like this:

public void SpawnWave()
    {
        impact = gameController.playerStr / gameController.predictedStr;
        waveWeight = weight * impact;
        weightCounter = waveWeight;
        
        while(weightCounter > mobStats.weight)
        {
            StartCoroutine(MobCombination_1_Random());
        }
        weight = (weight + 1) * 1.05f;
        gameController.EndWave();
    }

From it I will call different mob combinations like this one:

IEnumerator MobCombination_1_Random()
    {
        
        int mobNumber;
        mobNumber = (int)(weightCounter / mobStats.weight);
        if(mobNumber <= 7)
        {
            for (int i = 0; i < mobNumber; i++)
            {
                Vector2 position = new Vector2(this.transform.position.x, Random.Range(-4f, 4f));
                GameObject.Instantiate(flyingEnemy, position, Quaternion.identity);
                weightCounter -= mobStats.weight;
                Debug.Log("We made an enemy and now we're waiting one second.");
                yield return new WaitForSeconds(1f);
                Debug.Log("We waited one second.");
            }
        }
        else if (mobNumber > 7 && mobNumber <= 15)
        {
            int temp = Random.Range(8, mobNumber);
            for( int i = 0; i < temp; i++)
            {
                Vector2 position = new Vector2(this.transform.position.x, Random.Range(-4f, 4f));
                GameObject.Instantiate(flyingEnemy, position, Quaternion.identity);
                weightCounter -= mobStats.weight;
                yield return new WaitForSeconds(0.7f);
            }
        }
        yield break;
    }

The initial values of weight is 3 and impact is 1.
The problem is that instead of creating one enemy every 1 second in this example, it creates 3 then after a second 2 then after another second 1. At that point the weightCounter is less than 0 and should have ended the while loop. Why does the WaitForSeconds() not work in this example?

P.S. I know that the yield break; is pointless, but I’ve added it just as a future reminder to myself that it’s unfinished.

Your problem is not your coroutine but your SpawnWave method. Coroutines more or less run independently from the rest of your code or from the code that started the coroutine. Your SpawnWave method will run all it’s code within the same frame. Actually if you would have some blocking code your game would actually freeze because if you have an infinite loop in a normal method the current frame can never complete.

In your case you’re lucky that your game did not freeze. When you call StartCoroutine, Unity will execute your coroutine’s code up to the first yield statement. At this point StartCoroutine will return to the caller (in your case back to your SpawnWave method).

So what you’re actually doing here is starting 3 seperate coroutines at once which all run in parallel. Since you lower the global “weightCounter” before the first yield your while loop in SpawnWave can actually end properly. Though now you have 3 coroutines running in parallel and each will spawn an enemy in the frame they were started and each one will wait one second. Since you based your mobNumber on the weightCounter when the coroutine was started, the first coroutine has a count of 3, the second one a count of 2 and the last one a count of 1.

After the one second delay all 3 coroutines will continue. The last coroutine which has a count of one is already done with it’s for loop and will terminate. The first and second coroutine each will spawn a new enemy and wait for another second. After that delay the two remaining coroutines will go for their next loop iteration, though the second one has reached it’s count of 2 and will terminate while the first one spawns another enemy and waits again for one second. After that the first (and last remaining) coroutine will terminate as well.

If you want code to wait for some sequence to finish it has to be a coroutine itself. There is no way to simply wait in a normal method as this would freeze your game. You can simply turn your SpawnWave method into a coroutine and wait for your “MobCombination_1_Random” coroutine to finish.

public IEnumerator SpawnWave()
{
    impact = gameController.playerStr / gameController.predictedStr;
    waveWeight = weight * impact;
    weightCounter = waveWeight;
    
    while(weightCounter > mobStats.weight)
    {
        yield return StartCoroutine(MobCombination_1_Random());
    }
    weight = (weight + 1) * 1.05f;
    gameController.EndWave();
}

Note that it may be simpler and more effective to handle the whole spawning logic in a single coroutine. Starting a coroutine has some overhead. Coroutines are not methods but statemachine objects. Each time you start a coroutine a new instance of that statemachine will be created.

I’ve once wrote a very simple but powerful spawning system over here. As you can see it’s just a single coroutine that runs forever and simply lowers the spawn delays each time the waves have completed. Though you can get fancy how you want to increase difficulty each time. With this simple setup you can already design your waves completely in the inspector. Note that is was just written as a framework. I initially had the classic Warcraft3 tower defence in mind. Some maps spawn all enemies of a wave at once, others spawn one enemy at a time. The actual spawning is still missing. So you can use instantiate with or without a delay between each spawn or use an object pool to spawn the enemies.

Your issue is the StartCoroutine() call. You have this a while loop. Remember that whenever you yield in a coroutine it will return to the calling code. In this case your calling code is still in the while loop. I don’t have figures for all your variables but based on your description it looks like the coroutine MobCombination_1_Random() fires off 3 times before weightCounter > mobStats.weight is no longer true. This will occur in one frame. Your 3 coroutines will then terminate dependant on your conditional values and, again from your description, not all at the same time.

Based on the desired behaviour you are describing, you should only start this coroutine once i.e not in a loop.