3

I've been working on this section of code in the main Gameplay.cs script of my Unity 2D project for a long while now and no matter which way I change it and shift things around, it either causes Unity to lock up on Play or, after some tweaking, will instantly go from Wave 0 to Wave 9. Here is my original attempt, which causes Unity to freeze on Play. I do not understand why this happens and would like some insight on where my logic has gone wrong.

I am trying to achieve the following:

  • On Play, game waits startWait seconds.
  • After startWait, game goes into SpawnWaves() method and waits waveWait seconds between every wave and increments the wave count by one during each wave.
  • Every wave spawns zombieCount number of enemies and waits spawnWait seconds between each zombie is spawned.


private float startWait = 3f;   //Amount of time to wait before first wave
private float waveWait = 2f;    //Amount of time to wait between spawning each wave
private float spawnWait = 0.5f; //Amount of time to wait between spawning each enemy

private float startTimer = 0f; //Keep track of time that has elapsed since game launch
private float waveTimer = 0f;  //Keep track of time that has elapsed since last wave
private float spawnTimer = 0f; //Keep track of time that has elapsed since last spawn

private List<GameObject> zombies = new List<GameObject>();

[SerializeField]
private Transform spawnPoints; //The parent object containing all of the spawn point objects


void Start()
{
    deathUI.SetActive(false);

    //Set the default number of zombies to spawn per wave
    zombieCount = 5;

    PopulateZombies();
}

void Update()
{
    startTimer += Time.deltaTime;
    if (startTimer >= startWait)
    {
        waveTimer += Time.deltaTime;
        spawnTimer += Time.deltaTime;

        SpawnWaves();
    }


    //When the player dies "pause" the game and bring up the Death screen
    if (Player.isDead == true)
    {
        Time.timeScale = 0;
        deathUI.SetActive(true);
    }

}

void SpawnWaves()
{
    while (!Player.isDead && ScoreCntl.wave < 9)
    {
        if (waveTimer >= waveWait)
        {
            IncrementWave();
            for (int i = 0; i < zombieCount; i++)
            {
                if (spawnTimer >= spawnWait)
                {
                    Vector3 spawnPosition = spawnPoints.GetChild(Random.Range(0, spawnPoints.childCount)).position;
                    Quaternion spawnRotation = Quaternion.identity;
                    GameObject created = Instantiate(zombies[0], spawnPosition, spawnRotation);
                    TransformCreated(created);

                    spawnTimer = 0f;
                }
            }

            waveTimer = 0f;
        }
    }
}

I am a beginner and understand my code may not follow best practice. I also have a working enemy spawner that uses Coroutine and yield returns, but would like to get this version of my enemy spawning working.

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
TechnoCat
  • 145
  • 2
  • 2
  • 7
  • changing timescale requires extreme care. make sure player is not dead before first update of the script – Bizhan Feb 05 '17 at 07:56
  • you can also use coroutine with `yield return new WaitForSecondsRealtime` in order for game to continue to run after changing timescale to zero – Bizhan Feb 05 '17 at 08:01
  • In my Player.cs script, I am checking if the player isDead via a method called in Update() and if they are dead I set isDead == true, which is then used by Gameplay.cs. In Player.cs isDead == false on Awake(). Currently the game will run right up until 4 seconds have elapsed, which is when the first wave should occur. Then it freezes. – TechnoCat Feb 05 '17 at 08:12

2 Answers2

2

I don't have any eperience with Unity but with XNA, i assume their main game processing to be similar with both using Update() and Draw() functions.

An Update() function is generally called a few times per second, in your code SpawnWaves() should be executed after every Update call once startTimer >= startWait.

So lets look at SpawnWaves()

void SpawnWaves()
{
    while (!Player.isDead && ScoreCntl.wave < 9)
    {
        if (waveTimer >= waveWait)
        {
            IncrementWave();
            for (int i = 0; i < zombieCount; i++)
            {
                if (spawnTimer >= spawnWait)
                {
                    [..]
                    spawnTimer = 0f;
                }
            }
            waveTimer = 0f;
        }
    }
}

This while loop is a problem, because it is a sort of game loop in a game loop. It will only exit, if the player is dead or your wave count is over nine.

After the call of SpawnWaves() there are two possibilities, depending on Time.deltaTime, which is more or less random after a program start, waveTimer can be bigger or smaller than waveWait.

  • Case: waveTimer >= waveWait

    All code inside will be executed without delay. So if its its possible to IncrementWave() it will be done nine times in milliseconds. Then while is false and code inside never executed again.

  • Case: waveTimer < waveWait

    In this case IncrementWave() will never be called and your game could spin endless in this while loop.

So what you have to do is basically replacing this while loop with an if statement, to allow the program to continue.

If(!Player.isDead && ScoreCntl.wave < 9)

From then on your counter incrementations will be called on Update() more than once.

waveTimer += Time.deltaTime;
spawnTimer += Time.deltaTime;

And your conditions for wave changing and spawning, can become true over time.

if (waveTimer >= waveWait),
if (spawnTimer >= spawnWait)

I hope this can guide you to fix your problems.

Update:

