A Tower Defense game in Unity, part 2

If you’re in a hurry, you can find the complete source code on GitHub: https://github.com/dgkanatsios/TowerDefense. You can try the game here in WebGL.

It is recommended that you read part 1 before proceeding on reading the rest of this tutorial. On the first part, we explained the way game levels are saved/loaded from XML files, as well as the Object Pooling technique. In this part, we will deal with the core Tower Defense game mechanics.

This is the second post in 2 post tutorial regarding making a tower defense 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.

image_112F268B

PathPieces

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.

Waypoints

Same as above, this object will hold waypoint game objects.

Background

The Background game object is a parent to the various sprites that make up our background.

image_7CAC67BD

ScriptHolder

This object has various script components, extremely useful to our game.

image_62D8318E

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.

Bottom-BunnyGenerator

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).

CarrotSpanwner

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.

GUIText

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).

image_223875D0.png

image_2ECB444E

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;

to write

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”.

image_49976A5A.png

Finally, we created the ColorTint material that contains this shader and we applied it to each background sprite.

Our prefabs

Before diving into the code, we’d like to briefly mention the prefabs to be used in the game.

Arrow

Let’s take a loot at the Arrow prefab.

image_2FC5C4DCimage_74DE9212

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.

image_28A91915

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”.

Bunny

image_53840DD8image_4DCB84E3image_41C994AF

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).

Carrot

image_28642226

The Carrot prefab is a simple one, having a sprite renderer, a BoxCollider2D component (to recognize user’s taps/clicks) and a Carrot script.

Enemy

image_77647178image_356092E8.pngimage_33B4E876.png

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.

Path

image_310EBD76image_4934B882

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.

Tower

image_0D77B080image_3A8F2DFF

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.

image_17CDF642

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.

image_79833F4B

Source code

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.

Arrow

public class Arrow : MonoBehaviour {

	// Use this for initialization
	void Start () {
		//disable it after 5 seconds, whatever happens
		Invoke("Disable", 5f);
	}

	public void Disable()
	{
		//if we are called from another gameobject,
		//cancel the timed invoke
		CancelInvoke();
		//since we're pooling it, make it inactive instead of destroying it
		this.gameObject.SetActive(false);
	}

}

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).

AudioManager

public class AudioManager : MonoBehaviour {

    public AudioClip ArrowAudioClip, DeathSoundAudioClip;

    ///
<summary>
    /// Basic singleton implementation
    /// </summary>

    public static AudioManager Instance { get; private set; }

    void Awake()
    {
        Instance = this;
    }

    public void PlayArrowSound()
    {
        StartCoroutine(PlaySound(ArrowAudioClip));
    }

    public void PlayDeathSound()
    {
        StartCoroutine(PlaySound(DeathSoundAudioClip));
    }

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.

    private IEnumerator PlaySound(AudioClip clip)
    {
        //get an object from the pooler, activate it, play the sound
        //wait for sound completion and then deactivate the object
        GameObject go = ObjectPoolerManager.Instance.AudioPooler.GetPooledObject();
        go.SetActive(true);
        go.GetComponent<AudioSource>().PlayOneShot(clip);
        yield return new WaitForSeconds(clip.length);
        go.SetActive(false);
    }

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.

Constants

    public static class Constants
    {
        public static readonly Color RedColor = new Color(1f, 0f, 0f, 0f);
        public static readonly Color BlackColor = new Color(0f, 0f, 0f, 0f);
        public static readonly int BunnyCost = 50;
        public static readonly int CarrotAward = 10;
        public static readonly int InitialEnemyHealth = 50;
        public static readonly int ArrowDamage = 20;
        public static readonly float MinDistanceForBunnyToShoot = 3f;

    }

The Constants script is a static C# class that exposes some helper variables for our game. Their names should pretty much define their purpose.

Bunny

The Bunny script, attached to each Bunny the user drags on the screen, is responsible for tracking the nearest enemy and shooting at it.

public class Bunny : MonoBehaviour
{

