Tower Defense Tutorial Fixing More Scene Timing Issues

August 24, 2019

Tower Defense Tutorial Project

Fixing Problems with the Scene Manager

A major issue I may have to deal with is updating the version of this Unity project. To clean out space on my computer, I wanted to condense the versions of Unity I was using on several projects so I removed one of the older 2018 versions I was using originally for this tutorial. I wanted to upgrade it to a 2019 version, so I just used the most recent one I had. I don’t believe this should have very major impacts on this project, but it is worth noting for any extra strange behavior or errors.

First things first, I need to do a bit of play testing to get a feel for the current errors and go over the main scripts (mostly those dealing with scene management) to see where to start. The opening scenes seem to work well (the initial main scene into the level select menu). Selecting level one works ok. Any other level leads to weird errors (not properly closing other scenes so lights add up and UI elements stack on top of each other), which is expected since my work focused on getting level 1 to work first. Retrying while a wave is spawning leads to some interesting behavior (it spawns the second wave instead of the first when the level restarts).

To make it easier to keep track of all the different scripts, I started organizing the scripts folder hierarchy. This began with simply creating any hierarchy at all, as initially I just had all of the scripts in the scripts base folder. While it may be a bit risky since I do move things around from time to time at this stage, I decided it would make sense to make a folder for each scene and organize the scripts by scene basically at the first level. This would help me see which scripts are running in which scenes simply by checking the folders. And when I am in a certain scene, I can just open its corresponding scripts folder to see what all is available.

This was actually a very straight forward way of placing a lot of the scripts. Many of them pretty directly only exist in one scene that makes sense. Some of the more instantiated object related scripts (like for the turrets and the enemies) pretty clearly made sense to just put in the Level scene folder since that’s where they should be instantiated so they were easy to place as well.

PROBLEMS

PROBLEM #1

Restarting a level while the first wave is still spawning will have the second wave spawn first on restart.

This is most likely an error that happens because restarting while the coroutine is running for spawning a wave has the value reset that happens when restarting overridden by the coroutine then ending. Checking the WaveSpawner script affirms my assumption since it has a method called ResetWaveSpawner, which sets the waveIndex to 0 (the first wave) but the coroutine spawning the wave, called SpawnWave, increases the waveIndex by 1 once it is finished running. The coroutine is most likely “finishing” after the reset is happening, so the index gets pushed to 1 immediately (insted of being at 0 as it should).

Test #1:

Add a check to ResetWaveSpawner to see if the SpawnWave coroutine is running and stop it before resetting.

This is a perfect opportunity to try the new way I learned to control the flow of coroutines. Instead of using an outside bool variable that is set to true when the coroutine starts, false when it ends, and checking later if that bool is true/false to stop the coroutine, you can control an IEnumerator reference. You first create an IEnumerator reference variable. You then set this equal to the coroutine you want to start right before starting it, then Start this reference variable as your coroutine. When you want to check if it’s running or not later to determine if it needs stopped, you can just do a check to see if this reference variable is not null and then stop the coroutine.

SOLUTION: This worked perfectly. Restarting at anytime (even immediately when the wave starts) has the proper wave spawn. This also stopped the case of enemies continuing to spawn (which was the same issue of the coroutine running into the next scene start, it was just a longer coroutine in that error).

PROBLEM #2

The level does not end when the player defeats the final wave.

The LevelManager script that ultimately starts the “winning process” has a lot of conditions to start this process. The if statement checking in Update to see if the player has won has 4 separate conditions, so there is a high chance one of these is not acting as it should. This is a good place to start looking for why this error occurs. The conditions are as follows:
if (waveSpawnerReference.isActiveAndEnabled && waveSpawnerReference.WavesFinished() && waveSpawnerReference.enemiesAlive == 0 && GameIsOver == false)
{
WinLevel();
}

waveSpawnerReference.WavesFinished() is a method that does a bool check in the WaveSpawner script, so my first thought was to check there and make sure it was being set properly. I simply tested this condition with GameIsOver == false to prompt winning to see what this did. This resulted in the player winning immediately on the level starting. I then remembered, this is a scene loading timing issue reflected in the WavesFinished method check. This method is:
public bool WavesFinished()
{
if(waveIndex == waves.Length)
{
wavesFinished = true;
}
return wavesFinished;
}
The reason this checks as true immediately is because the method gets called before any of the values are set, so the waveIndex is 0 by default (which is accurate) but the waves.Length, which should be the count of how many waves are in the level, is also defaulted to 0 before being set. This results in this being true at frame one.

Looking at the WaveSpawner, I see why these conditions are not being met simultaneously. The WaveSpawner has an Update check to see if the waveIndex == waves.Length to indicate that the last wave has been spawned, and if this check is true, if disables itself. So the condition including a check for hitting the end of the waves AND seeing if the WaveSpawner is enabled will never be true simultaneously.

Test #1:

