Creating a Spawn Manager for AI Characters in Unity 2021
I’ll be focusing on Object Pooling and Wave System mechanics in this article through my Singleton SpawnManager class.
Overview of the Spawn_Manager Game Object
The Spawn_Manager was created as an Empty Game Object before I added a script and a box collider to it.
The box collider is necessary to determine when the Robot Ais have reached the end of the path in my scene.
Make sure isTrigger is enabled.
I’ll cover the script itself in a little bit, but I think it will be easier to go over the class array variables as seen in the Inspector.
For prototypes, I prefer to serialize anything that I even *might* want to see.
This really helps diagnose issues with my code.
The script variables start with some Vector3 values that will be used to reset the Robot game object’s position.
You can do this here on the SpawnManager or on the RobotAI script, either way works.
I also have set the Spawn_Manager game object to be my Parent game object for all spawned game objects (robots).
To spawn a prefab, I need a reference to it so we have that next.
To avoid OutOfIndex errors with our arrays or lists, we need to keep track of everything in our pool.
So, I’m tracking the total number of prefabs in the pool, how many pooled game objects are left as I go down the list enabling (spawning) each one, and what the next pooled game object I will use is by index value.
The next section covers the Attack Waves that will be generated using the Object Pooling system.
This is especially nice for Game Designers to tweak values for gameplay purposes without having to touch the code.
I keep track of the number of waves, the current wave attacking, when the current wave started, what the current time is for comparison, and the Min/Max values how many game objects (Robots) will spawn per wave as well as how long each wave will last before the next wave is triggered.
This is followed by the arrays or lists that will hold all of this information for ease of reference.
Please forgive the red, I find tinting the editor during play mode to be very helpful.
You can see that with the values and game object references updating in real time during play mode, I can really drill down on what is going on with SpawnManager’s state.
The MonoSingleton Class / Script
I’m not going to dive into this, but I just wanted to make it clear what my SpawnManager class is inheriting from.
This is a utility script that makes creating new singleton classes very easy rather than recoding the singleton pattern over and over.
The SpawnManager Class / Script
Here we have some pretty straight-forward using statements, though I think Rider mistakenly added the VisualScripting statement.
The class inherits from MonoSingleton which is itself also a monobehavior.
Then I create a Struct to organize my Wave System’s data easier.
Each variable of “Type Wave” will have id, length, spawnAmount properties.
The id signifying which wave, the length signifies duration (probably should have called it duration!), and the spawnAmount sets how many game objects to spawn when this Wave is running.
From there, we can see the class variables I’ve covered from the Editor screen and a _spawnWaves array declaration.
OnEnable() & Start() Methods
We do some basic initialization in these methods.
Creating arrays with the correct length and adding default values to others.
The Start() method first calls the DoNullChecks() method.
I like to separate out the null checking from the other initialization code in Start() since it can be space consuming.
Variables that are null but should not be, when possible, are assigned default values.
This is noted in the console log of the Unity Editor.
Start() Does Nearly Everything
We’ll try to run through this code in the order it executes as much as possible.
After null checking, the next item on Start() method’s list is populate the _spawnedPoolGOs (GO stands for Game Objects) list variable.
This could also be done with an array, but I’m trying to stretch myself a bit to broaden my toolset.
The GeneratePoolGOs() method returns a List of Type Game Object to the _spawnedPoolGOs = GeneratePoolGOs(_amountOfGOsInPool) line of the Start() method.
By iterating through a simple for loop, we instantiate the desired number of game objects using the designated prefab.
We also disable them.
The next method called from Start() goes through the newly generated list of instantiated game objects in order to reposition and set the rotation of each one to the default values.
This position and rotation is the starting waypoint with the Robot game object facing the doorway.
After the Start() method sets _currWave to 0, it calls the GenerateWaves() method.
I have taken a randomized approach to the length (duration) and the number of Robots that will spawn in each wave.
By simply iterating for the specified number of waves, Random.Range assigns a random number in a range from the min variable value to the max variable value.
These are then assigned to a couple of stand-in arrays.
Since I cannot expose the _spawnWaves array to the Unity Editor, I simply duplicate the data to the _waveLengths and _amountForWave arrays.
This makes it easy for me to see the exact values in use during play mode.
Note that “Wave wavesArray” is proceeded by “ref”, meaning that it is not a copy of an array but a reference to an existing array.
This means that assignments to “wavesArray” in the GenerateWaves() Method will assign directly to the _spawnWaves array variable.
We could have also just directly accessed this variable in GenerateWaves(), but this way is more robust.
It allows us to pass in any number of Wave arrays which could be useful if this game became more complex than the prototype we are creating.
The StartWaves() Coroutine
The last thing our Start() method calls is the StartWaves() coroutine which is really where the magic begins for our wave system.
This method calls on the EnableWaveGOs() coroutine to enable the pooled game objects (Robots) in a staggered manner, rather than all at once.
Again, I’ve used Random.Range.
Random.Range only allows integer values and this really isn’t making the animation of my Robots running look good.
They look like they are copy pasted.
Using divide by 2 on the resulting random at least adds half seconds to the to the delaySpawn variable, which helps a lot!
The StartWaves() coroutine is being used in a recursive fashion.
So long as the current wave is less than the total number of waves, this coroutine calls itself.
In this way we can condense our code while taking full advantage of the WaitForSeconds() call to implement our wave length (duration) to separate waves.
We also make sure to check if there are less remaining pooled game objects than the maximum possible of game objects used in a wave.
As shown above, our Spawn_Manager game object is the parent game object for our pooled game objects (Robots).
As shown above during play mode, we can see the key values that will trigger the reset from moving down the list of pooled game objects back to the first pooled game object.
So long as “Num Of Remaining GOs In Pool” > “Max Spawns Per Wave”, it will continue moving down the list of pooled game objects.
If this is the case, we reset the identifier for the next game object in our pool to be activated to the very first game object in the pool itself.
You may want to maximize the gif above, but suffice it to say, once the threshold for resetting the pool is hit, the first game object in the pool is enabled rather than continuing down the list and potentially running into an IndexOutOfBounds error.