    //arrow sound found here
    //https://www.freesound.org/people/Erdie/sounds/65734/

    public Transform ArrowSpawnPosition;
    public GameObject ArrowPrefab;
    public float ShootWaitTime = 2f;
    private float LastShootTime = 0f;
    GameObject targetedEnemy;
    private float InitialArrowForce = 500f;

    // Use this for initialization
    void Start()
    {
        State = BunnyState.Inactive;
        //find where we're shooting from
        ArrowSpawnPosition = transform.FindChild("ArrowSpawnPosition");
    }

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.

    void Update()
    {
        //if we're in the last round and we've killed all enemies, do nothing
        if (GameManager.Instance.FinalRoundFinished &&
            GameManager.Instance.Enemies.Where(x => x != null).Count() == 0)
            State = BunnyState.Inactive;

        //searching for an enemy
        if (State == BunnyState.Searching)
        {
            if (GameManager.Instance.Enemies.Where(x => x != null).Count() == 0) return;

            //find the closest enemy
            //aggregate method proposed here
            //http://unitygems.com/linq-1-time-linq/
            targetedEnemy = GameManager.Instance.Enemies.Where(x => x != null)
           .Aggregate((current, next) => Vector2.Distance(current.transform.position, transform.position)
               < Vector2.Distance(next.transform.position, transform.position)
              ? current : next);

            //if there is an enemy and is close to us, target it
            if (targetedEnemy != null && targetedEnemy.activeSelf
                && Vector3.Distance(transform.position, targetedEnemy.transform.position)
                < Constants.MinDistanceForBunnyToShoot)
            {
                State = BunnyState.Targeting;
            }

        }

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.

        else if (State == BunnyState.Targeting)
        {
            //if the targeted enemy is still close to us, look at it and shoot!
            if (targetedEnemy != null
                && Vector3.Distance(transform.position, targetedEnemy.transform.position)
                    < Constants.MinDistanceForBunnyToShoot)
            {
                LookAndShoot();
            }
            else //enemy has left our shooting range, so look for another one
            {
                State = BunnyState.Searching;
            }
        }
    }

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.

    private void LookAndShoot()
    {
        //look at the enemy
        Quaternion diffRotation = Quaternion.LookRotation
            (transform.position - targetedEnemy.transform.position, Vector3.forward);
        transform.rotation = Quaternion.RotateTowards
            (transform.rotation, diffRotation, Time.deltaTime * 2000);
        transform.eulerAngles = new Vector3(0, 0, transform.eulerAngles.z);

        //make sure we're almost looking at the enemy before start shooting
        Vector2 direction = targetedEnemy.transform.position - transform.position;
        float axisDif = Vector2.Angle(transform.up, direction);
        //shoot only if we have 20 degrees rotation difference to the enemy
        if (axisDif <= 20f)         {             if (Time.time - LastShootTime > ShootWaitTime)
            {
                Shoot(direction);
                LastShootTime = Time.time;
            }

        }
    }

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.

    private void Shoot(Vector2 dir)
    {
        //if the enemy is still close to us
        if (targetedEnemy != null && targetedEnemy.activeSelf
            && Vector3.Distance(transform.position, targetedEnemy.transform.position)
                    < Constants.MinDistanceForBunnyToShoot)
        {
            //create a new arrow
            GameObject go = ObjectPoolerManager.Instance.ArrowPooler.GetPooledObject();
            go.transform.position = ArrowSpawnPosition.position;
            go.transform.rotation = transform.rotation;
            go.SetActive(true);
            //SHOOT IT!
            go.GetComponent<Rigidbody2D>().AddForce(dir * InitialArrowForce);
            AudioManager.Instance.PlayArrowSound();
        }
        else//find another enemy
        {
            State = BunnyState.Searching;
        }

    }

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.

    private BunnyState State;

