This tutorial is brought to you by Microsoft.
If you’re in a hurry, you can find the complete source code on GitHub.
It is recommended that you read part 1 before proceeding on reading the rest of this tutorial, since it is heavily based on what we described in the first part! Don’t tell me I didn’t warn you!!!
This is the second post in two post tutorials regarding making a tower defense game in Unity. In the first post we described the game scenario and gameplay and we also mentioned the level editor, the XML file and structure that holds level information and the object pooler methodology we use. Again, if you haven’t read it, I strongly encourage you to do it ASAP, this post will be much clearer after that!
We’ll start by exploring the prefabs we have in our game.
The RootGO gameobject
In the instructions we gave at the previous post about making a new level, we mentioned dragging a prefab called “RootGO” to the scene. Let’s see this object’s children in detail.
PathPieces is an empty game object that acts as a parent to the path sprites we will create, based on the details we’ll fetch from the XML file. If you’re wondering if it serves any other purpose other than the better organization of assets in the scene, then the answer is, pretty much, no.
Same as above, this object will hold waypoint game objects.
The Background game object is a parent to the various sprites that make up our background.
This object has various script components, extremely useful to our game.
We can see references to a GameManager script, a DragDropBunny script, an AudioManager script and an ObjectPoolerManager script. If you’ve read the previous post, you’ll recognize the ObjectPoolerManager script that will assist us with the arrows and audio objects creation.
The Bottom game object holds a BunnyGenerator which is a simple bunny sprite and a simple black background, which will act as a background to the game details that will be visible to the user (current round, lives available etc.). The bunny sprite will be dragged along the screen to create new protector bunnies. The BunnyGenerator is also referenced from the DragDropBunny script and the bunny sprite is referenced in the GameManager (check above screenshot).
The CarrotSpawner game object holds the CarrotSpawner script which has the duty to spawn carrots in the game scene, for our player to tap/click and gain carrot money.
The GUIText game object holds a GUIText component which displays game related info (lives left, money left, current round etc.).
The color changing background sprite
Here, we’d like to highlight an important UI aspect of the game. When the user drags a new bunny onto the scene, there are some areas that it should not be placed, e.g. onto the paths. In order to make this visible to the user, we are making the background sprite appear red (we’re ‘tinting’ it).
By checking each bg_tile (the game object that carries the background sprite – children of the Background game object), we can see that it has a “ColorTint” material that has a “Sprites/ColorTint” shader. This is a custom shader that we created with the help of this post in reddit. We downloaded the official Unity shaders, opened the Sprites-Default.shader and modified the line
fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
fixed4 c = tex2D(_MainTex, IN.texcoord) + IN.color;
In this way, when we assign a color to the material that the shader is applied on, the pixel’s color will have the assigned color added and not multiplied (as in the default shader). In this way, when we give the material a Red color, the material will get “more red” while preserving the initial color values. We also changed the shader name (first line of the shader script) to “Sprites/ColorTint”.
Finally, we created the ColorTint material that contains this shader and we applied it to each background sprite.
Before diving into the code, we’d like to briefly mention the prefabs to be used in the game.
Let’s take a look at the Arrow prefab.
The Arrow prefab is basically a game object with a RigidBody2D, a BoxCollider2D and an Arrow script. The SpriteRenderer is contained in a child game object and not in the parent one. You may wonder why this is happening. Answer is that the default arrow sprite points to the right, whereas we want the initial rotation for the arrow to be pointing to the top. So, we place the sprite game object into another game object and we assign a proper rotation to the sprite. We would follow the same strategy if we wanted to change the pivot point of a game object.
Things worth mentioning are also the Arrows Box collider (pictured above) and the fact that its rigidbody2D has a gravity scale of 0, so that it is not affected by gravity (it wouldn’t make sense for the arrow to fall down on the y-axis for this game). Finally, the Arrow is tagged as “Arrow”.
The Bunny prefab has a BoxCollider2D (to help us with its dragging – described later), follows the same strategy as the Arrow for its rotation (SpriteRenderer is contained in a child game object) and has the Bunny script which references the ArrowSpawnPosition, which is the location where the arrows will be shot from (pictured in the blue shape above).
The Carrot prefab is a simple one, having a sprite renderer, a BoxCollider2D component (to recognize user’s taps/clicks) and a Carrot script.
The enemy prefab follows the same strategy for its rotation like Arrow and Bunny. It also has a PolygonCollider2D component (boundaries illustrated in the above image) and the Enemy script.
The path game object has a sprite renderer and a BoxCollider2D component, in order to be visible in ray casting. We’ll describe this process later, but imagine that we need to prevent user from creating new bunnies on the path. We’ll use ray casting to determine that, later.
The tower game object has a sprite renderer and a CircleCollider2D component (again, for ray casting purposes).
Layers and sorting layers
Also, worth mentioning is the fact that all of the above sprites have been assigned to an appropriate sorting layer in order to determine visibility. Layer order shown below.
We have also created some layers for the game objects, to help with Physics collision (both for arrow shooting and ray casting purposes). Layers are shown below, it should be clear which objects they have been assigned to.
Only thing left in our tutorial is the source code review! Let’s visit the scripts, one by one. We’ll start by the easier and shorter ones, and we’ll finish with the mother of all, the GameManager.
The Arrow script is attached to the Arrow prefab. At Start, we invoke the Disable method, after 5 seconds. Disable method starts by cancelling its invocation (this’ll work if called from external scripts) and then sets the game object as inactive. This, due to the fact that we are pooling the Arrow from the list of already created arrows in our arrows object pooler (if you don’t have a clue about what I’m saying, check the first blog post).
The AudioManager script attached to our ScriptHolder gameobject is a singleton and holds a reference to two of the audio clips we want to play, specifically the sound of the shot arrows and the sound that the badgers make when they meet their creator. It exposes an Instance field that will return the AudioManager reference and has two public methods, the PlayArrowSound and the PlayDeathSound. Both call the PlaySound method via the StartCoroutine method. In short, the StartCoroutine can call methods that can pause their execution via calls to the yield new WaitForSeconds(duration) statement.
The PlaySound method returns an IEnumerator (since we want it to be callable from the StartCoroutine method). It fetches a game object from the AudioPooler instance, activates it, plays the audioclip passed via the clip variable, pauses the method’s execution for a duration equal to the sound’s duration and, at the end, deactivates the game object so it can be reused from the object pooler.
The Constants script is a static C# class that exposes some helper variables for our game. Their names should pretty much define their purpose.
The Bunny script, attached to each Bunny the user drags on the screen, is responsible for tracking the nearest enemy and shooting at it.
The Bunny script has a reference to the Arrow it will shoot and to the position that the arrows will be shot from (ArrowSpawnPosition). At start, the state of the bunny is inactive.
The Update method is split into two parts, the “search for an enemy” part and the “look and shoot at it” part.
On every Update call, we run a check to see if we have finished playing the final round of the level and if all the enemies are gone. If this is the case, then we’re not expecting any more enemies, so the state of the bunny is set to inactive.
When the Bunny is searching for an enemy, we use a LINQ query to find the enemy closest to us. If we find one and if it’s in the Bunny’s shooting range (defined by the Constants.MinDistanceForBunnyToShoot variable), then we transition to the Targeting state, copying the reference of the closest enemy to the targetedEnemy variable, i.e. so that the Bunny ‘locks’ at the specific enemy.
On the Targeting state, we check if the enemy has been destroyed (either by this or by another Bunny) and if it is still on the Bunny’s shooting range. If this is the case, then we proceed on looking towards the enemy and shooting at it. If the enemy has died or has escaped our shooting range, we return to the Searching state, to possibly find another enemy.
The LookAndShoot method uses the standard approach with LookRotation and RotateTowards methods to rotate towards the ‘locked’ enemy. We also make sure that the rotation is only on the z axis. In order to be able to shoot at the enemy, the direction of the vector representing the position difference between Bunny and locked enemy must be less than 20 degrees (we can’t shoot at an enemy with our back looking at it, right?). If this is the case, then we check if the Bunny is allowed to shoot (based on the ShootWaitTime variable and the LastShootTime). If this is the case, we make a copy of the time this shot takes place and we shoot at the direction of our enemy.
The Shoot method will get a copy of the Arrow prefab (by fetching it from the respective ObjectPooler), position it, rotate it, activate it and add a force to it, to dispatch it towards the enemy’s direction. We also perform a check to see if the enemy we’re locked onto has died or has escaped, to possibly transition our state to Searching.
Finally, the Bunny has an Activate method (that is called when we create the Bunny via the drag and drop mechanism) to start Searching for an enemy.
The Carrot script begins by getting a reference to the main Camera. Remember that carrots fall randomly from the top of the screen and when the user taps on them, she gains carrot money to build more Bunnies.
On each Update call, the carrot falls down at a specific speed and rotates on the z axis. User can tap on the carrot (we use thePhysics2D.OverlapPoint method to check that, ensuring that we check only the Carrot layer). If she taps on one, then we call a method on the GameManager instance to increase player’s money and we destroy the carrot object, since we no longer need it.
The CarrotSpawner class has the duty to drop Carrot game objects from the top of the screen. It has Start and Stop methods (called from the GameManager class) that invoke the coroutine called SpawnCarrots. This method:
- spawns a carrot prefab at a random point in the top of the screen
- sets it to a random fall speed
- waits for a random number of seconds between a minimum and a maximum interval via the new WaitForSeconds statement (remember that the MinCarrotSpawnTime and MaxCarrotSpawnTime are pulled from the XML file containing the level details)
The Enemy script is attached to our enemy badgers.
Script starts by getting the Health value from the Constants class. This will be the initial health amount of our enemy.
Remember that the enemies get created and follow a certain path, defined by the level’s waypoints, to reach their destination, which is the Bunny house.
Each enemy has a single waypoint in the screen that it is targeting at any given time, till it reaches the bunny house and get destroyed (since we no longer need it), removing one of the user’s lives. So, at each Update call, we check if the enemy has reached its current destination waypoint, by comparing the distance between the enemy’s position and the waypoint’s one. If it’s less than a small threshold (0.01 in this example), we check if this waypoint is the final one (i.e. the bunny house). If this is the case, then we remove the bunny and remove one player’s life. If not (there are more waypoints) then we set the current waypoint target to the next one. Plus, we make the enemy look at it by the use of the LookAt method. We use –Vector3.forward as this is the vector that points to the world “up” in our scene. In the end of the Update method, we call the MoveTowards method to move the enemy towards the desired waypoint.
The OnCollisionEnter2D method will occur when the enemy collides with another object. If it collides with an Arrow (which was obviously shot from a Bunny) then the enemy loses some health, determined by the ArrowDamage variable. If the enemy health drops to zero, the enemy is removed. In the end, we call the arrow’s Disable method (not Destroy, since we’re object pooling it).
The RemoveAndDestroy method is called when the enemy dies. It plays a death sound via the AudioManager, removes the current enemy from the enemy list on GameManager and raises the EnemyKilled event.
The DragDropBunny script is the last script we’ll review before the biggest script of the game, the GameManager. This script is responsible for the user’s ability to drag the bunny sprite located in the BunnyGenerator game object towards the game scene in order to create new bunnies. This, provided the user has enough carrot money.
The DragDropBunny script begins by getting a reference to the main camera. It also has a reference to the BunnyGenerator, the BunnyPrefab, a variable to indicate whether the user is dragging and a temporary game object to cache the background sprite behind the path, in order to color it red if the user cannot place the dragged bunny there.
The Update method is split into 3 parts. In the first part, we check if the user is not a