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.
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.).
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).
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:
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 already dragging a new bunny and if she has enough money to create one. If the user taps into the bunny generator, we set the isDragging variable to true and create a new bunny at the BunnyGenerator’s position. We also revert the color on any background sprites that had their color changed (if the user could not place a new bunny on top of them => explained in the next paragraph!).
In the second part, we check if the user is dragging. If this is the case, we move the bunny to the position that the user’s finger (or mouse pointer) is at. Then we’re checking if the new bunny is on top of a Path, the Tower (the bunny house) or on top of another bunny (we use Count() > 1 in this case since the user’s finger is already on top of 1 bunny, the one we’re dragging!). We’re using the RaycastAll method to determine this. Of course, we cannot place the new bunny on the Tower, on the Path (since the enemies are walking on it) and we can’t have two bunnies at the same position.
So, if the user attempts to drag the newly created bunny at a forbidden location, we should indicate that to the user. If you remember, we will five a red tint to the background tile located in the forbidden location. We get a reference to the Background tile, get a reference to its SpriteRenderer component, give it a RedColor color and cache the specific sprite in order to revert later (via the ResetTempBackgroundColor method). However, if the user attempts to move the bunny to an allowed location, we just revert the color on previously set background sprites.
In the final part of the Update method, this is where the user lifts the finger (or the mouse button) and she’s carrying a new bunny. We run a check (via the RayCastAll method) if the location is an allowed one (same technique as before). If we can leave the bunny on this location, we modify user’s money subtracting the bunny’s cost and activate the bunny in order to start searching for enemies. If we cannot leave a bunny at this location, we destroy the one we were dragging. In both cases, we set the isDragging flag to false, so that user can drag a new bunny on the level.
The ResetBackgroundColor method just takes the cached background sprite material and reverts its color to black (so the sprite itself appears with its original color).
Hope you’re still here with me, we’re almost over! We’ll review the final MonoBehaviour in our game, the GameManager class. This class is in charge of the whole level management and our central game state machine.
Since, as you probably have discovered by now, we’re referring to this class from all over our game, we implemented it as a singleton.
We define a lot of helper fields in this class for our game. Here you can see that we’re having an Enemy list, prefabs about Enemy, Path and Tower and we have helper fields for the Waypoints and their parent and fields to help with the stuff we pull from the current level’s XML file plus the CarrotSpawner.
We also have fields for the available money the player has, fields for carrot spawn times, current lives, current round index and current state. We also have a reference to the BunnyGenerator sprite, a flag to determine whether we have finished playing the final round of the level plus a GUITextreference that displays the necessary stuff that the player can see (you could call it a mini HUD).
The Start method calls the IgnoreLayerCollision method (described below), initializes the enemies list and finds the PathPieces and Waypointsgameobjects in the scene. It goes on by fetching the details of the XML file and calls the CreateLevelFromXML method to, well, create the level from the stuff contained in the XML file. Finally, it begins the game by setting the corresponding state.
The CreateLevelFromXML method creates the level from the XML file details. It begins by creating the paths (also setting their parent and the sorting layer), it goes on by creating the waypoints and the tower and finishes by getting the initial money and the carrot spawn times.
The IgnoreLayerCollisions method will make the Arrow collide only with the Enemy. Why is this? Since we have many colliders on the scene (to help with mouse taps/clicks and Raycasts), an arrow could collide with all of them. Since we need it to collide only with the Enemies, we explicitly tell the Physics engine to ignore all other collisions of the arrow with existing objects. Plus, we eliminate the collisions of the Bunny and the Enemy objects. Needless to say, the above method works since we have set a layer to all relevant prefabs/gameobjects. It’s important to mention that, instead of using code, we could also use the Layer Collision Matrix Unity editor kindly provides us with.
The NextRound method is called when the next round is about to start. At first, it waits for 2 seconds, so we give the player some time to place any new bunnies on the level. Then, it fetches the round details from the loaded XML stuff. For each enemy, it instantiates a new enemy prefab, sets the enemy’s speed (subsequent rounds will incur in a bigger speed) and sets a handler for the EnemyKilled event. Finally, it adds the newly created enemy to the enemy list and waits for a number of seconds. The bigger the round index, the less time the method will wait before spawning a new enemy.
The OnEnemyKilled method is called when an enemy dies. It checks if there are other enemies on the scene. If not, then it calls theCheckAndStartNewRound method.
The CheckAndStartNewRound method, as we just saw, is called when all the enemies on the screen have died. The method checks if there are any other rounds in the details loaded from the XML file. If there are, then it calls the NextRound coroutine. Else, it sets the FinalRoundFinished flag to true.
If the game state is either Won or Lost, then a single tap or click will restart the level. In a normal (production) game, the Won state could navigate you to a next level, a reward screen etc. A Lost state, on the other hand, could show you a friendly UI to buy potential In App Purchases, e.g. more initial money for the level.
The DestroyExistingEnemiesAndCarrots method finds all the enemies (via the list we have) and carrots (via their Tag) and removes them from the screen.
The AlterMoneyAvailable method adds or subtracts money for the user. It will add money if the user has tapped/clicked on a carrot and will subtract money if a user creates a new bunny. As a bonus, it will make the BunnyGenerator sprite slightly transparent if the user does not have enough money to create another bunny.
The OnGUI method just displays useful information to the user (the HUD we described) based on the current fields and game state of the GameManager class. It uses the old and classic GUIText method. If you’re looking for a more attractive UI, then Unity 4.6 and onwards is your friend! In fact, Unity 4.6 Beta has been released in public, so I highly encourage you to download and test it.
If you’ve been reading this far, congratulations! I hope you liked this tutorial, feel free to use code you found (either as a whole or pieces of it). The game can be extended in numerous ways, e.g. adding an option to sell the bunnies you create, making the enemies damage the bunnies, adding more types of protectors to help the bunnies, adding more types of enemies and adding more levels (of course). Go ahead and try it! If you find anything that doesn’t work or could be implemented in a better way, don’t hesitate to sound off in the comments. As always, you can find the complete source code for all my tutorials on GitHub. CU next time!
If you are new to Unity, check out a cool intro video series here. For instructions on how to deploy your existing game onto Windows Store/Phone, check out the free Microsoft Virtual Academy video here: http://www.microsoftvirtualacademy.com/training-courses/porting-unity-games-to-windows-store-and-windows-phone