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
Update note: This tutorial has been updated to Unity 2017.1 by Jeff Fisher. The original tutorial was written by Barbara Reichart.

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:

  1. It calculates the bug’s current movement direction by subtracting the current waypoint’s position from that of the next waypoint.
  2. It uses Mathf.Atan2 to determine the angle toward which newDirection points, in radians, assuming zero points to the right. Multiplying the result by 180 / Mathf.PI converts the angle to degrees.
  3. 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.

The bug now looks where it’s going.

Your bug should follow the road (Here sped up by factor 20, so it's more fun to watch)

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:

nextWaveAnimation

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.

This is what your Game Manager Behavior should look like

This is what your Game Manager Behavior should look like

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.

For the player everything starts with wave 1.

Internally you start counting with 0, but for the player everything starts with wave 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 Tag

Create a Tag named Enemy.

create a new tag

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:

  1. Get the index of the current wave, and check if it’s the last one.
  2. 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 than timeBetweenWaves. Otherwise, you check whether timeInterval is bigger than this wave’s spawnInterval. In either case, you make sure you haven’t spawned all the enemies for this wave.
  3. If necessary, spawn an enemy by instantiating a copy of enemyPrefab. You also increase the enemiesSpawned count.
  4. 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.
  5. Upon beating the last wave this runs the game won animation.
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.