    public void Activate()
    {
        State = BunnyState.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.

Carrot

public class Carrot : MonoBehaviour
{

    //carrot sprite found in http://opengameart.org/content/easter-carrot-pick-up-item

    Camera mainCamera;

    // Use this for initialization
    void Start()
    {
        mainCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        transform.position = new Vector3(
            transform.position.x,
            transform.position.y - Time.deltaTime * FallSpeed,
            transform.position.z);
        transform.Rotate(0, 0, Time.deltaTime * 10);

        if (Input.GetMouseButtonDown(0))
        {
            //check if the user tap/click touches the carrot
            Vector2 location = mainCamera.ScreenToWorldPoint(Input.mousePosition);

            if (this.GetComponent<BoxCollider2D>() == Physics2D.OverlapPoint(location,
                1 << LayerMask.NameToLayer("Carrot")))
            {
                //increase player money
                GameManager.Instance.AlterMoneyAvailable(Constants.CarrotAward);
                //destroy carrot
                Destroy(this.gameObject);
            }
        }
    }

    public float FallSpeed = 1;
}

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 the Physics2D.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.

CarrotSpawner

public class CarrotSpawner : MonoBehaviour {

	///
<summary>
	/// Carrot prefab
	/// </summary>

	public GameObject Carrot;

	public void StartCarrotSpawning()
	{
		StartCoroutine(SpawnCarrots());
	}

	public void StopCarrotSpawning()
	{
		StopAllCoroutines();
	}
	private IEnumerator SpawnCarrots()
	{
		while (true)
		{
			//select a random position
			float X = Random.Range(100, Screen.width - 100);
			Vector3 randomPosition = Camera.main.ScreenToWorldPoint(new Vector3(X, 0, 0));
			//create and drop a carrot
			GameObject carrot = Instantiate(Carrot,
				new Vector3(randomPosition.x, transform.position.y, transform.position.z),
				Quaternion.identity) as GameObject;
			carrot.GetComponent<Carrot>().FallSpeed = Random.Range(1f, 3f);
			//wait for random seconds, based on level parameters
			yield return new WaitForSeconds
				(Random.Range(GameManager.Instance.MinCarrotSpawnTime,
				GameManager.Instance.MaxCarrotSpawnTime));
		}
	}

}

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)

Enemy

The Enemy script is attached to our enemy badgers.

public class Enemy : MonoBehaviour
{
    //death sound found here
    //https://www.freesound.org/people/psychentist/sounds/168567/

    public int Health;
    int nextWaypointIndex = 0;
    public float Speed = 1f;
    // Use this for initialization
    void Start()
    {
        Health = Constants.InitialEnemyHealth;
    }

Script starts by getting the Health value from the Constants class. This will be the initial health amount of our enemy.

    void Update()
    {

        //calculate the distance between current position
        //and the target waypoint
        if (Vector2.Distance(transform.position,
            GameManager.Instance.Waypoints[nextWaypointIndex].position) < 0.01f)
        {
            //is this waypoint the last one?
            if (nextWaypointIndex == GameManager.Instance.Waypoints.Length - 1)
            {
                RemoveAndDestroy();
                GameManager.Instance.Lives--;
            }
            else
            {
                //our enemy will go to the next waypoint
                nextWaypointIndex++;
                //our simple AI, enemy is looking at the next waypoint
                transform.LookAt(GameManager.Instance.Waypoints[nextWaypointIndex].position,
                    -Vector3.forward);
                //only in the z axis
                transform.eulerAngles = new Vector3(0, 0, transform.eulerAngles.z);
            }
        }

        //enemy is moved towards the next waypoint
        transform.position = Vector2.MoveTowards(transform.position,
            GameManager.Instance.Waypoints[nextWaypointIndex].position,
            Time.deltaTime * Speed);
    }

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.

    void OnCollisionEnter2D(Collision2D col)
    {
        if (col.gameObject.tag == "Arrow")
        {//if we're hit by an arrow
            if (Health > 0)
            {
                //decrease enemy health
                Health -= Constants.ArrowDamage;
                if (Health <= 0)
                {
                    RemoveAndDestroy();
                }
            }
            col.gameObject.GetComponent<Arrow>().Disable(); //disable the arrow
        }
    }

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).

