There’s no question tower defense games are insanely popular – whether we’re talking about the PC market, console market, or mobile game market. Defending a territory and stopping enemies is a true classic for games, as it involves tense action and strategy at the same time. But, how do the towers of tower defense games function?
In this Unity tutorial, we’ll go through the specifics of the tower creation process and how to have them shoot projectiles at the enemies in your game. No matter what kind of tower defense game you’re building, these basics will ensure you’re up and running with the most fundamental aspect of them.
Let’s get started!
Before You Begin & Project Files
Before you begin, note that some basic Unity experience is required, including knowledge of how to set up your project and create enemies for the game.
Regardless of experience level, though, you can download a copy of the assets (and scripts) used in this tutorial here.
BUILD GAMES FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.
Creating the Tower GameObject
Let’s create a new empty object called “CastleTower“. The tower is going to be shooting projectiles at the first enemy that it sees.
The 3D model for the tower should be added to the Assets > Models > Towers folder. You can use any model you want, or use the one we provided with the project files.
Make sure that the model sits under the parent “CastleTower” object, and add the following components:
- Sphere Collider (Trigger) for the tower’s attack range
- “Tower” C# script
We also need to create an empty child object to set the Transform at which we are spawning the projectiles. (e.g. at the top of the tower)
Scripting the Tower
Inside the script we created a moment ago, we’re going to define an enum for the tower’s target priority as such:
- First: The tower is going to target the first enemy that enters its range
- Close: The tower is going to target the closest enemy within the range
- Strong: The tower is going to target the enemy with the most health
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } }
We also need to declare the following variables for the target information and the tower’s attack settings:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; }
When an enemy enters the trigger (OnTriggerEnter), we’re going to add it to our curEnemiesInRange list. And when it leaves (OnTriggerExit), if it’s in the list, we’re going to remove it:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; private void OnTriggerEnter (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Add(other.GetComponent<Enemy>()); } } private void OnTriggerExit (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Remove(other.GetComponent<Enemy>()); } } }
Inside the Update function, we want to make sure that the tower is attacking at a set attackRate. To do this, we need to check how much time has elapsed from the last attack (Time.time – lastAttackTime).
If it is bigger than attackRate, the tower is ready to attack again. This is where we should reset the lastAttackTime and attack our current target (enemy) if it is not null.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; void Update () { // attack every "attackRate" seconds if(Time.time - lastAttackTime > attackRate) { lastAttackTime = Time.time; curEnemy = GetEnemy(); if(curEnemy != null) Attack(); } } private void OnTriggerEnter (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Add(other.GetComponent<Enemy>()); } } private void OnTriggerExit (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Remove(other.GetComponent<Enemy>()); } } }
Let’s now define the GetEnemy function and the Attack function:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; void Update () { // attack every "attackRate" seconds if(Time.time - lastAttackTime > attackRate) { lastAttackTime = Time.time; curEnemy = GetEnemy(); if(curEnemy != null) Attack(); } } // returns the current enemy for the tower to attack Enemy GetEnemy () { } // attacks the curEnemy void Attack () { } private void OnTriggerEnter (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Add(other.GetComponent<Enemy>()); } } private void OnTriggerExit (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Remove(other.GetComponent<Enemy>()); } } }
The GetEnemy function should return the current enemy for the tower to attack. But first, we need to check if the current enemy exists.
Chances are that the enemy is within the tower’s attack range (= exists in the curEnemiesInRange list) but has been already destroyed by another tower. In that case, the list will contain a null element, which we need to avoid as this is going to cause a null reference exception error.
Let’s remove any null element inside the curEnemiesInRange list by using the RemoveAll method:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; void Update () { // attack every "attackRate" seconds if(Time.time - lastAttackTime > attackRate) { lastAttackTime = Time.time; curEnemy = GetEnemy(); if(curEnemy != null) Attack(); } } // returns the current enemy for the tower to attack Enemy GetEnemy () { curEnemiesInRange.RemoveAll(x => x == null); } // attacks the curEnemy void Attack () { GameObject proj = Instantiate(projectilePrefab, projectileSpawnPos.position, Quaternion.identity); proj.GetComponent<Projectile>().Initialize(curEnemy, projectileDamage, projectileSpeed); } private void OnTriggerEnter (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Add(other.GetComponent<Enemy>()); } } private void OnTriggerExit (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Remove(other.GetComponent<Enemy>()); } } }
Note the use of a lambda expression (x => x == null) to create a Predicate, which defines the condition to remove. The List<T>.RemoveAll(Predicate<T>) method can traverse the list from the beginning, and remove any element ‘x‘ which meets the criteria (in this case, the criteria is that x is null).
Check out the C# List.RemoveAll method for more information: https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.removeall?view=net-6.0
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; void Update () { // attack every "attackRate" seconds if(Time.time - lastAttackTime > attackRate) { lastAttackTime = Time.time; curEnemy = GetEnemy(); if(curEnemy != null) Attack(); } } // returns the current enemy for the tower to attack Enemy GetEnemy () { curEnemiesInRange.RemoveAll(x => x == null); if(curEnemiesInRange.Count == 0) return null; if(curEnemiesInRange.Count == 1) return curEnemiesInRange[0]; } // attacks the curEnemy void Attack () { } private void OnTriggerEnter (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Add(other.GetComponent<Enemy>()); } } private void OnTriggerExit (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Remove(other.GetComponent<Enemy>()); } } }
Then we can return any remaining enemy in the list, or null if there is none. If we’re returning an enemy, remember to return the right enemy based on the TowerTargetPriority:
- If it is TowerTargetPriority.First, the enemy to return is simply the first element in the list (curEnemiesInRange[0]).
- If it is TowerTargetPriority.Close, we need to find the enemy to return by looping through each enemy in the list and checking the distance between the tower and each enemy’s position.
When comparing the distance, we need to find the vector between the two points (e.g. TowerPosition – EnemyPosition) and then find its magnitude.
However, the Sqrt ( √ ) calculation is quite complicated and takes longer to execute than the normal arithmetic operations. So if you’re simply comparing distances, Unity recommends using Vector3.sqrMagnitude instead of using the magnitude property.
Although sqrMagnitude doesn’t give you a usable distance in terms of units per second, it is much faster and is useful when we’re comparing it with another distance. It is a more performance-friendly way to compare distances. (Note that you should only compare it against the squares of distances. For more information, check https://docs.unity3d.com/ScriptReference/Vector3-sqrMagnitude.html).
We can use the above calculation to find the closest enemy in range.
Finally, if the TowerTargetPriority is set to Strong, we will use a foreach loop and find the enemy with the highest health points:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; void Update () { // attack every "attackRate" seconds if(Time.time - lastAttackTime > attackRate) { lastAttackTime = Time.time; curEnemy = GetEnemy(); if(curEnemy != null) Attack(); } } // returns the current enemy for the tower to attack Enemy GetEnemy () { curEnemiesInRange.RemoveAll(x => x == null); if(curEnemiesInRange.Count == 0) return null; if(curEnemiesInRange.Count == 1) return curEnemiesInRange[0]; switch(targetPriority) { case TowerTargetPriority.First: { return curEnemiesInRange[0]; } case TowerTargetPriority.Close: { Enemy closest = null; float dist = 99; for(int x = 0; x < curEnemiesInRange.Count; x++) { float d = (transform.position - curEnemiesInRange[x].transform.position).sqrMagnitude; if(d < dist) { closest = curEnemiesInRange[x]; dist = d; } } return closest; } case TowerTargetPriority.Strong: { Enemy strongest = null; int strongestHealth = 0; foreach(Enemy enemy in curEnemiesInRange) { if(enemy.health > strongestHealth) { strongest = enemy; strongestHealth = enemy.health; } } return strongest; } } return null; } // attacks the curEnemy void Attack () { } private void OnTriggerEnter (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Add(other.GetComponent<Enemy>()); } } private void OnTriggerExit (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Remove(other.GetComponent<Enemy>()); } } }
Scripting the Projectiles
In this last part of the tutorial, we’re going to be setting up the Tower Projectiles in order to attack the enemies.
Let’s begin by creating an empty GameObject called “CastleProjectile”, and adding a new C# script called “Projectile”:
We will also add a basic Cube as a child to visualize the projectile.
Let’s save it as a Prefab and remove the instance from the scene.
Let’s open up the script and declare the following variables:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Projectile : MonoBehaviour { private Enemy target; private int damage; private float moveSpeed; public GameObject hitSpawnPrefab; }
First of all, we will define the Initialize function so that the projectile moves at the given speed to damage the given target by the given damage amount. This Initialize function should be called whenever a new projectile instance is created by the Tower script.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Projectile : MonoBehaviour { private Enemy target; private int damage; private float moveSpeed; public GameObject hitSpawnPrefab; public void Initialize (Enemy target, int damage, float moveSpeed) { this.target = target; this.damage = damage; this.moveSpeed = moveSpeed; } }
Inside the Update function, we’re going to update the projectile’s position if the target exists. If the target is null, then we will destroy the projectile:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Projectile : MonoBehaviour { private Enemy target; private int damage; private float moveSpeed; public GameObject hitSpawnPrefab; public void Initialize (Enemy target, int damage, float moveSpeed) { this.target = target; this.damage = damage; this.moveSpeed = moveSpeed; } void Update () { if(target != null) { } else { Destroy(gameObject); } } }
To move the projectile towards the target, we will use Vector3.MoveTowards to update the transform.position. The projectile should also look at the target, and damage the target once it reaches the target.
After damaging the target, the projectile instance should be destroyed, and a new instance should be created:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Projectile : MonoBehaviour { private Enemy target; private int damage; private float moveSpeed; public GameObject hitSpawnPrefab; public void Initialize (Enemy target, int damage, float moveSpeed) { this.target = target; this.damage = damage; this.moveSpeed = moveSpeed; } void Update () { if(target != null) { transform.position = Vector3.MoveTowards(transform.position, target.transform.position, moveSpeed * Time.deltaTime); transform.LookAt(target.transform); if(Vector3.Distance(transform.position, target.transform.position) < 0.2f) { target.TakeDamage(damage); if(hitSpawnPrefab != null) Instantiate(hitSpawnPrefab, transform.position, Quaternion.identity); Destroy(gameObject); } } else { Destroy(gameObject); } } }
Let’s save the script and go over to the Tower script to define the Attack function. Here, we will create a new projectile instance and initialize it with the pre-defined variables. We will also make it rotate along the Y-axis to look at the current target:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Tower : MonoBehaviour { public enum TowerTargetPriority { First, Close, Strong } [Header("Info")] public float range; private List<Enemy> curEnemiesInRange = new List<Enemy>(); private Enemy curEnemy; public TowerTargetPriority targetPriority; public bool rotateTowardsTarget; [Header("Attacking")] public float attackRate; private float lastAttackTime; public GameObject projectilePrefab; public Transform projectileSpawnPos; public int projectileDamage; public float projectileSpeed; void Update () { // attack every "attackRate" seconds if(Time.time - lastAttackTime > attackRate) { lastAttackTime = Time.time; curEnemy = GetEnemy(); if(curEnemy != null) Attack(); } } // returns the current enemy for the tower to attack Enemy GetEnemy () { curEnemiesInRange.RemoveAll(x => x == null); if(curEnemiesInRange.Count == 0) return null; if(curEnemiesInRange.Count == 1) return curEnemiesInRange[0]; switch(targetPriority) { case TowerTargetPriority.First: { return curEnemiesInRange[0]; } case TowerTargetPriority.Close: { Enemy closest = null; float dist = 99; for(int x = 0; x < curEnemiesInRange.Count; x++) { float d = (transform.position - curEnemiesInRange[x].transform.position).sqrMagnitude; if(d < dist) { closest = curEnemiesInRange[x]; dist = d; } } return closest; } case TowerTargetPriority.Strong: { Enemy strongest = null; int strongestHealth = 0; foreach(Enemy enemy in curEnemiesInRange) { if(enemy.health > strongestHealth) { strongest = enemy; strongestHealth = enemy.health; } } return strongest; } } return null; } // attacks the curEnemy void Attack () { if(rotateTowardsTarget) { transform.LookAt(curEnemy.transform); transform.eulerAngles = new Vector3(0, transform.eulerAngles.y, 0); } GameObject proj = Instantiate(projectilePrefab, projectileSpawnPos.position, Quaternion.identity); proj.GetComponent<Projectile>().Initialize(curEnemy, projectileDamage, projectileSpeed); } private void OnTriggerEnter (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Add(other.GetComponent<Enemy>()); } } private void OnTriggerExit (Collider other) { if(other.CompareTag("Enemy")) { curEnemiesInRange.Remove(other.GetComponent<Enemy>()); } } }
Now we can assign the public variables in the Inspector and hit Play to test it out (though you’ll of course need to set up enemies on your end to see it function):
- Range: 2
- Target Priority: First
- Attack Rate: 0.5
- Projectile Damage: 1
- Projectile Speed: 6
Conclusion
And that is our tutorial complete! Well done on creating the towers for your tower defense game!
Tower defense games are excellent for you to test your level design skills by placing the towers in the best strategic spots possible for each level. You’ll also learn a lot about balancing the game difficulty to be challenging while not too cumbersome at the same time.
Now you’re ready to keep pushing your project forward, as you still need to generate the spawning of the enemy waves and make them move along custom waypoints. You’ll also need to assess when the enemy units take damage to calculate their remaining health. However, these are topics for another tutorial – for now setting up your towers is a great first step!
We hope you have enjoyed learning with us and wish you all the best of luck in your future projects!
Want to learn more about farming sims? Try our complete Create a Tower Defense Game course.