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
Welcome to part two of How to Create a Tower Defense Game in Unity. You’re making a tower defense game in Unity, and at the end of part one, you could place and upgrade monsters. You also had one enemy attack the cookie.
However, the enemy had no idea which way to face! Also, it was a poor excuse for an attack. In this part, you’ll add enemy waves and arm your monsters so they can defend your precious cookie.
Getting Started
In Unity, open your completed project from the first part of this tutorial series, or if you’re just joining in now, download the starter project and open TowerDefense-Part2-Starter.
Open GameScene from the Scenes folder.
Rotate the Enemies
At the end of the last tutorial, the enemy followed the road, but appeared to have no idea which way to face.
Open MoveEnemy.cs in your IDE, and add the following method to fix this.
private void RotateIntoMoveDirection()
{
//1
Vector3 newStartPosition = waypoints [currentWaypoint].transform.position;
Vector3 newEndPosition = waypoints [currentWaypoint + 1].transform.position;
Vector3 newDirection = (newEndPosition - newStartPosition);
//2
float x = newDirection.x;
float y = newDirection.y;
float rotationAngle = Mathf.Atan2 (y, x) * 180 / Mathf.PI;
//3
GameObject sprite = gameObject.transform.Find("Sprite").gameObject;
sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle, Vector3.forward);
}
RotateIntoMoveDirection
rotates the enemy so that it always looks forward, like so:
- It calculates the bug’s current movement direction by subtracting the current waypoint’s position from that of the next waypoint.
- It uses
Mathf.Atan2
to determine the angle toward whichnewDirection
points, in radians, assuming zero points to the right. Multiplying the result by180 / Mathf.PI
converts the angle to degrees. - Finally, it retrieves the child named Sprite and rotates it
rotationAngle
degrees along the z-axis. Note that you rotate the child instead of the parent so the health bar — you’ll add it soon — remains horizontal.
In Update()
, replace the comment // TODO: Rotate into move direction
with the following call to RotateIntoMoveDirection
:
RotateIntoMoveDirection();
Save the file and switch to Unity. Run the scene; now your monster knows where he’s going.
One single enemy? Hardly impressive. Let the hordes come. And like in every tower defense game, hordes will come in waves!
Inform the Player
Before you set the hordes into motion, you need to let the player know about the coming onslaught. Also, why not display the current wave’s number at the top of the screen?
Several GameObjects need wave information, so you’ll add it to the GameManagerBehavior component on GameManager.
Open GameManagerBehavior.cs in your IDE and add these two variables:
public Text waveLabel;
public GameObject[] nextWaveLabels;
The waveLabel
stores a reference to the wave readout at the top right corner of the screen. nextWaveLabels
stores the two GameObjects that when combined, create an animation you’ll show at the start of a new wave, as shown below:
Save the file and switch to Unity. Select GameManager in the Hierarchy. Click on the small circle to the right of Wave Label, and in the Select Text dialog, select WaveLabel in the Scene tab.
Now set the Size of Next Wave Labels to 2. Then assign Element 0 to NextWaveBottomLabel and Element 1 to NextWaveTopLabel the same way as you set Wave Label.
If the player has lost the game, he shouldn’t see the next wave message. To handle this, switch back to GameManagerBehavior.cs in your IDE and add another variable:
public bool gameOver = false;
In gameOver
you’ll store whether the player has lost the game.
Once again, you’ll use a property to keep the game’s elements in sync with the current wave. Add the following code to GameManagerBehavior
:
private int wave;
public int Wave
{
get
{
return wave;
}
set
{
wave = value;
if (!gameOver)
{
for (int i = 0; i < nextWaveLabels.Length; i++)
{
nextWaveLabels[i].GetComponent<Animator>().SetTrigger("nextWave");
}
}
waveLabel.text = "WAVE: " + (wave + 1);
}
}
Creating the private variable, property and getter should be second nature by now. But again, the setter is a bit trickier.
You update wave
with the new value
.
Then you check that the game is not over. If so, you iterate over all labels in nextWaveLabels — those labels have an Animator component. To trigger the animation on the Animator you set the trigger nextWave.
Lastly, you set waveLabel
‘s text
to the value of wave + 1
. Why the +1
? – Normal human beings do not start counting at zero. Weird, I know :]
In Start()
, set the value of this property:
Wave = 0;
You start counting at Wave
number 0.
Save the file, then run the scene in Unity. The Wave readout properly starts at 1.
Waves: Spawn, Spawn, Spawn
It sounds obvious, but you need to be able to create more enemies to unleash the hordes — right now you can’t do that. Furthermore, you shouldn’t spawn the next wave once the current wave is obliterated — at least for now.
So, the games must be able to recognize whether there are enemies in the scene, and Tags are a good way to identify game objects.
Set Enemy Tags
Select the Enemy prefab in the Project Browser. At the top of the Inspector, click on the Tag dropdown and select Add Tag.
Create a Tag named Enemy.
Select the Enemy prefab. In the Inspector, set its Tag to Enemy.
Define Enemy Waves
Now you need to define a wave of enemies. Open SpawnEnemy.cs in your IDE, and add the following class implementation before SpawnEnemy
:
[System.Serializable]
public class Wave
{
public GameObject enemyPrefab;
public float spawnInterval = 2;
public int maxEnemies = 20;
}
Wave holds an enemyPrefab
, the basis for instantiating all enemies in that wave, a spawnInterval
, the time between enemies in the wave in seconds and the maxEnemies
, which is the quantity of enemies spawning in that wave.
This class is Serializable, which means you can change the values in the Inspector.
Add the following variables to the SpawnEnemy
class:
public Wave[] waves;
public int timeBetweenWaves = 5;
private GameManagerBehavior gameManager;
private float lastSpawnTime;
private int enemiesSpawned = 0;
This sets up some variables for spawning that are quite similar to how you moved the enemies along waypoints.
You’ll define the game’s various waves in waves
, and track the number of enemies spawned and when you spawned them in enemiesSpawned
and lastSpawnTime
, respectively.
Players need breaks after all that killing, so set timeBetweenWaves
to 5 seconds
Replace the contents of Start()
with the following code.
lastSpawnTime = Time.time;
gameManager =
GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
Here you set lastSpawnTime
to the current time, which will be when the script starts as soon as the scene loads. Then you retrieve the GameManagerBehavior
in the familiar way.
Add this to Update()
:
// 1
int currentWave = gameManager.Wave;
if (currentWave < waves.Length)
{
// 2
float timeInterval = Time.time - lastSpawnTime;
float spawnInterval = waves[currentWave].spawnInterval;
if (((enemiesSpawned == 0 && timeInterval > timeBetweenWaves) ||
timeInterval > spawnInterval) &&
enemiesSpawned < waves[currentWave].maxEnemies)
{
// 3
lastSpawnTime = Time.time;
GameObject newEnemy = (GameObject)
Instantiate(waves[currentWave].enemyPrefab);
newEnemy.GetComponent<MoveEnemy>().waypoints = waypoints;
enemiesSpawned++;
}
// 4
if (enemiesSpawned == waves[currentWave].maxEnemies &&
GameObject.FindGameObjectWithTag("Enemy") == null)
{
gameManager.Wave++;
gameManager.Gold = Mathf.RoundToInt(gameManager.Gold * 1.1f);
enemiesSpawned = 0;
lastSpawnTime = Time.time;
}
// 5
}
else
{
gameManager.gameOver = true;
GameObject gameOverText = GameObject.FindGameObjectWithTag ("GameWon");
gameOverText.GetComponent<Animator>().SetBool("gameOver", true);
}
Go through this code step by step:
- Get the index of the current wave, and check if it’s the last one.
- If so, calculate how much time passed since the last enemy spawn and whether it’s time to spawn an enemy. Here you consider two cases. If it’s the first enemy in the wave, you check whether
timeInterval
is bigger thantimeBetweenWaves
. Otherwise, you check whethertimeInterval
is bigger than this wave’sspawnInterval
. In either case, you make sure you haven’t spawned all the enemies for this wave. - If necessary, spawn an enemy by instantiating a copy of
enemyPrefab
. You also increase theenemiesSpawned
count. - You check the number of enemies on screen. If there are none and it was the last enemy in the wave you spawn the next wave. You also give the player 10 percent of all gold left at the end of the wave.
- Upon beating the last wave this runs the game won animation.