    public event EventHandler EnemyKilled;

    void RemoveAndDestroy()
    {
        AudioManager.Instance.PlayDeathSound();
        //remove it from the enemy list
        GameManager.Instance.Enemies.Remove(this.gameObject);
        Destroy(this.gameObject);
        //notify interested parties that we died
        if (EnemyKilled != null)
            EnemyKilled(this, EventArgs.Empty);
    }
}

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.

DragDropBunny

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.

public class DragDropBunny : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
        mainCamera = Camera.main;
    }

    private Camera mainCamera;
    //type of bunnies we'll create
    public GameObject BunnyPrefab;
    //the starting object for the drag
    public GameObject BunnyGenerator;
    bool isDragging = false;
    //temp bunny
    private GameObject newBunny;

    //will be colored red if we cannot place a bunny there
    private GameObject tempBackgroundBehindPath;

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.

    void Update()
    {
        //if we have money and we can drag a new bunny
        if (Input.GetMouseButtonDown(0) && !isDragging &&
            GameManager.Instance.MoneyAvailable >= Constants.BunnyCost)
        {
            ResetTempBackgroundColor();
            Vector2 location = mainCamera.ScreenToWorldPoint(Input.mousePosition);
            //if user has tapped onto the bunny generator
            if (BunnyGenerator.GetComponent<CircleCollider2D>() ==
                Physics2D.OverlapPoint(location, 1 << LayerMask.NameToLayer("BunnyGenerator")))
            {
                //initiate dragging operation and create a new bunny for us to drag
                isDragging = true;
                //create a temp bunny to drag around
                newBunny = Instantiate(BunnyPrefab, BunnyGenerator.transform.position, Quaternion.identity)
                    as GameObject;
            }
        }

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 (e.g. if the user could not place a new bunny on top of them, explained in the next paragraph!).

        else if (Input.GetMouseButton(0) && isDragging)
        {
            Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit2D[] hits = Physics2D.RaycastAll(ray.origin, ray.direction);
            if (hits.Length > 0 && hits[0].collider != null)
            {
                newBunny.transform.position = hits[0].collider.gameObject.transform.position;

                //if we're hitting a path or tower
                //or there is an existing bunny there
                //we use > 1 since we're hovering over the newBunny gameobject
                //(i.e. there is already a bunny there)
                if (hits.Where(x => x.collider.gameObject.tag == "Path"
                    || x.collider.gameObject.tag == "Tower").Count() > 0
                    || hits.Where(x=>x.collider.gameObject.tag == "Bunny").Count() > 1)
                {
                    //we cannot place a bunny there
                    GameObject backgroundBehindPath = hits.Where
                        (x => x.collider.gameObject.tag == "Background").First().collider.gameObject;
                    //make the sprite material "more red"
                    //to let the user know that we can't place a bunny here
                    backgroundBehindPath.GetComponent<SpriteRenderer>().color = Constants.RedColor;

                    if (tempBackgroundBehindPath != backgroundBehindPath)
                        ResetTempBackgroundColor();
                    //cache it to revert later
                    tempBackgroundBehindPath = backgroundBehindPath;
                }
                else //just reset the color on previously set paths
                {
                    ResetTempBackgroundColor();
                }

            }
        }

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.

        else if (Input.GetMouseButtonUp(0) && isDragging)
        {
            ResetTempBackgroundColor();
            //check if we can leave the bunny here
            Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);

            RaycastHit2D[] hits = Physics2D.RaycastAll(ray.origin, ray.direction,
                Mathf.Infinity, ~(1 << LayerMask.NameToLayer("BunnyGenerator")));             //in order to place it, we must have a background and no other bunnies             if (hits.Where(x=>x.collider.gameObject.tag == "Background").Count() > 0
                && hits.Where(x => x.collider.gameObject.tag == "Path").Count() == 0
                && hits.Where(x=>x.collider.gameObject.tag == "Bunny").Count() == 1)
            {
                //we can leave a bunny here, so decrease money and activate it
                GameManager.Instance.AlterMoneyAvailable(-Constants.BunnyCost);
                newBunny.transform.position =
                    hits.Where(x => x.collider.gameObject.tag == "Background")
                    .First().collider.gameObject.transform.position;
                newBunny.GetComponent<Bunny>().Activate();
            }
            else
            {
                //we can't leave a bunny here, so destroy the temp one
                Destroy(newBunny);
            }
            isDragging = false;

        }
    }

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.

    private void ResetTempBackgroundColor()
    {
        if (tempBackgroundBehindPath != null)
            tempBackgroundBehindPath.GetComponent<SpriteRenderer>().color = Constants.BlackColor;
    }

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).

