Objective: Introduce another wave of more dangerous enemies and pave the way to the creation of many others.
I’m writing this tutorial to explain how I tried to solve a coding challenge about this game: have more than one enemy wave.
If this game has to see the light, it obviously cannot have just one level!
The idea that came to my mind was to see each wave as a level and each level as a new scene in the game. In this way I could reuse many objects, implement some level-depending logic and many other features.
Each level is completed when a certain amount of enemies are killed by the player.
But let’s proceed with order.
Our logic will be hold by a script, attached to an empty game object.
This is a preview of what will be the inspector when we’re finished.
I added a bunch of public variables in the script. They’re meant to replace some values all over the project, such as speeds, spawning rates (min and max in the form of array), score values etc.
The idea is to grab those values from the LvlManager Script, each scene will have its Script with different values, so for example in level 2 enemies are quicker than those in level 1 and can shoot, also they spawn at a higher rate.
Right now, level 2 would just be a copy of level 1 with different values, but further levels may differ for the presence of other objects/Scripts, such as different kind of enemies and, if that presence is to be dynamic (i.e. spawning), it can be turned on and off through other variables in LvlManager.
An example? We can add a new prefab enemy, tell that it is available only starting from level (scene) 3 and manage its random spawning in the spawning manager.
This is one way of creating a wave effect and manage differences between waves.
An interesting part is the logic I put behind all this, namely the level completion and scene changing logics.
If we add a kill counter (just an int variable) in player, at each kill we can call this method from LvlManager Script:
Lines 5 to 7 check if the kill target is reached and then load next scene using a coroutine to wait a couple of seconds first, to void abrupt interruption of the current scene (believe me, it’s awful).
The first two lines are referring to another, powerful Unity instruments: the PlayerPrefs. PlayerPrefs class provides some very useful static methods that allow to store simple information (namely strings, floats and ints) that can be accessed globally at any time, even throughout the scenes.
How this is useful in this level management? Well, think of this scenario:
- Out of three lives, you have 1 left, meaning that at least one engine is on fire
- You have gained score by killing enemies and destroying asteroids
- You’re about to make the last kill before reaching the next level
The kill is done and a new scene is loaded, with everything inside. But it is totally new and the variables have their default values. But that’s not ok, you want to keep your score, you want to keep your lives count (no cheating here)… Do you get where I’m going here?
Using PlayerPrefs is one way to store that info! In particular, lines 3–4 of the previous snippet are storing a float with the “cursor position” of the music playback so I can resume music in the next scene without having to restart the song. But I assure you there are more useful usages, as you will see.
It is not enough to update the PlayerPrefs at each change (for example the score), you have to save them (to disk)! That is why when score changes the prefs are updated, but only when the kill counter goes on everything is saved: because there is the chance of a scene change and data must be saved.
On the other hand, data must be loaded on start in a new scene. Let’s look at the Start() method in the Player Script:
Set methods have a get counterpart. Those calls will retrieve the values whenever the Start method is executed i.e. at the beginning of each scene. You can also notice a second parameter, which is the default value to use in absence of the specific pref.
This leads to another problem: PlayerPrefs are persistent. They will hold the data even after application restart! So if I die or simply exit the game with 1k score and restart the game, I will find the same score!
In order to avoid this unwanted persistence, I made a PlayerPrefClear method that deletes the data. This method will be run when the game is closed but will also be included in the player death routine. If we win the level, instead, the get methods will carry on their task with success.
I want to finish this tutorial with another example: in level 2, I replaced the still wave asteroid with a moving one, I tweaked the LevelManager (I also implemented an enemy laser logic, check my GitHub). This is the result when the killing target is just 1 to pass from level 1 to level 2: