Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

How to Create a Tower Defense Game in Unity – Part 2

$
0
0
Update note: This tutorial has been updated to Unity 2017.1 by Jeff Fisher. The original tutorial was written by Barbara Reichart.

blocks-twitter

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.

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

The bug now looks where it’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:

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.

Internally you start counting with 0, but for the player everything starts with wave 1.

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.

Set Spawn Intervals

Save the file and switch to Unity. Select Road in the Hierarchy. In the Inspector, set the Size of Waves to 4.

For now, set Enemy Prefab to Enemy for all four elements. Set the Spawn Interval and Max Enemies fields as follows:

  • Element 0: Spawn Interval: 2.5, Max Enemies: 5
  • Element 1: Spawn Interval: 2, Max Enemies: 10
  • Element 2: Spawn Interval: 2, Max Enemies: 15
  • Element 3: Spawn Interval: 1, Max Enemies: 5

The final setup should look like the screenshot below.

Waves
Of course, you can play around with those settings to increase or decrease the carnage.
Run the game. Ah-ha! The bugs are marching toward your cookie!

bugs

Optional: Add Different Types of Enemies

No tower defense game is complete with only one type of enemy. Luckily, the Prefabs folder contains another option, Enemy2.

Select Prefabs\Enemy2 in Inspector and add the MoveEnemy script to it. Set its Speed to 3 and its Tag to Enemy. You can now use this speedy bug to keep the player on his toes!

Update Player Health – Killing Me Softly

Even though hordes of bugs storm towards the cookie, the player takes no damage. But no more. The player should take a hit when he lets the enemy encroach.

Open GameManagerBehavior.cs in your IDE, and add the following two variables:

public Text healthLabel;
public GameObject[] healthIndicator;

You’ll use healthLabel to access the player’s health readout, and healthIndicator to access the five little green cookie-crunching monsters — they simply represent player health in a more fun way than a standard health label.

Manage Health

Next, add a property to maintain the player’s health in GameManagerBehavior:

private int health;
public int Health
{
  get
  {
    return health;
  }
  set
  {
    // 1
    if (value < health)
    {
      Camera.main.GetComponent<CameraShake>().Shake();
    }
    // 2
    health = value;
    healthLabel.text = "HEALTH: " + health;
    // 3
    if (health <= 0 && !gameOver)
    {
      gameOver = true;
      GameObject gameOverText = GameObject.FindGameObjectWithTag("GameOver");
      gameOverText.GetComponent<Animator>().SetBool("gameOver", true);
    }
    // 4
    for (int i = 0; i < healthIndicator.Length; i++)
    {
      if (i < Health)
      {
        healthIndicator[i].SetActive(true);
      }
      else
      {
        healthIndicator[i].SetActive(false);
      }
    }
  }
}

This manages the player's health. Once again, the bulk of the code is in the setter:

  1. If you're reducing the player's health, use the CameraShake component to create a nice shake effect. This script is included with the project and not covered here.
  2. Update the private variable and the health label in the top left corner of the screen.
  3. If health drops to 0 and it's not yet game over, set gameOver to true and trigger the GameOver animation.
  4. Remove one of the monsters from the cookie. If it just disabled them, this bit could be written more simply, but it also supports re-enabling them when you add health.

Initialize Health in Start():

Health = 5;

You set Health to 5 when the scene starts playing.

With this property in place, you can now update the player's health whenever a bug reaches the cookie. Save this file and then switch to MoveEnemy.cs, still in your IDE.

Update Health

To update the player's health, find the comment in Update() that reads // TODO: deduct health and replace it with this code:

GameManagerBehavior gameManager =
    GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
gameManager.Health -= 1;

This gets the GameManagerBehavior and subtracts one from its Health.

Save the file and switch to Unity.

Select GameManager in the Hierarchy and set its Health Label to HealthLabel.

Expand Cookie in the Hierarchy and drag and drop its five HealthIndicator children into GameManager's Health Indicator array -- the health indicators are the tiny green monsters happily eating their cookie.

Play the scene and wait for the bugs to reach the cookie. Do nothing until you lose.

cookie-attack

Monster Wars: The Revenge of the Monsters