GameManager

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.

public class GameManager : MonoBehaviour
{
    //basic singleton implementation
    [HideInInspector]
    public static GameManager Instance { get; private set; }

    void Awake()
    {
        Instance = this;
    }

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.

    //enemies on screen
    public List<GameObject> Enemies;
    //prefabs
    public GameObject EnemyPrefab;
    public GameObject PathPrefab;
    public GameObject TowerPrefab;
    //list of waypoints in the current level
    public Transform[] Waypoints;
    private GameObject PathPiecesParent;
    private GameObject WaypointsParent;
    //file pulled from resources
    private LevelStuffFromXML levelStuffFromXML;
    //will spawn carrots on screen
    public CarrotSpawner CarrotSpawner;

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.

    //helpful variables for our player
    [HideInInspector]
    public int MoneyAvailable { get; private set; }
    [HideInInspector]
    public float MinCarrotSpawnTime;
    [HideInInspector]
    public float MaxCarrotSpawnTime;
    public int Lives = 10;
    private int currentRoundIndex = 0;
    [HideInInspector]
    public GameState CurrentGameState;
    public SpriteRenderer BunnyGeneratorSprite;
    [HideInInspector]
    public bool FinalRoundFinished;
    public GUIText infoText;

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 GUIText reference that displays the necessary stuff that the player can see (you could call it a mini HUD).

    void Start()
    {
        IgnoreLayerCollisions();

        Enemies = new List<GameObject>();
        PathPiecesParent = GameObject.Find("PathPieces");
        WaypointsParent = GameObject.Find("Waypoints");
        levelStuffFromXML = Utilities.ReadXMLFile();

        CreateLevelFromXML();

        CurrentGameState = GameState.Start;

        FinalRoundFinished = false;
    }

The Start method calls the IgnoreLayerCollision method (described below), initializes the enemies list and finds the PathPieces and Waypoints gameobjects 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.

    private void CreateLevelFromXML()
    {
        foreach (var position in levelStuffFromXML.Paths)
        {
            GameObject go = Instantiate(PathPrefab, position,
                Quaternion.identity) as GameObject;
            go.GetComponent<SpriteRenderer>().sortingLayerName = "Path";
            go.transform.parent = PathPiecesParent.transform;
        }

        for (int i = 0; i < levelStuffFromXML.Waypoints.Count; i++)
        {
            GameObject go = new GameObject();
            go.transform.position = levelStuffFromXML.Waypoints[i];
            go.transform.parent = WaypointsParent.transform;
            go.tag = "Waypoint";
            go.name = "Waypoints" + i.ToString();
        }

        GameObject tower = Instantiate(TowerPrefab, levelStuffFromXML.Tower,
            Quaternion.identity) as GameObject;
        tower.GetComponent<SpriteRenderer>().sortingLayerName = "Foreground";

        Waypoints = GameObject.FindGameObjectsWithTag("Waypoint")
            .OrderBy(x => x.name).Select(x => x.transform).ToArray();

        MoneyAvailable = levelStuffFromXML.InitialMoney;
        MinCarrotSpawnTime = levelStuffFromXML.MinCarrotSpawnTime;
        MaxCarrotSpawnTime = levelStuffFromXML.MaxCarrotSpawnTime;
    }

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.

