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.
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.
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!
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:
- 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. - Update the private variable and the health label in the top left corner of the screen.
- If health drops to 0 and it's not yet game over, set
gameOver
totrue
and trigger theGameOver
animation. - 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.
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.
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.
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.
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.
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;
}
}
- In
OnEnemyDestroy
, you remove the enemy fromenemiesInRange
. When an enemy walks on the trigger around your monsterOnTriggerEnter2D
is called. - You then add the enemy to the list of
enemiesInRange
and addOnEnemyDestroy
to theEnemyDestructionDelegate
. This makes sure thatOnEnemyDestroy
is called when the enemy is destroyed. You don't want monsters to waste ammo on dead enemies now -- do you? - In
OnTriggerExit2D
you remove the enemy from the list and unregister your delegate. Now you know which enemies are in range. - You calculate the new bullet position using
Vector3.Lerp
to interpolate between start and end positions. - If the bullet reaches the
targetPosition
, you verify thattarget
still exists. - You retrieve the target's
HealthBar
component and reduce its health by the bullet'sdamage
. - If the health of the enemy falls to zero, you destroy it, play a sound effect and reward the player for marksmanship.
- 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. - Instantiate a new bullet using the
bulletPrefab
forMonsterLevel
. Assign thestartPosition
andtargetPosition
of the bullet. - Make the game juicier: Run a shoot animation and play a laser sound whenever the monster shoots.
- 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. - Call
Shoot
if the time passed is greater than the fire rate of your monster and setlastShotTime
to the current time. - 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 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);
}
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.
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:
Bullets to kill your enemies? - Check! Open fire!
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);
}
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.
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.