For this behaviour it's a good idea to seperate IncrementWave and spawn conditions. For this purpose you need an extra bool variable, called haveToSpawn in this example.

    if (waveTimer >= waveWait)
    {
      IncrementWave();
      waveTimer = 0f;
      haveToSpawn = true; //new variable
    }

    if (haveToSpawn && spawnTimer >= spawnWait)
    {
        for (int i = 0; i < zombieCount; i++)
        {
            [..] //spawn each zonbie
        }
        spawnTimer = 0f; //reset spawn delay
        haveToSpawn = false; //disallow spawing
    }
Florian p.i.
  • 622
  • 3
  • 7
  • Wow, that should have been incredibly obvious. I suspected the Update() was the problem and tried moving SpawnWaves() into Start(), but didn't think to simply switch out the while for an if. Thank you! However, there must be something wrong with my subsequent code, because no matter what I do it pretty much ignores (??) the for loop and spawns a single zombie, increments wave, and repeats. When it should spawn more zombies per wave. I even tried moving i++ into the loop itself, after a zombie definitely spawns, but that just makes Unity freeze at the point a zombie should spawn. – TechnoCat Feb 05 '17 at 17:10
  • 1
    I wrote a little upadte that should spawn all zombies. Hope it works. – Florian p.i. Feb 05 '17 at 17:54
  • Hilariously, it spawned a flood of zombies one right after another. The wave incrementation worked perfectly, but the spawnWait is supposed to occur between each single zombie spawns within a given wave. In this case it spawned zombieCount all at once instead of one by one and then waited waveWait seconds before unleashing all of zombieCount again. Which works really nicely for spawning all at once. – TechnoCat Feb 05 '17 at 19:17
  • 1
    In this case use a variable to save the number of spawned zombies and replace for(int i...) with something like if( zombieSpawned < zombieCount) and increase zombieSpawned each time. And to make sure haveToSpawn isn't reseted after the first, add an extra if statement like if(zombieSpawned == zombieCount){ haveToSpawn = false;}. Because the spawning time is now multiplied you should disable te waveTimer++ as long as haveToSpawn == true otherwise a new wave might be arriving too soon. Finally set zombieSpawned = 0; after haveToSpawn = true. This is everything i can do for you. – Florian p.i. Feb 05 '17 at 19:43
1

Use a coroutine to call the spawn waves method:

private float startWait = 3f;   //Amount of time to wait before first wave
private float waveWait = 2f;    //Amount of time to wait between spawning each wave
private float spawnWait = 0.5f; //Amount of time to wait between spawning each enemy


private float startTimer = 0f; //Keep track of time that has elapsed since game launch
private float waveTimer = 0f;  //Keep track of time that has elapsed since last wave
private float spawnTimer = 0f; //Keep track of time that has elapsed since last spawn

private List<GameObject> zombies = new List<GameObject>();

[SerializeField]
private Transform spawnPoints; //The parent object containing all of the spawn point objects


void Start()
{
    deathUI.SetActive(false);

    //Set the default number of zombies to spawn per wave
    zombieCount = 5;

    PopulateZombies();

    StartCoroutine(SpawnWaves());
}

void Update()
{
    startTimer += Time.deltaTime;
    if (startTimer >= startWait)
    {
        waveTimer += Time.deltaTime;
        spawnTimer += Time.deltaTime;
    }


    //When the player dies "pause" the game and bring up the Death screen
    if (Player.isDead == true)
    {
        Time.timeScale = 0;
        deathUI.SetActive(true);
    }

}

IEnumerator SpawnWaves()
{

    //wait 3 seconds
    yield return new WaitForSeconds(startWait);



    //then:

    while (!Player.isDead && ScoreCntl.wave < 9)
    {
        if (waveTimer >= waveWait)
        {
            IncrementWave();
            for (int i = 0; i < zombieCount; i++)
            {
                if (spawnTimer >= spawnWait)
                {
                    Vector3 spawnPosition = spawnPoints.GetChild(Random.Range(0, spawnPoints.childCount)).position;
                    Quaternion spawnRotation = Quaternion.identity;
                    GameObject created = Instantiate(zombies[0], spawnPosition, spawnRotation);
                    TransformCreated(created);

                    spawnTimer = 0f;
                }
            }

            waveTimer = 0f;
        }

        //wait until the end of frame
        yield return null;
    }
}

To have a better understanding of how unity coroutines work:

A coroutine is a method with return type of IEnumerator and behaves as a collection of code blocks which are executed asynchronously. The yield instruction separates code blocks and specifies the amount of time frames to wait before next code block starts executing.

There are several types of yield instructions:

  • null : waits until the frame ends (before rendering)
  • WaitForEndOfFrame : waits until the frames ends (after rendering)
  • WaitForSeconds : waits for specified seconds (timescale included)
  • WaitForSecondsRealtime : waits for specified seconds (timescale ignored)

You can think of

  • Update as a coroutine with yield return null
  • LateUpdate as a coroutine with yield return new WaitForEndOfFrame()
  • FixedUpdate as a coroutine with yield return new WaitForSeconds(.2f)
Bizhan
  • 16,157
  • 9
  • 63
  • 101
  • 1
    Thank you! I was avoiding using a Coroutine and multiple yield returns - as I've already gone that route - but I did not think to use a Coroutine combined with the timers (instead of all yield returns). This did work, aside from my subsequent code being wonky - which is another issue altogether I will work through. And thank you for the additional information regarding Coroutines and Updates. – TechnoCat Feb 05 '17 at 17:18