How to Create a Tower Defense Game in Unity – Part 2

In this second and final part of the Unity tower defense tutorial, you’ll add some shooting monsters into the the mix. By Jeff Fisher.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Track Enemies in Range

Now the monsters need to know which enemies to target. You have a bit of prework to do on the Monster and the Enemy before you implement.

Select Prefabs\Monster in the Project Browser and add a Circle Collider 2D component to it in the Inspector.

Set the collider's Radius to 2.5 -- this sets the monsters' firing range.

Check Is Trigger so that objects pass through the area rather than bump into it.

Finally, at the top of the Inspector, set Monster's Layer to Ignore Raycast. Click Yes, change children in the dialog. If you don't ignore raycasts, the collider reacts to click events. That is a problem because the Monsters block events meant for the Openspots below them.

Bildschirmfoto 2015-06-05 um 14.47.15

To allow detection of an enemy in the trigger area, you need to add a collider and rigid body to it, because Unity only sends trigger events if one of the colliders has a rigid body attached.

In the Project Browser, select Prefabs\Enemy. Add a Rigidbody 2D component with Body Type set to Kinematic. This means the body shouldn't be affected by physics.

Add a Circle Collider 2D with a Radius of 1. Repeat those steps for Prefabs\Enemy 2

The triggers are now set up, so monsters detect when an enemy is in range.

You need to prepare one more thing: A script that notifies monsters when an enemy is destroyed so they don't cause an exception by continuing to fire.

Create a new C# script named EnemyDestructionDelegate and add it to both the Enemy and Enemy2 prefabs.

Open EnemyDestructionDelegate.cs in your IDE, and add the following delegate declaration:

public delegate void EnemyDelegate (GameObject enemy);
public EnemyDelegate enemyDelegate;

Here you create a delegate, which is a container for a function that can be passed around like a variable.

Note: Use delegates when you want one game object to actively notify other game objects of changes. Learn more about delegates from the Unity documentation.

Add the following method:

void OnDestroy()
{
  if (enemyDelegate != null)
  {
    enemyDelegate(gameObject);
  }
}

Upon destruction of a game object, Unity calls this method automatically, and it checks whether the delegate is not null. In that case, you call it with the gameObject as a parameter. This lets all listeners that are registered as delegates know the enemy was destroyed.

Save the file and go back to Unity.

Give Monsters a License to Kill

And now the monsters can detect enemies in range. Add a new C# script to the Monster prefab and name it ShootEnemies.

Open ShootEnemies.cs in your IDE, and add the following using statement to get access to Generics.

using System.Collections.Generic;

Add a variable to keep track of all enemies within range:

public List<GameObject> enemiesInRange;

In enemiesInRange, you'll store all enemies that are in range.

Initialize the field in Start().

enemiesInRange = new List<GameObject>();

In the beginning, there are no enemies in range, so you create an empty list.

Fill the enemiesInRange list! Add this code to the script:

// 1
void OnEnemyDestroy(GameObject enemy)
{
  enemiesInRange.Remove (enemy);
}

void OnTriggerEnter2D (Collider2D other)
{
// 2
  if (other.gameObject.tag.Equals("Enemy"))
  {
    enemiesInRange.Add(other.gameObject);
    EnemyDestructionDelegate del =
        other.gameObject.GetComponent<EnemyDestructionDelegate>();
    del.enemyDelegate += OnEnemyDestroy;
  }
}
// 3
void OnTriggerExit2D (Collider2D other)
{
  if (other.gameObject.tag.Equals("Enemy"))
  {
    enemiesInRange.Remove(other.gameObject);
    EnemyDestructionDelegate del =
        other.gameObject.GetComponent<EnemyDestructionDelegate>();
    del.enemyDelegate -= OnEnemyDestroy;
  }
}

Where to go From Here

Select a Target

Give Monsters Bullets - Lots of Bullets!

Get Bigger Bullets

Leveling the Bullets

Open Fire

Put it All Together

Save the file and then run the game in Unity. To test whether it works, place a monster, select it and watch the changes to the enemiesInRange list in the Inspector.

Now monsters know which enemy is in range. But what do they do when there are multiple in-range enemies?

They attack the one closest to the cookie, of course!

Open MoveEnemy.cs in your IDE, and add this new method to calculates this:

This code calculates the length of road not yet traveled by the enemy. It does so using Distance, which calculates the difference between two Vector3 instances.

You'll use this method later to figure out which target to attack. However, your monsters are unarmed and helpless, so fix that first.

Save the file and go back to Unity to begin setting up your bullets.

Drag and drop Images/Objects/Bullet1 from the Project Browser into the scene. Set z position to -2 -- x and y positions don't matter because you set them each time you instantiate a new bullet at run time.