Create a DeactivateWaveSpawner method which will allow me to deactivate the spawner with the LevelManager AFTER performing the check to see if it’s active for winning (and just after calling the losing method for continuity).

I also had to tweak the WaveSpawner script a bit since it was in charge of deactivating itself as soon as it spawned the final wave. I wanted it to stay active until the player won or lost, and then have those methods deactivate it. This method however gave me very interesting errors. It still was having the player win immediately the first time I selected level 1, but every subsequent time I selected level 1 from the level select menu, it worked perfectly fine. It started properly and allowed the player to win correctly.

Once again, this is an annoying timing issue. The WaveSpawner actually starts enabled in when starting the game from scratch in the Logic scene. The WaveInformation script in the tested Level scene contains the following:
private void Start()
{
waveSpawnerReference = WaveSpawner.GetInstance();

// Reset the wave spawner each time a level loads
waveSpawnerReference.resetWaveSpawner();

PassWaveInformation();
waveSpawnerReference.enabled = true;
}
What’s happening is that the necessary values are being passed to the WaveSpawner through the PassWaveInformation method and the game is immediately ending with a win and then deactivating the WaveSpawner. Now on returing to level 1, the values needed are already set, and these statements before the final statement which enables the WaveSpawner are fed into nothing (since the WaveSpawner is disabled at that time). Now however, anything that checks the values of WaveSpawner will be checking the correct values as intended since they were set way back when the level was previously started. This is removing any frame perfect timing check errors since everything is set before the WaveSpawner is even turned on, but only when starting the level after the first run.

Test #2:

Edit the conditions in the WavesFinished method in the WaveSpawner to be more accurate.

SOLUTION: The general idea is that this is basically returning a false positive check by immediately checking the default value to meet its only condition, so I am just going to add another check to make sure a default case that doesn’t make sense to ever happen anyway isn’t what one of the current values is set to. So basically I changed the condition from:
if(waveIndex == waves.Length)
to
if(waves.Length != 0 && waves.Length)
No level should ever have 0 waves in it anyway, so this check is safe for my game environment and removes the false positive check with the default values where everything is initially at 0.

Using this method worked perfectly, and may also serve as a useful secondary check regardless to reduce the chances of future wave spawning bugs.

PROBLEM #3

The spawn coroutine is still giving some errors in some scene change circumstances.

The main issue with the spawn coroutine was happening when retrying a level, so I specifically fixed a method which was called at the start of loading a level scene so that it would stop the spawn coroutine. However, Unity still doesn’t like that this coroutine continues in other scene swap scenarios (like going back to the menu). It isn’t a game breaking error, but it still shows up as an error in the logs as the game wants to complete the coroutine but cannot because certain objects are not gone (or destroyed according to Unity). This is not a big issue, but I thought it was an interesting enough problem to record as I basically want a consistent way to stop this coroutine when the scenes change.

My first thought was to have a StopCoroutine call in an OnDisable method since that’s a pretty general practice, but unfortunately that does not work here. It would be in the WaveSpawner script which always exists since it’s part of the logic scene. On second thought, this should work because the WaveSpawner should be disabled anytime we leave the Level and Base scene anyway. Fixing that may lead to a nice solution for fixing this error.

Test #1:

Use an OnDisable method in WaveSpawner to stop the SpawnWave coroutine, and follow this by making sure anytime we leave the Level/Base scene we disable the WaveSpawner.

SOLUTION: I looked to the LevelManager and OverlayCanvasManager in the Base scene as these together deal with all of the UI interactions that lead to leaving the current Level and Base scene combination. The main issue was that using Retry and Menu were not deactivating the WaveSpawner, so I looked to add this deactivation into the OverlayCanvasManager where these button functionalities reside.

Since LevelManager already has a reference to WaveSpawner and was deactivating it, I actually just made the WaveSpawner deactivation into a public method, DeactivateWaveSpawner, which called the WaveSpawner’s DeactivateWaveSpawner method, and gave the OverlayCanvasManager a reference to the LevelManager so it could just use this method. I’m not sure if this is better, but it seemed silly to have two seperate references to the same object for the same reason when these two scripts would always be on the same core object anyway.

I then noticed that all my small methods for switching scenes (Retry, Menu, NextLevel) all called the ResetOverlayUI method, which just sets all the UI elements in the level to inactive before switching scenes just in case they would stick around for any reason. Since they all called this method, I just added the DeactivateWaveSpawner reference here, to the ResetOverlayUI method. Now they all deactivate the WaveSpawner before shifting scenes.

This appeared to work, as the error stopped coming up. I even tested with a very large amount of enemies in one wave just to ensure it should still be running after switching scenes had it not been actively stopped. This also meant I could remove stopping the coroutine from the waveSpawnReset method in WaveSpawner without any issue. This was the method referenced by WaveInformation I initially used to solve the weird double spawning issue when restarting a level. This problem is now more cleanly solved as well.

NEXT STEP

Everything seems to work cleanly with Level 1, so the next step is just making sure any level works.