    private void IgnoreLayerCollisions()
    {
        int bunnyLayerID = LayerMask.NameToLayer("Bunny");
        int enemyLayerID = LayerMask.NameToLayer("Enemy");
        int arrowLayerID = LayerMask.NameToLayer("Arrow");
        int bunnyGeneratorLayerID = LayerMask.NameToLayer("BunnyGenerator");
        int backgroundLayerID = LayerMask.NameToLayer("Background");
        int pathLayerID = LayerMask.NameToLayer("Path");
        int towerLayerID = LayerMask.NameToLayer("Tower");
        int carrotLayerID = LayerMask.NameToLayer("Carrot");
        Physics2D.IgnoreLayerCollision(bunnyLayerID, enemyLayerID); //Bunny and Enemy (when dragging the bunny)
        Physics2D.IgnoreLayerCollision(arrowLayerID, bunnyGeneratorLayerID); //Arrow and BunnyGenerator
        Physics2D.IgnoreLayerCollision(arrowLayerID, backgroundLayerID); //Arrow and Background
        Physics2D.IgnoreLayerCollision(arrowLayerID, pathLayerID); //Arrow and Path
        Physics2D.IgnoreLayerCollision(arrowLayerID, bunnyLayerID); //Arrow and Bunny
        Physics2D.IgnoreLayerCollision(arrowLayerID, towerLayerID); //Arrow and Tower
        Physics2D.IgnoreLayerCollision(arrowLayerID, carrotLayerID); //Arrow and Carrot
    }

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.