Add a new C# script named BulletBehavior, and add the following variables to it in your IDE:

speed determines how quickly bullets fly; damage is self-explanatory.

The target, startPosition, and targetPosition determine the bullet's direction.

distance and startTime track the bullet's current position. gameManager rewards players when they crush an enemy.

Assign values to these variables in Start():

You set startTime to the current time and calculate the distance between the start and target positions. You also get the GameManagerBehavior as usual.

Add the following code to Update() to control the bullet movement:

Save the file and return to Unity.

Wouldn't it be cool if your monster shot bigger bullets at higher levels? - Yes, yes, it would! Fortunately, this is easy to implement.

Drag and drop the Bullet1 game object from the Hierarchy to the Project tab to create a prefab of the bullet. Remove the original object from the scene -- you don't need it anymore.

Duplicate the Bullet1 prefab twice. Name the copies Bullet2 and Bullet3.

Select Bullet2. In the Inspector, set the Sprite Renderer component's Sprite field to Images/Objects/Bullet2. This makes Bullet2 look a bit bigger than Bullet1.

Repeat that procedure to set the Bullet3 prefab's sprite to Images/Objects/Bullet3.

Next, set how much damage the bullets deliver in Bullet Behavior.

Select the Bullet1 prefab in the Project tab. In Inspector you can see the Bullet Behavior (Script), and there you set the Damage to 10 for Bullet1, 15 for Bullet2, and 20 for Bullet3 -- or whatever makes you happy there.

Note: I set the values so that at higher levels, the cost per damage is higher. This counteracts the fact that the upgrade allows the player to improve the monsters in the best spots.

Bullet prefabs - size increases with level

Assign different bullets to different monster levels so stronger monsters shred enemies faster.

Open MonsterData.cs in your IDE, and add these variables to MonsterLevel:

These will set the bullet prefab and fire rate for each monster level. Save the file and head back to Unity to finish setting up your monsters.

Select the Monster prefab in the Project Browser. In the Inspector, expand Levels in the Monster Data (Script) component. Set Fire Rate to 1 for each of the elements. Then set Bullet for Elements 0, 1 and 2 to Bullet1, Bullet2 and Bullet3, respectively.

Your monster levels should be configured as shown below:

MonsterData with bullets

Bullets to kill your enemies? - Check! Open fire!

Pew Pew - lasers ! (from Gisela Giardino)

Open the ShootEnemies.cs in your IDE, and add some variables:

As their names suggest, these variables keep track of when this monster last fired, as well the MonsterData structure that includes information about this monster's bullet type, fire rate, etc.

Assign values to those fields in Start():

Here you set lastShotTime to the current time and get access to this object's MonsterData component.

Add the following method to implement shooting:

Time to wire everything together. Determine the target and make your monster watch it.

Still in ShootEnemies.cs, add this code to Update():

Go through this code step by step.

Save the file and play the game in Unity. Your monsters vigorously defend your cookie. You’re totally, completely DONE!

You can download the finished project here.