Monsters in place? Check. Enemies advancing? Check — and they look mean! Time to mow those suckers down!

This requires several things:

  • A health bar, so the player knows which enemies are strong and weak
  • Detection of enemies within the range of a monster
  • Decision points -- which enemy to fire upon
  • Lots of bullets

Enemy Health Bar

You'll use two images to implement the health bar, one for a dark background and a slightly smaller green bar you'll scale to match the enemy's health.

Drag Prefabs\Enemy into the scene from the Project Browser.

Then drag Images\Objects\HealthBarBackground onto Enemy in the Hierarchy to add it as a child.

In the Inspector, set the Position for HealthBarBackground to (0, 1, -4).

Next, select Images\Objects\HealthBar in the Project Browser and ensure its Pivot is set to Left. Then, add it as a child of Enemy in the Hierarchy and set its Position to (-0.63, 1, -5). Set its X Scale to 125.

Add a new C# script named HealthBar to the HealthBar game object. Later, you'll edit it to adjust length of the health bar.

With Enemy selected in the Hierarchy, make sure it's position is (20, 0, 0).

Click on Apply at the top of the Inspector to save all your changes as part of the prefab. Finally, delete Enemy from the Hierarchy.

Bug with healthbar

Now, repeat those steps to add the health bar to Prefabs\Enemy2.

Adjust Health Bar Length

Open HealthBar.cs in your IDE, and add the following variables:

public float maxHealth = 100;
public float currentHealth = 100;
private float originalScale;

maxHealth stores the enemy's maximal health points, and currentHealth tracks how much health remains. Lastly, originalScale remembers the health bar's original size.

Store the object's originalScale in Start():

originalScale = gameObject.transform.localScale.x;

You save the localScale's x value.

Set the health bar's scale by adding the following to Update():

Vector3 tmpScale = gameObject.transform.localScale;
tmpScale.x = currentHealth / maxHealth * originalScale;
gameObject.transform.localScale = tmpScale;

You copy localScale to a temporary variable because you cannot adjust only its x value. Then, calculate a new x scale based on the bug's current health, and set the temporary variable back on localScale.

Save the file and run the game in Unity. You'll see health bars above the enemies.

Resistance is futile! - Wait, what resistance?

While the game runs, expand one of the Enemy(Clone) objects in the Hierarchy and select its HealthBar child. Change its Current Health value and check for that health bar to change.

AdjustHealthbar

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.

Bildschirmfoto 2015-06-05 um 14.47.15

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.

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;
  }
}
  1. In OnEnemyDestroy, you remove the enemy from enemiesInRange. When an enemy walks on the trigger around your monster OnTriggerEnter2D is called.
  2. You then add the enemy to the list of enemiesInRange and add OnEnemyDestroy to the EnemyDestructionDelegate. This makes sure that OnEnemyDestroy is called when the enemy is destroyed. You don't want monsters to waste ammo on dead enemies now -- do you?
  3. In OnTriggerExit2D you remove the enemy from the list and unregister your delegate. Now you know which enemies are in range.
  4. 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);
    }
    
    1. You calculate the new bullet position using Vector3.Lerp to interpolate between start and end positions.
    2. If the bullet reaches the targetPosition, you verify that target still exists.
    3. You retrieve the target's HealthBar component and reduce its health by the bullet's damage.
    4. If the health of the enemy falls to zero, you destroy it, play a sound effect and reward the player for marksmanship.

    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.

    Bullet prefabs - size increases with level

    Bullet prefabs - size increases with level

    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:

    MonsterData with bullets

    Bullets to kill your enemies? - Check! Open fire!

    Pew Pew - lasers ! (from Gisela Giardino)

    Pew Pew - lasers ! (from Gisela Giardino)

    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);
    }
    
    1. 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.
    2. Instantiate a new bullet using the bulletPrefab for MonsterLevel. Assign the startPosition and targetPosition of the bullet.
    3. Make the game juicier: Run a shoot animation and play a laser sound whenever the monster shoots.

    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.

    1. 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.
    2. Call Shoot if the time passed is greater than the fire rate of your monster and set lastShotTime to the current time.
    3. 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 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:

  • 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.

The post How to Create a Tower Defense Game in Unity – Part 2 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>