    IEnumerator NextRound()
    {
        //give the player 2 secs to do stuff
        yield return new WaitForSeconds(2f);
        //get a reference to the next round details
        Round currentRound = levelStuffFromXML.Rounds[currentRoundIndex];
        for (int i = 0; i < currentRound.NoOfEnemies; i++)
        {//spawn a new enemy
            GameObject enemy = Instantiate(EnemyPrefab, Waypoints[0].position, Quaternion.identity) as GameObject;
            Enemy enemyComponent = enemy.GetComponent<Enemy>();
            //set speed and enemyKilled handler
            enemyComponent.Speed += Mathf.Clamp(currentRoundIndex, 1f, 5f);
            enemyComponent.EnemyKilled += OnEnemyKilled;
            //add it to the list and wait till you spawn the next one
            Enemies.Add(enemy);
            yield return new WaitForSeconds(1f / (currentRoundIndex == 0 ? 1 : currentRoundIndex));
        }

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.

    void OnEnemyKilled(object sender, EventArgs e)
    {
        bool startNewRound = false;
        //explicit lock, since this may occur any time by any enemy
        //not 100% that this is needed, but better safe than sorry!
        lock (lockerObject)
        {
            if (Enemies.Where(x => x != null).Count() == 0 && CurrentGameState == GameState.Playing)
            {
                startNewRound = true;
            }
        }
        if (startNewRound)
            CheckAndStartNewRound();
    }

The OnEnemyKilled method is called when an enemy dies. It checks if there are other enemies on the scene. If not, then it calls the CheckAndStartNewRound method.

    private void CheckAndStartNewRound()
    {
        if (currentRoundIndex < levelStuffFromXML.Rounds.Count - 1)
        {
            currentRoundIndex++;
            StartCoroutine(NextRound());
        }
        else
        {
            FinalRoundFinished = true;
        }
    }

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.

    void Update()
    {
        switch (CurrentGameState)
        {
            //start state, on tap, start the game and spawn carrots!
            case GameState.Start:
                if (Input.GetMouseButtonUp(0))
                {
                    CurrentGameState = GameState.Playing;
                    StartCoroutine(NextRound());
                    CarrotSpawner.StartCarrotSpawning();
                }
                break;
            case GameState.Playing:
                if (Lives == 0) //we lost
                {
                    //no more rounds
                    StopCoroutine(NextRound());
                    DestroyExistingEnemiesAndCarrots();
                    CarrotSpawner.StopCarrotSpawning();
                    CurrentGameState = GameState.Lost;
                }
                else if (FinalRoundFinished && Enemies.Where(x => x != null).Count() == 0)
                {
                    DestroyExistingEnemiesAndCarrots();
                    CarrotSpawner.StopCarrotSpawning();
                    CurrentGameState = GameState.Won;
                }
                break;

The Update method performs appropriate actions based on the game state.

  • If we’re about to start the game, one tap (or click) will begin it by calling the NextRound coroutine and start spawning the carrots.
  • During the course of the game
  • if player’s lives are 0
  • we won’t have any more rounds, so we stop the NextRound coroutine
  • we destroy existing enemies and carrots
  • prevent any more carrots from spawning
  • set the state of the game as Lost
  • if we’ve finished playing the last round and there are no more enemies on the scene, this means that the user has won! In this case
  • we remove all enemies and carrots from the scene (there are no enemies, of course)
  • prevent any more carrots from spawning
  • set the game state as Won
            case GameState.Won:
                if (Input.GetMouseButtonUp(0))
                {//restart
                    Application.LoadLevel(Application.loadedLevel);
                }
                break;
            case GameState.Lost:
                if (Input.GetMouseButtonUp(0))
                {//restart
                    Application.LoadLevel(Application.loadedLevel);
                }
                break;
            default:
                break;
        }
    }

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.


    private void DestroyExistingEnemiesAndCarrots()
    {
        //get all the enemies
        foreach (var item in Enemies)
        {
            if (item != null)
                Destroy(item.gameObject);
        }
        //get all the carrots
        var carrots = GameObject.FindGameObjectsWithTag("Carrot");
        foreach (var item in carrots)
        {
            Destroy(item);
        }
    }

The DestroyExistingEnemiesAndCarrots method finds all the enemies (via the list we have) and carrots (via their Tag) and removes them from the screen.

    public void AlterMoneyAvailable(int money)
    {
        MoneyAvailable += money;
        //we're also modifying the BunnyGenerator alpha color
        //yeah, I know, I could use an event for that, next time!
        if (MoneyAvailable < Constants.BunnyCost)
        {
            Color temp = BunnyGeneratorSprite.color;
            temp.a = 0.3f;
            BunnyGeneratorSprite.color = temp;
        }
        else
        {
            Color temp = BunnyGeneratorSprite.color;
            temp.a = 1.0f;
            BunnyGeneratorSprite.color = temp;
        }
    }

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.

    void OnGUI()
    {
        Utilities.AutoResize(800, 480);
        switch (CurrentGameState)
        {
            case GameState.Start:
                infoText.text = "Tap to start!";
                break;
            case GameState.Playing:
                infoText.text = "Money: " + MoneyAvailable.ToString() + "\n"
                    + "Life: " + Lives.ToString() + "\n" +
                    string.Format("round {0} of {1}", currentRoundIndex + 1, levelStuffFromXML.Rounds.Count);
                break;
            case GameState.Won:
                infoText.text = "Won! Tap to restart!";
                break;
            case GameState.Lost:
                infoText.text = "Lost :( Tap to restart!";
                break;
            default:
                break;
        }
    }

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.

Conclusion

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). Game can be extended in numerous ways, e.g. adding 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!

This game: https://github.com/dgkanatsios/TowerDefense and http://unitysamples.azurewebsites.net/TowerDefense
All my projects: https://github.com/dgkanatsios

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 Microsoft Virtual Academy video here: http://www.microsoftvirtualacademy.com/training-courses/porting-unity-games-to-windows-store-and-windows-phone

3 thoughts on “A Tower Defense game in Unity, part 2

  1. If u create a lot of tower and want to have different damage for each tower then i have to create new script for each tower? Or change at which script?

    Like

    • By “tower” you mean the Bunny, right? Right now they don’t have any “Health” property, you need to create a public one (like public int Health = 50;) and decrease it every time the enemy hits the “tower”.

      Like

Leave a comment