Wow, so you really did a lot between both tutorials and you have a cool game to show for it.
Here are a few ideas to build on what you've done:

  1. In OnEnemyDestroy, you remove the enemy from enemiesInRange. When an enemy walks on the trigger around your monster OnTriggerEnter2D is called.
  2. You then add the enemy to the list of enemiesInRange and add OnEnemyDestroy to the EnemyDestructionDelegate. This makes sure that OnEnemyDestroy is called when the enemy is destroyed. You don't want monsters to waste ammo on dead enemies now -- do you?
  3. In OnTriggerExit2D you remove the enemy from the list and unregister your delegate. Now you know which enemies are in range.
  4. public float DistanceToGoal()
    {
      float distance = 0;
      distance += Vector2.Distance(
          gameObject.transform.position, 
          waypoints [currentWaypoint + 1].transform.position);
      for (int i = currentWaypoint + 1; i < waypoints.Length - 1; i++)
      {
        Vector3 startPosition = waypoints [i].transform.position;
        Vector3 endPosition = waypoints [i + 1].transform.position;
        distance += Vector2.Distance(startPosition, endPosition);
      }
      return distance;
    }
    
    public float speed = 10;
    public int damage;
    public GameObject target;
    public Vector3 startPosition;
    public Vector3 targetPosition;
    
    private float distance;
    private float startTime;
    
    private GameManagerBehavior gameManager;
    
    startTime = Time.time;
    distance = Vector2.Distance (startPosition, targetPosition);
    GameObject gm = GameObject.Find("GameManager");
    gameManager = gm.GetComponent<GameManagerBehavior>();
    
    // 1 
    float timeInterval = Time.time - startTime;
    gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance);
    
    // 2 
    if (gameObject.transform.position.Equals(targetPosition))
    {
      if (target != null)
      {
        // 3
        Transform healthBarTransform = target.transform.Find("HealthBar");
        HealthBar healthBar = 
            healthBarTransform.gameObject.GetComponent<HealthBar>();
        healthBar.currentHealth -= Mathf.Max(damage, 0);
        // 4
        if (healthBar.currentHealth <= 0)
        {
          Destroy(target);
          AudioSource audioSource = target.GetComponent<AudioSource>();
          AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
    
          gameManager.Gold += 50;
        }
      }
      Destroy(gameObject);
    }
    
    1. You calculate the new bullet position using Vector3.Lerp to interpolate between start and end positions.
    2. If the bullet reaches the targetPosition, you verify that target still exists.
    3. You retrieve the target's HealthBar component and reduce its health by the bullet's damage.
    4. If the health of the enemy falls to zero, you destroy it, play a sound effect and reward the player for marksmanship.
    Bullet prefabs - size increases with level
    public GameObject bullet;
    public float fireRate;
    
    Pew Pew - lasers ! (from Gisela Giardino)
    private float lastShotTime;
    private MonsterData monsterData;
    
    lastShotTime = Time.time;
    monsterData = gameObject.GetComponentInChildren<MonsterData>();
    
    void Shoot(Collider2D target)
    {
      GameObject bulletPrefab = monsterData.CurrentLevel.bullet;
      // 1 
      Vector3 startPosition = gameObject.transform.position;
      Vector3 targetPosition = target.transform.position;
      startPosition.z = bulletPrefab.transform.position.z;
      targetPosition.z = bulletPrefab.transform.position.z;
    
      // 2 
      GameObject newBullet = (GameObject)Instantiate (bulletPrefab);
      newBullet.transform.position = startPosition;
      BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>();
      bulletComp.target = target.gameObject;
      bulletComp.startPosition = startPosition;
      bulletComp.targetPosition = targetPosition;
    
      // 3 
      Animator animator = 
          monsterData.CurrentLevel.visualization.GetComponent<Animator>();
      animator.SetTrigger("fireShot");
      AudioSource audioSource = gameObject.GetComponent<AudioSource>();
      audioSource.PlayOneShot(audioSource.clip);
    }
    
    1. Get the start and target positions of the bullet. Set the z-Position to that of bulletPrefab. Earlier, you set the bullet prefab's z position value to make sure the bullet appears behind the monster firing it, but in front of the enemies.
    2. Instantiate a new bullet using the bulletPrefab for MonsterLevel. Assign the startPosition and targetPosition of the bullet.
    3. Make the game juicier: Run a shoot animation and play a laser sound whenever the monster shoots.
    GameObject target = null;
    // 1
    float minimalEnemyDistance = float.MaxValue;
    foreach (GameObject enemy in enemiesInRange)
    {
      float distanceToGoal = enemy.GetComponent<MoveEnemy>().DistanceToGoal();
      if (distanceToGoal < minimalEnemyDistance)
      {
        target = enemy;
        minimalEnemyDistance = distanceToGoal;
      }
    }
    // 2
    if (target != null)
    {
      if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate)
      {
        Shoot(target.GetComponent<Collider2D>());
        lastShotTime = Time.time;
      }
      // 3
      Vector3 direction = gameObject.transform.position - target.transform.position;
      gameObject.transform.rotation = Quaternion.AngleAxis(
          Mathf.Atan2 (direction.y, direction.x) * 180 / Mathf.PI,
          new Vector3 (0, 0, 1));
    }
    
    1. Determine the target of the monster. Start with the maximum possible distance in the minimalEnemyDistance. Iterate over all enemies in range and make an enemy the new target if its distance to the cookie is smaller than the current minimum.
    2. Call Shoot if the time passed is greater than the fire rate of your monster and set lastShotTime to the current time.
    3. Calculate the rotation angle between the monster and its target. You set the rotation of the monster to this angle. Now it always faces the target.
  1. You calculate the new bullet position using Vector3.Lerp to interpolate between start and end positions.
  2. If the bullet reaches the targetPosition, you verify that target still exists.
  3. You retrieve the target's HealthBar component and reduce its health by the bullet's damage.
  4. If the health of the enemy falls to zero, you destroy it, play a sound effect and reward the player for marksmanship.
  1. Get the start and target positions of the bullet. Set the z-Position to that of bulletPrefab. Earlier, you set the bullet prefab's z position value to make sure the bullet appears behind the monster firing it, but in front of the enemies.
  2. Instantiate a new bullet using the bulletPrefab for MonsterLevel. Assign the startPosition and targetPosition of the bullet.
  3. Make the game juicier: Run a shoot animation and play a laser sound whenever the monster shoots.
  1. Determine the target of the monster. Start with the maximum possible distance in the minimalEnemyDistance. Iterate over all enemies in range and make an enemy the new target if its distance to the cookie is smaller than the current minimum.
  2. Call Shoot if the time passed is greater than the fire rate of your monster and set lastShotTime to the current time.
  3. Calculate the rotation angle between the monster and its target. You set the rotation of the monster to this angle. Now it always faces the target.
