Building 3D Tower Defense Game — Turret
We’ve successfully built a simple wave spawning system in the previous blog post, so now let’s get into building our very first turret.
Creating a Turret Prefab
I’ve used Unity to model a simple turret that can rotate its head to target the enemies, and turn it into a prefab that we can reuse.
Next, let’ place it on a base in our scene and center it.
Visualizing the turret’s range
First we need a variable for the turret’s range:
[SerializeField] private float _range = 15f;
Next, I want to is visualize the turret’s range when we select it.
using UnityEditor;
void OnDrawGizmosSelected()
{
#if UNITY_EDITOR
Handles.color = Color.yellow;
Handles.DrawWireDisc(transform.position, Vector3.up, _range);
#endif
}
I use Handles.DrawWireDisc inside of OnDrawGizmosSelected to draw a circle around our turret when it’s being selected. In order to use the Handles.DrawWireDisc function, we’d need to reference the UnityEditor namespace at the top of the script.
I placed the code inside the Unity Editor conditional compilation block (#if UNITY_EDITOR) to ensure it's only executed in the Unity Editor and not in the built game.
And here’s the result:
Adding the logic to scan for targets
Firstly, I want to declare a variable for the prioritized target of the turret (since it can only target one enemy at a time) and the enemy tag which is “Enemy” (ensure that you’ve created a tag called “Enemy” for the enemy prefab).
[SerializeField] private GameObject _target;
[SerializeField] private string _enemyTag = "Enemy";
Next, I’m gonna use Collider to detect all colliders within the turret range. Here’s base logic:
private void ScanForTargets()
{
// Find all colliders in the turret's range
Collider[] colliders = Physics.OverlapSphere(transform.position, _range);
// Detect the enemies within range
foreach (Collider collider in colliders)
{
if (collider.CompareTag("Enemy"))
{
// Logic to handle enemies within range
}
}
}
From here, there are two ways you can choose a target for your turret: distance-based approach or index-based approach.
Distance-based approach
This is the easier approach among the two.
private void ScanForTargets()
{
// Find all colliders in the turret's range
Collider[] colliders = Physics.OverlapSphere(transform.position, _range);
// Track the closest enemy
GameObject closestEnemy = null;
float shortestDistance = Mathf.Infinity;
foreach (Collider collider in colliders)
{
if (collider.CompareTag("Enemy"))
{
// Calculate the distance to the enemy
float distanceToEnemy = Vector3.Distance(collider.transform.position, transform.position);
// If the enemy is closer and hasn't been targeted, update the closest enemy
if (distanceToEnemy < shortestDistance)
{
closestEnemy = collider.gameObject;
shortestDistance = distanceToEnemy;
}
}
}
if (closestEnemy != null)
{
_target = closestEnemy;
}
else
{
_target = null;
}
}
With this approach, we’re basically using the Collider array to store all GameObjects within the turret’s range that have the “Enemy” tag, and we set the closet enemy to be the prioritized target of the turret.
I want the turret to scan for targets every 0.3 seconds when the game starts, so we’re gonna use the InvokeRepeating method:
void Start()
{
InvokeRepeating("ScanForTargets", 0f, 0.3f);
}
And here’s the result:
Quick tip: you can use Gizmos.DrawLine to draw a line between two objects. I’ve drawn a line between the firepoint (which is located inside of the turret barrel) and the enemies.
[SerializeField] private GameObject _firePoint;
void OnDrawGizmosSelected()
{
if(_target != null) {
Gizmos.color = Color.red;
Gizmos.DrawLine(_firePoint.transform.position, _target.transform.position);
}
}
Index-based approach
This is a little more complex approach because need to give each spawned enemy an index (which is unique to them). So let’s create a new script for the enemy called “Enemy”:
using UnityEngine;
public class Enemy : MonoBehaviour
{
[SerializeField] private int _enemyIndex;
public void SetEnemyIndex(int index)
{
_enemyIndex = index;
}
public int GetEnemyIndex()
{
return _enemyIndex;
}
}
In this script, I declared a variable for each enemy’s index, and created two public functions to set and get an enemy’s index.
Next, let’s update the WaveSpawner script to give each enemy an index when they are spawned, starting from zero.
private int _enemyIndex;
void Start()
{
_enemyIndex = 0;
}
IEnumerator SpawnWave()
{
_waveIndex++;
if (_waveIndex > 1)
{
_gamePlayUI.UpdateWaveText(_waveIndex);
}
Enemy enemy = Instantiate(_enemyPrefab, _spawnPos.transform.position, Quaternion.identity).GetComponent<Enemy>();
enemy.SetEnemyIndex(_enemyIndex);
_enemyIndex++;
yield return null;
}
Now, let’s create the logic for the ScanForTargets function:
void ScanForTargets()
{
// Find all colliders in the turret's range
Collider[] colliders = Physics.OverlapSphere(transform.position, _range);
// Target the enemy based on index
int smallestEnemyIndex = int.MaxValue;
GameObject prioritizedEnemy = null;
foreach (Collider collider in colliders)
{
if (collider.CompareTag("Enemy"))
{
Enemy enemy = collider.GetComponent<Enemy>();
int enemyIndex = enemy.GetEnemyIndex();
if (enemyIndex < smallestEnemyIndex)
{
smallestEnemyIndex = enemyIndex;
prioritizedEnemy = enemy.gameObject;
}
}
}
if (prioritizedEnemy != null)
{
_target = prioritizedEnemy;
} else
{
_target = null;
}
}
And here’s the result:
And I’m gonna go with this approach instead of the distance-based one.
Making the turret turn to the targeted enemy
I only want the turret’s head to turn not its whole body so let’s get a reference to the head and declare a variable for the turn/rotation speed.
[SerializeField] private GameObject _turretHead;
private float _rotationSpeed = 10f;
And here’s the logic to rotate the turret’s head:
void RotateTurretHead()
{
Vector3 dir = _target.transform.position - transform.position;
Quaternion lookToRotation = Quaternion.LookRotation(dir);
Vector3 rotation = Quaternion.Lerp(_turretHead.transform.rotation, lookToRotation, _rotationSpeed * Time.deltaTime).eulerAngles;
_turretHead.transform.rotation = Quaternion.Euler(0f, rotation.y, 0f);
}
So, we calculate the distance vector between the target and the turret and create a “Quaternion” called “lookToRotation” that represents the rotation needed to make the turret look in the direction specified by the dir vector. Then, we use Quaternion.Lerp to smoothly calculate a transition between the current rotation the turret’s head and the “lookToRotation” and then apply the new rotation to the turret’s head.
I only want the turret’s head to rotate when it has a target.
void Update()
{
if(_target == null)
{
return;
}
RotateTurretHead();
}
And here’s the result:
In the next article, we’ll be looking at making our turret shoot bullets toward the enemies.