I want to make it so that the next scene loads after the waves are done but for some reason I can't figure out how to prevent it from happening early

Viewed 61

In the second level of my game I made a wave system with 3 different types of zombies. This wave system spawns in three different waves. My problem is that I cannot think of a way to start the next scene because whatever I try makes it start the next scene within 1 wave. I have tried using the bools that check if the waves have ran but the problem is that I have to have one of the true or else the second and third wave spawns together. I tried putting it in the spawner for the third wave but that didn't work. I tried adding ZKLeft.Length == 0 but that didn't work. Do you know any possible ways to prevent the next scene starting early without starting a wave early? Thanks!

Sorry if the code is bad I am a student

using System.Collections;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using UnityEngine;


public class Waves : Zombie
{    
    public GameObject behemoth;    
    public GameObject runner;    
    public GameObject zombie;    

    private bool HasSecondWaveRan = false;
    private bool HasThirdWaveRan = true;

    private int xPos;

    public GameObject[] ZKleft;

    // Start is called before the first frame update
    private void Start()
    {
        ZKleft = GameObject.FindGameObjectsWithTag("Zombie");

        for (int i = 0; i < 10; i++)
        {
            xPos = Random.Range(31, -14);            
            Instantiate(zombie, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
            
        }

        for (int i = 0; i < 3; i++)
        {
            xPos = Random.Range(31, -14);            
            Instantiate(runner, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
            
        }
        
    }

    // Update is called once per frame
    private void Update()
    {
        ZKleft = GameObject.FindGameObjectsWithTag("Zombie");        

        if (ZKleft.Length == 0)
        {
            StartCoroutine(SecondWave());
            HasSecondWaveRan = true;
        }

        if (ZKleft.Length == 0)
        {
            StartCoroutine(ThirdWave());
            HasThirdWaveRan = true;
        }

        if (HasThirdWaveRan == true && HasSecondWaveRan == true)
        {
            SceneManager.LoadScene("Level_2_Completed"); 
        }
    }

    IEnumerator SecondWave()
    {
        if (HasSecondWaveRan == false)
        {
            
            yield return new WaitForSeconds(5.0f);

            for (int i = 0; i < 5; i++)
            {
                xPos = Random.Range(31, -14);
                Instantiate(runner, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
            }

            for (int i = 0; i < 20; i++)
            {
                xPos = Random.Range(31, -14);
                Instantiate(zombie, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
            }

            HasThirdWaveRan = false;
            HasSecondWaveRan = true;            
        }
    }

    IEnumerator ThirdWave()
    {
        if (HasThirdWaveRan == false)
        {
            yield return new WaitForSeconds(5.0f);

            for (int i = 0; i < 5; i++)
            {
                xPos = Random.Range(31, -14);
                Instantiate(runner, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
            }

            for (int i = 0; i < 20; i++)
            {
                xPos = Random.Range(31, -14);
                Instantiate(zombie, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
            }

            for (int i = 0; i < 3; i++)
            {
                xPos = Random.Range(31, -14);
                Instantiate(behemoth, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
            }

            HasThirdWaveRan = true;
            
        }
    }
    
}

2 Answers

Update goes through all your if statements in just one frame. So when you set a Boolean to true, then executes the next if statement, thus changing all of them to true. You could try something like this:

   private bool HasSecondWaveRan = false;
   private bool HasThirdWaveRan = false;

   ...

   private void Update()
   {
       ZKleft = GameObject.FindGameObjectsWithTag("Zombie");

       if (ZKleft.Length == 0)
       {
           if (!HasSecondWaveRan && !HasSecondWaveRan)
           {
               StopCoroutine(FirstWave());
    //you don’t have a ‘FirstWave’, but I thought I
    //might include it. Remove it if you don’t want
    //a first wave.
               StartCoroutine(SecondWave());
               HasSecondWaveRan = true;
               return;
           }
           else if (HasSecondWaveRan && !HasThirdWaveRan)
           {
               StopCoroutine(SecondWave());
               StartCoroutine(ThirdWave());
               HasThirdWaveRan = true;
               return;
           }
           else if (HasSecondWaveRan && HasThirdWaveRan)
           {
               StopCoroutine(ThirdWave());
               SceneManager.LoadScene("Level_2_Completed"); 
           }
       }
   }

You can remove the if statements from the coroutine s that compare Has_WaveRan and false. I use ‘return’ to cancel the update function if the if statement ran. It should work if you just replace this code with your update function. If it doesn’t and/or you get errors, comment and describe the line the error it occurs and the error code. Change the coroutines to this:


    IEnumerator SecondWave()
    {
        for (int i = 0; i < 5; i++)
        {
            xPos = Random.Range(31, -14);
            Instantiate(runner, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
        }

        for (int i = 0; i < 20; i++)
        {
            xPos = Random.Range(31, -14);
            Instantiate(zombie, new Vector3(xPos, 0.4f, 6), Quaternion.identity);       
        }
        yield return new WaitForSeconds(5.0f);
    }

    IEnumerator ThirdWave()
    {
        for (int i = 0; i < 5; i++)
        {
            xPos = Random.Range(31, -14);
            Instantiate(runner, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
        }

        for (int i = 0; i < 20; i++)
        {
            xPos = Random.Range(31, -14);
            Instantiate(zombie, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
        }

        for (int i = 0; i < 3; i++)
        {
            xPos = Random.Range(31, -14);
            Instantiate(behemoth, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
        }
        yield return new WaitForSeconds(5.0f);
    }

Instead of waiting until a wave "has run" which in your current code actually only means "all objects are spawned and than we waited a fixed amount of time" I would rather wait until all enemy instances are actually killed.

To be way more flexible I would use one single wave spawn method and instead of hardcode the waves let the developer (you) create complete customizable waves vis the Inspector!

First of all I would add a dedicated script on all your zomby/enemy prefabs like e.g.

// For now does nothing than keeping track of all existing instances of this script
public class Enemy : MonoBehaviour
{
    // stores all instances of this component => all existing enemies in the scene
    private static readonly HashSet<Enemy> instances = new HashSet<Enemy>();

    // Readonly access property
    // returns a new HashSet so nobody on the outside can alternate the content
    public static HashSet<Enemy> Instances => new HashSet<Enemy>(instances);

    // For our script we only need the count which is cheaper then going through the "Instances" property
    public static int Count => instances.Count;

    private void Awake()
    {
        // Register yourself as soon as you come alive
        instances.Add(this);
    }

    private void OnDestroy ()
    {
        // Unregister yourself as soon as you were destryoed
        instances.Remove(this);
    }
}

Then I would use ScriptableObject. These are basically quite similar to prefabs instead you don't spawn them, they just exist. They are kind of profile assets but can also implement additional behavior.

// See https://docs.unity3d.com/ScriptReference/CreateAssetMenuAttribute.html
[CreateAssetMenu]
public class WaveProfile : ScriptableObject
{
    // Here you put any information a wave needs e.g.
    // an array of informations of which prefabs and how many of them to spawn
    public PrefabInfo[] PrefabInfos;

    // Depending on your needs you could have additional re-usable profiles
    // to determine when exactly this wave is allowed to BEGIN
    public ScriptableDelay Delay;
}

[Serializable]
public class PrefabInfo
{
    // which prefab to spawn
    public Enemy Prefab;
    // how many
    [Min(1)]public int Amount = 1;
}

// Inherit from this to create as many custom waiters as you need
public abstract class ScriptableDelay : ScriptableObject
{
    // every inheritor will implement its own wait condition(s)
    public abstract IEnumerator Wait();
}

and in separate scripts you could implement different conditions for when a wave may start e.g.

[CreateAssetMenu]
public class TimeDelay : ScriptableDelay
{
    // As the name suggest this one just uses a fixed time delay
    // before starting this wave

    // time in seconds
    public float time = 1f;

    public override IEnumerator Wait()
    {
        yield return new WaitForSeconds(time);
    }
}

and another example

[CreateAssetMenu]
public class AllEnemiesDeadDelay : ScriptableDelay
{
    // This one waits until all enemy instances are destroyed before
    // starting this wave

    public override IEnumerator Wait()
    {
        yield return new WaitWhile(() => Enemy.Count > 0);
    }
}

or for starting a wave immediately just leave the Delay empty. It is enough to create one asset per type since you can simply re-use them and reference the same delay asset in multiple places.

Then finally in your spawner script you can have multiple of such WaveProfiles, wait for the condition, spawn all prefabs and move on to the next wave, etc. Finally you can have an additional wait condition at the end after the last wave spawn to determine when exactly to load the next scene.

// Not sure why this inherits from Zombie?
public class Waves : Zombie
{    
    // Here you drag in all desired wave profiles for this scene
    public WaveProfile[] waveProfiles;

    // as before in the wave profile add a conditional for when to go to the next
    // scene after the last wave has spawned
    // most probbaly you would want to go for the "AllEnemiesDeadDelay"
    public ScriptableDelay afterLastWaveBeforeNextSceneDelay;

    // name of the scene to load after all waves have run and the "afterLastWaveBeforeNextSceneDelay"
    // has passed
    public string nextScene = "Level_2_Completed";

    // Yes! If you make "Start" return "IEnumerator" then Unity
    // automatically starts it as a Coroutine!
    private IEnumerator Start()
    {
        // iterate over all waves EXCEPT the last one
        // because for the last one we wait in a different way
        foreach(var profile in waveProfiles)
        {
            // Wait for the condition of this wave
            if(profile.Delay)
            {
                yield return profile.Delay.Wait();
            }

            // Spawn all the different enemiy prefabs of the current wave
            foreach(var prefabInfo in profile.PrefabInfos)
            {
                for(var i = 0; i < prefabInfo.Amount; i++)
                {
                    var xPos = Random.Range(31, -14);            
                    Instantiate(prefabInfo.Prefab, new Vector3(xPos, 0.4f, 6), Quaternion.identity);
                }
            }
        }
 
        // After the last have has spawned wait until all enemies are killed
        if(afterLastWaveBeforeNextSceneDelay)
        {
            afterLastWaveBeforeNextSceneDelay.Wait();
        }

        SceneManager.LoadScene(nextScene); 
    }
}
Related