Bullet prefabs - size increases with level
Pew Pew - lasers ! (from Gisela Giardino)
public float DistanceToGoal()
{
  float distance = 0;
  distance += Vector2.Distance(
      gameObject.transform.position, 
      waypoints [currentWaypoint + 1].transform.position);
  for (int i = currentWaypoint + 1; i < waypoints.Length - 1; i++)
  {
    Vector3 startPosition = waypoints [i].transform.position;
    Vector3 endPosition = waypoints [i + 1].transform.position;
    distance += Vector2.Distance(startPosition, endPosition);
  }
  return distance;
}
public float speed = 10;
public int damage;
public GameObject target;
public Vector3 startPosition;
public Vector3 targetPosition;

private float distance;
private float startTime;

private GameManagerBehavior gameManager;
startTime = Time.time;
distance = Vector2.Distance (startPosition, targetPosition);
GameObject gm = GameObject.Find("GameManager");
gameManager = gm.GetComponent<GameManagerBehavior>();
// 1 
float timeInterval = Time.time - startTime;
gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance);

// 2 
if (gameObject.transform.position.Equals(targetPosition))
{
  if (target != null)
  {
    // 3
    Transform healthBarTransform = target.transform.Find("HealthBar");
    HealthBar healthBar = 
        healthBarTransform.gameObject.GetComponent<HealthBar>();
    healthBar.currentHealth -= Mathf.Max(damage, 0);
    // 4
    if (healthBar.currentHealth <= 0)
    {
      Destroy(target);
      AudioSource audioSource = target.GetComponent<AudioSource>();
      AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);

      gameManager.Gold += 50;
    }
  }
  Destroy(gameObject);
}
public GameObject bullet;
public float fireRate;
private float lastShotTime;
private MonsterData monsterData;
lastShotTime = Time.time;
monsterData = gameObject.GetComponentInChildren<MonsterData>();
void Shoot(Collider2D target)
{
  GameObject bulletPrefab = monsterData.CurrentLevel.bullet;
  // 1 
  Vector3 startPosition = gameObject.transform.position;
  Vector3 targetPosition = target.transform.position;
  startPosition.z = bulletPrefab.transform.position.z;
  targetPosition.z = bulletPrefab.transform.position.z;

  // 2 
  GameObject newBullet = (GameObject)Instantiate (bulletPrefab);
  newBullet.transform.position = startPosition;
  BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>();
  bulletComp.target = target.gameObject;
  bulletComp.startPosition = startPosition;
  bulletComp.targetPosition = targetPosition;

  // 3 
  Animator animator = 
      monsterData.CurrentLevel.visualization.GetComponent<Animator>();
  animator.SetTrigger("fireShot");
  AudioSource audioSource = gameObject.GetComponent<AudioSource>();
  audioSource.PlayOneShot(audioSource.clip);
}
GameObject target = null;
// 1
float minimalEnemyDistance = float.MaxValue;
foreach (GameObject enemy in enemiesInRange)
{
  float distanceToGoal = enemy.GetComponent<MoveEnemy>().DistanceToGoal();
  if (distanceToGoal < minimalEnemyDistance)
  {
    target = enemy;
    minimalEnemyDistance = distanceToGoal;
  }
}
// 2
if (target != null)
{
  if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate)
  {
    Shoot(target.GetComponent<Collider2D>());
    lastShotTime = Time.time;
  }
  // 3
  Vector3 direction = gameObject.transform.position - target.transform.position;
  gameObject.transform.rotation = Quaternion.AngleAxis(
      Mathf.Atan2 (direction.y, direction.x) * 180 / Mathf.PI,
      new Vector3 (0, 0, 1));
}
  • More enemy types and monsters
  • Multiple enemy paths
  • Different levels

Each of these ideas requires minimal changes and can make your game addictive. If you created a new game from this tutorial, we'd love to play it -- so share the link and your brags in the comments.

You can find interesting thoughts on making a hit tower defense game in this interview.

Thank you for taking the time to work through these tutorials. I look forward to seeing your awesome concepts and killing lots of monsters.

Jeff Fisher

Contributors

Jeff Fisher

Author

Barbara Reichart

Author

Mitch Allen

Tech Editor

Sean Duffy

Final Pass Editor

Over 300 content creators. Join our team.