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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Create a Tower Defense Game in Unity – Part 2
35 mins
- Getting Started
- Rotate the Enemies
- Inform the Player
- Waves: Spawn, Spawn, Spawn
- Set Enemy Tags
- Define Enemy Waves
- Set Spawn Intervals
- Optional: Add Different Types of Enemies
- Update Player Health – Killing Me Softly
- Manage Health
- Update Health
- Monster Wars: The Revenge of the Monsters
- Enemy Health Bar
- Adjust Health Bar Length
- Track Enemies in Range
- Give Monsters a License to Kill
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.
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.
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.
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:
Bullets to kill your enemies? - Check! Open fire!
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:
- In
OnEnemyDestroy
, you remove the enemy fromenemiesInRange
. When an enemy walks on the trigger around your monsterOnTriggerEnter2D
is called. - You then add the enemy to the list of
enemiesInRange
and addOnEnemyDestroy
to theEnemyDestructionDelegate
. This makes sure thatOnEnemyDestroy
is called when the enemy is destroyed. You don't want monsters to waste ammo on dead enemies now -- do you? - In
OnTriggerExit2D
you remove the enemy from the list and unregister your delegate. Now you know which enemies are in range. - You calculate the new bullet position using
Vector3.Lerp
to interpolate between start and end positions. - If the bullet reaches the
targetPosition
, you verify thattarget
still exists. - You retrieve the target's
HealthBar
component and reduce its health by the bullet'sdamage
. - If the health of the enemy falls to zero, you destroy it, play a sound effect and reward the player for marksmanship.
- 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. - Instantiate a new bullet using the
bulletPrefab
forMonsterLevel
. Assign thestartPosition
andtargetPosition
of the bullet. - Make the game juicier: Run a shoot animation and play a laser sound whenever the monster shoots.
- 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. - Call
Shoot
if the time passed is greater than the fire rate of your monster and setlastShotTime
to the current time. - 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.
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.
Select a Target
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:
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;
}
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.
Give Monsters Bullets - Lots of 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:
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;
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()
:
startTime = Time.time;
distance = Vector2.Distance (startPosition, targetPosition);
GameObject gm = GameObject.Find("GameManager");
gameManager = gm.GetComponent<GameManagerBehavior>();
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:
// 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);
}
Save the file and return to Unity.
Get Bigger Bullets
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.
Leveling the Bullets
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
:
public GameObject bullet;
public float fireRate;
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:
Bullets to kill your enemies? - Check! Open fire!
Open Fire
Open the ShootEnemies.cs in your IDE, and add some variables:
private float lastShotTime;
private MonsterData monsterData;
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()
:
lastShotTime = Time.time;
monsterData = gameObject.GetComponentInChildren<MonsterData>();
Here you set lastShotTime
to the current time and get access to this object's MonsterData
component.
Add the following method to implement shooting:
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);
}
Put it All Together
Time to wire everything together. Determine the target and make your monster watch it.
Still in ShootEnemies.cs, add this code to Update()
:
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));
}
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!
Where to go From Here
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:
- You calculate the new bullet position using
Vector3.Lerp
to interpolate between start and end positions. - If the bullet reaches the
targetPosition
, you verify thattarget
still exists. - You retrieve the target's
HealthBar
component and reduce its health by the bullet'sdamage
. - If the health of the enemy falls to zero, you destroy it, play a sound effect and reward the player for marksmanship.
- 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. - Instantiate a new bullet using the
bulletPrefab
forMonsterLevel
. Assign thestartPosition
andtargetPosition
of the bullet. - Make the game juicier: Run a shoot animation and play a laser sound whenever the monster shoots.
- 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. - Call
Shoot
if the time passed is greater than the fire rate of your monster and setlastShotTime
to the current time. - 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.
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.
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.