This is the second part of the tutorial
"Creating a Tower Defense Game in Unity .
" We are creating a tower defense game in Unity, and by the end of the
first part , we learned how to place and upgrade monsters. We also have one enemy attacking cookies.
However, the enemy does not know where to look! In addition, the attack alone looks weird. In this part of the tutorial we will add waves of enemies and arm the monsters so that they can protect the precious cookies.
Getting Started
Open in Unity the project on which we stopped in the last part. If you have joined us just now, then download
the blank project and open
TowerDefense-Part2-Starter .
Open
GameScene from the
Scenes folder.
Turn the enemies
At the end of the previous tutorial, the enemy learned to move along the road, but it seems he has no idea where to look.
Open the
MoveEnemy.cs script in the IDE and add the following method to it to correct the situation.
private void RotateIntoMoveDirection() {
RotateIntoMoveDirection
turns the enemy so that he always
RotateIntoMoveDirection
ahead. He does this as follows:
- Calculates the current direction of movement of the beetle, subtracting the position of the current route point from the position of the next point.
- Uses
Mathf.Atan2
to determine the angle in radians, in which newDirection
directed (the zero point is on the right). Multiplies the result by 180 / Mathf.PI
, converting the angle into degrees. - Finally, it gets the child object of the Sprite and turns it on
rotationAngle
degrees along the axis. Notice that we rotate the child , not the parent object, so that the strip of energy that we add later remains horizontal.
In
Update()
, we replace the comment
// TODO:
following call to
RotateIntoMoveDirection
:
RotateIntoMoveDirection();
Save the file and return to Unity. Run the scene; now the enemy knows where he is going.
Now the bug knows where it is going.
A single enemy does not look very impressive. We need hordes! And as in any tower defense game, hordes run in waves!
We inform the player
Before we begin to move the hordes, we need to warn the player about the impending battle. In addition, it is necessary to display the current wave number at the top of the screen.
Wave information is required by several GameObjects, so we will add it to the
GameManagerBehavior component of the
GameManager object.
Open
GameManagerBehavior.cs in the IDE and add the following two variables:
public Text waveLabel; public GameObject[] nextWaveLabels;
waveLabel
stores a link to the wave number label in the upper right corner of the screen.
nextWaveLabels
stores two GameObjects that create an animation in combination, which we will show at the beginning of a new wave:
Save the file and return to Unity. Select
GameManager in the
Hierarchy . Click the circle to the right of the
Wave Label and in the
Select Text dialog box, select
WaveLabel from the
Scene tab.
Now set the
Size for
Next Wave Labels to
2 . Now select
NextWaveBottomLabel for
Element 0 , and
NextWaveTopLabel for
Element 1 just as we did with Wave Label.
This is how Game Manager Behavior should now look.If a player loses, he should not see a message about the next wave. To handle this situation, go back to
GameManagerBehavior.cs and add another variable:
public bool gameOver = false;
In
gameOver
we will keep the value of whether the player has lost.
Here we again use the property to synchronize the elements of the game 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 a private variable, property, and getter should already be a familiar action for you. But with the setter again everything is a little more interesting.
We assign
wave
new
value
.
Then we check if the game is over. If not, then loop
around all the labels
nextWaveLabels - these labels have a component
Animator . To enable animation
Animator we set the
nextWave trigger.
Finally, we set the
text
at
waveLabel
to
wave + 1
. Why
+1
? Ordinary people do not start counting from scratch (yes, this is strange).
In
Start()
set the value of this property:
Wave = 0;
We start with the account number
0 Wave
.
Save the file and run the scene in Unity. The Wave label will correctly display 1.
For a player, it all starts with wave 1.Waves: creating heaps of enemies
It may seem obvious, but to attack the horde you need to create more enemies - as long as we do not know how to do it. Moreover, we should not create the next wave until the current wave is destroyed.
That is, the game should be able to recognize the presence of enemies in the scene and
tags are a good way to identify game objects here.
Setting enemy tags
Select the
Enemy prefab in the Project Browser. At the top of the
Inspector, click on the
Tag drop-down list and select
Add Tag .
Create a
Tag called
Enemy .
Select prefab
Enemy . In the
Inspector, set
the Enemy tag for it.
Setting waves of enemies
Now we need to set a wave of enemies. Open
SpawnEnemy.cs in the IDE and add the following class implementation in front of
SpawnEnemy
:
[System.Serializable] public class Wave { public GameObject enemyPrefab; public float spawnInterval = 2; public int maxEnemies = 20; }
Wave contains
enemyPrefab
- the basis for creating instances of all enemies in this wave,
spawnInterval
- the time between enemies in a wave in seconds and
maxEnemies
- the number of enemies created in this wave.
The class is
Serializable , that is, we can change its 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;
Here we set variables for spawning enemies, which is very similar to how we moved enemies between waypoints.
We set waves for individual enemies in the
waves
and track the number of created enemies and the time they were created in
enemiesSpawned
and
lastSpawnTime
.
After all these kills, players need time to take a breather, so we
timeBetweenWaves
5 seconds for
timeBetweenWaves
.
Replace the contents of
Start()
following code.
lastSpawnTime = Time.time; gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
Here we assign
lastSpawnTime
value of the current time, that is, the time the script is run after the scene is loaded. Then we get the already familiar way
GameManagerBehavior
.
Add the following code to
Update()
:
Let's sort it out step by step:
- We obtain the index of the current wave and check whether it is the last one.
- If yes, then we calculate the time elapsed after the previous spawn of the enemies and check whether it is time to create the enemy. Here we consider two cases. If this is the first enemy in the wave, then we check whether
timeInterval
is more than timeBetweenWaves
. Otherwise, we check if the timeInterval
is timeInterval
than the spawnInterval
wave. In any case, we check that we have not created all the enemies in this wave. - If necessary, spawn the enemy, creating an instance of
enemyPrefab
. Also increase the value of enemiesSpawned
. - Check the number of enemies on the screen. If they are not there, and this was the last enemy in the wave, then we create the next wave. Also at the end of the wave, we give the player 10 percent of the remaining gold.
- After the victory over the last wave, the victory animation in the game is played here.
Setting spawn intervals
Save the file and return to Unity. Select the
Road object in the
Hierarchy . In the
Inspector, set the
Size of the
Waves object to
4 .
For now, select the
Enemy object for all four elements as the
Enemy Prefab . Configure the
Spawn Interval and
Max Enemies fields as follows:
- Element 0 : Spawn Interval: 2.5 , Max Energy: 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 finished circuit should look like this:
Of course, you can experiment with these values to increase or decrease the complexity.
Run the game. Aha Beetles began the way to your cookie!
Additional task: add different types of enemies
No tower defense game can be considered complete with only one type of enemy. Fortunately, in the
Prefabs folder
there is also
Enemy2 .
In the
Inspector, select
Prefabs \ Enemy2 and add the
MoveEnemy script to it. Set the
Speed to
3 and set
the Enemy tag . Now you can use this fast enemy so that the player does not relax!
Player life upgrade
Even though hordes of enemies attack cookies, the player does not take any damage. But soon we will fix it. The player must suffer if he allows the enemy to sneak up.
Open
GameManagerBehavior.cs in the IDE and add the following two variables:
public Text healthLabel; public GameObject[] healthIndicator;
We use
healthLabel
to access the value of a player’s life, and
healthIndicator
to access five little green monsters chewing on cookies — they simply symbolize a player’s health; it's funnier than a standard health indicator.
Health management
Now add a property that stores the player's health in
GameManagerBehavior
:
private int health; public int Health { get { return health; } set {
So we manage the health of the player. And again the main part of the code is located in the setter:
- If we reduce the health of the player, then use the
CameraShake
component to create a beautiful shaking effect. This script is included in the downloadable project and we will not consider it here. - Update the private variable and health label in the upper left corner of the screen.
- If your health has dropped to 0 and the end of the game has not yet arrived, then
gameOver
to true
and start the gameOver
animation. - We remove one of the monsters with cookies. If we simply turn them off, then this part can be written easier, but here we support re-enabling in case of adding health.
Initialize
Health
to
Start()
:
Health = 5;
We set
Health
to
5
when the scene begins to play.
Having done all this, we can now update the health of the player when the beetle gets to the cookies. Save the file and go to the IDE script
MoveEnemy.cs .
Health change
To change your health, find a comment in
Update()
with the words
// TODO:
and replace it with this code:
GameManagerBehavior gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>(); gameManager.Health -= 1;
This is how we get
GameManagerBehavior
and subtract one
Health
from it.
Save the file and return to Unity.
Select the
GameManager object from
Hierarchy and select the
HealthLabel value for its
Health Label .
Deploy the Cookie object in
Hierarchy and drag five of its children
HealthIndicator into the
GameManager's Health Indicator array - health indicators are small green monsters eating cookies.
Start the scene and wait until the bugs reach the cookies. Do not do anything until you lose.
Revenge of the Monsters
Monsters in place? Yes. Enemies are attacking? Yes, and they look ominous! It is time to respond to these animals!
For this we need the following:
- Health band, so that the player knows which enemies are strong and which are weak
- Detection of enemies in the radius of the monster
- Deciding which enemy to shoot at
- Pile of shells
Enemy health band
To implement the health bar, we use two images - one for a dark background, and the second (the green bar a bit smaller) will be scaled according to the health of the enemy.
Drag from the
Project Browser into the
Prefabs \ Enemy scene.
Then in the
Hierarchy, drag
Images \ Objects \ HealthBarBackground to
Enemy to add it as a child.
In the
Inspector, set the
Position of the
HealthBarBackground to
(0, 1, -4) .
Then in the
Project Browser, select
Images \ Objects \ HealthBar and make sure that its
Pivot is set to
Left . Then add it as a child element of
Enemy in the
Hierarchy and set its
Position value
(-0.63, 1, -5) . For the
X scale
Scale, set the value to
125 .
Add a new
C # script called
HealthBar to the HealthBar game object. Later we will change it to change the length of the health bar.
After selecting the
Enemy object in
Hierarchy , make sure that its position is
(20, 0, 0) .
Click
Apply at the top of the
Inspector to save all changes as part of the prefab. Finally, delete the
Enemy object in
Hierarchy .
Now repeat all these steps to add a health bar for
Prefabs \ Enemy2 .
Change the length of the health band
Open
HealthBar.cs in IDE and add the following variables:
public float maxHealth = 100; public float currentHealth = 100; private float originalScale;
maxHealth
stores the maximum health of the enemy, and
currentHealth
remaining health. Finally, in the
originalScale
is the initial size of the health bar.
Store the
originalScale
object in
Start()
:
originalScale = gameObject.transform.localScale.x;
We store the
x
value of the
localScale
property.
Set the scale of the health band by adding the following code to
Update()
:
Vector3 tmpScale = gameObject.transform.localScale; tmpScale.x = currentHealth / maxHealth * originalScale; gameObject.transform.localScale = tmpScale;
We can copy
localScale
to a temporary variable, because we cannot change its
x value separately. Then we calculate the new scale by
x based on the current health of the beetle and
localScale
value of the temporary variable to localScale again.
Save the file and run the game in Unity. Above the enemies you will see the strip of health.
While the game is running, in
Hierarchy , expand one of the
Enemy (Clone) objects and select its child element
HealthBar . Change its
Current Health value and see how its health band changes.
Detect enemies in range
Now our monsters need to know which enemies to aim at. But before you realize this opportunity, you must prepare Monster and Enemy.
Select in the Project Browser
Prefabs \ Monster and add the
Circle Collider 2D component to it in the
Inspector .
Set the
Radius parameter of the collider to
2.5 - so we will specify the attack range of the monsters.
Check the
Is Trigger checkbox so that objects pass through this area and not collide with it.
Finally, at the top of the
Inspector , select
Ignore Raycast for the Monster
Layer object. Click in the dialog box on
Yes, change children . If you do not select Ignore Raycast, the collider will respond to mouse click events. This will be a problem because monsters block events destined for Openspot objects under them.
To ensure that the enemy is detected in the trigger area, we need to add a collider and a rigid body to it, because Unity only sends trigger events when a rigid body is attached to one of the colliders.
In the
Project Browser, select
Prefabs \ Enemy . Add a
Rigidbody 2D component and select
Kinematic for
Body Type . This means that the body will not be affected by physics.
Add a
Circle Collider 2D with a
Radius of
1 . Repeat these steps for
Prefabs \ Enemy 2 .
The triggers are set up, so the monsters will realize that the enemies are within range.
We need to prepare one more thing: a script informing monsters when an enemy is destroyed so that they do not cause an exception while continuing to fire.
Create a new
C # script called
EnemyDestructionDelegate and add it to the
Enemy and
Enemy2 prefabs .
In
EnemyDestructionDelegate.cs, open the IDE and add the following delegation declaration:
public delegate void EnemyDelegate (GameObject enemy); public EnemyDelegate enemyDelegate;
Here we create a
delegate
(delegate), that is, a container for a function that can be passed as a variable.
Note : delegates are used when one game object must actively notify other game objects about changes. Read more about delegates in the Unity documentation .
Add the following method:
void OnDestroy() { if (enemyDelegate != null) { enemyDelegate(gameObject); } }
When a game object is destroyed, Unity automatically calls this method and checks the delegate for
null
inequality. In our case, we call it with a
gameObject
as a parameter. This allows all respondents registered as delegates to know that the enemy has been destroyed.
Save the file and return to Unity.
We give the monsters a license to kill
And now monsters can detect enemies within their range. Add a new
C # script to the
Monster prefab and name it
ShootEnemies .
In IDE,
open ShootEnemies.cs and add the following
using
construct to it to get access to
Generics
.
using System.Collections.Generic;
Add a variable to track all enemies in range:
public List<GameObject> enemiesInRange;
In
enemiesInRange
we will keep all enemies within range.
Initialize the field in
Start()
.
enemiesInRange = new List<GameObject>();
At the very beginning there are no enemies in the radius of action, so we create an empty list.
Fill in the
enemiesInRange
list! Add this code to the script:
- In
OnEnemyDestroy
we remove the enemy from enemiesInRange
. When an enemy OnTriggerEnter2D
trigger around a monster, OnTriggerEnter2D
is OnTriggerEnter2D
. - Then we add the enemy to the list of
enemiesInRange
and add the EnemyDestructionDelegate
event OnEnemyDestroy
. So we guarantee that OnEnemyDestroy
will be caused when the enemy is OnEnemyDestroy
. We don't want the monsters to spend their ammo on dead enemies, right? - In
OnTriggerExit2D
we remove the enemy from the list and unregister the delegate. Now we know which enemies are in range.
Save the file and run the game in Unity. To make sure everything works, locate the monster, select it and follow the
enemiesInRange
changes in the list of
enemiesInRange
.
Target selection
Now the monsters know which enemy is in range. But what will they do when there are several enemies in a radius?
Of course, they will attack the closest to the liver!
Open the IDE script
MoveEnemy.cs and add a new method that evaluates this monster:
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; }
The code calculates the length of the path, not yet passed by the enemy. For this, he uses
Distance
, which is calculated as the distance between two
Vector3
instances.
We will use this method later to figure out which target to attack. However, as long as our monsters are not armed and helpless, so first we will deal with it.Save the file and return to Unity to begin setting up the shells.We give the monsters shells. Lots of shells!
Drag from the Project Browser to the Images / Objects / Bullet1 scene . Set the position for z to -2 - the positions to x and y are not important, because we set them every time we create a new projectile while executing the program.Add a new C # script called BulletBehavior , and then add the following variables to the 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 the speed of the projectiles; The purpose is damage
clear from the title.target
, startPosition
and targetPosition
determine the direction of the projectile.distance
and startTime
track the current position of the projectile. gameManager
rewards the player when he kills the enemy.Assign the values of these variables to Start()
: startTime = Time.time; distance = Vector2.Distance (startPosition, targetPosition); GameObject gm = GameObject.Find("GameManager"); gameManager = gm.GetComponent<GameManagerBehavior>();
startTime
we set the current time value and calculate the distance between the initial and target positions. Also, as usual, we get GameManagerBehavior
.To control the movement of the projectile, add to the Update()
following code:
- We calculate the new position of the projectile, using
Vector3.Lerp
for interpolation between the initial and final positions. - If the projectile reaches
targetPosition
, then we check whether there is more target
. HealthBar
damage
.- , , .
Unity.
Wouldn't it be great if the monster starts shooting more shells at high levels? Fortunately, this is easy to implement.Drag the Bullet1 game object from the Hierarchy to the Project tab to create a projectile prefab. Remove the original object from the scene — we will no longer need it.Duplicate Bullet1 prefab twice . Name the copies of Bullet2 and Bullet3 .Select Bullet2 . In the Inspector, set the Sprite field of the Sprite Renderer component to Images / Objects / Bullet2. So we will make Bullet2 a little more than Bullet1.Repeat the procedure to change the prefab Bullet3 sprite to Images / Objects / Bullet3 .Next in the Bullet Behavior we configure the amount of damage caused by the shells.Select the Bullet1 prefab in the Project tab . In the Inspector you will see the Bullet Behavior (Script) , in which you can assign a Damage value of 10 for Bullet1 , 15 for Bullet2 and 20 for Bullet3 - or any other values you like.Note : I changed the values so that at higher levels the cost of damage becomes higher. This prevents the upgrade from allowing the player to upgrade monsters on the best spots.
Projectile Prefabs - Size Increases with LevelProjectile level change
Assign different projectiles to different levels of monsters so that stronger monsters destroy enemies faster.Open MonsterData.cs in the IDE and add it to the MonsterLevel
following variables: public GameObject bullet; public float fireRate;
So we will set the prefab of the projectile and the frequency of shooting for each level of monsters. Save the file and return to Unity to complete the monster setup.Select the Monster prefab in the Project Browser . In the Inspector, expand the Levels in the Monster Data (Script) component . Set each element's Fire Rate to 1 . Then set the Bullet parameter for Element 0, 1, and 2 to Bullet1 , Bullet2, and Bullet3 .Monster levels should be configured as follows:Shells kill enemies? Yes!
Let's open fire!Open fire
IDE
ShootEnemies.cs :
private float lastShotTime; private MonsterData monsterData;
, ,
MonsterData
, , .
Start()
:
lastShotTime = Time.time; monsterData = gameObject.GetComponentInChildren<MonsterData>();
lastShotTime
MonsterData
.
, :
void Shoot(Collider2D target) { GameObject bulletPrefab = monsterData.CurrentLevel.bullet;
- . z z
bulletPrefab
. z , , . - Create a copy of the new projectile with the
bulletPrefab
appropriate MonsterLevel
. Assign startPosition
and targetPosition
projectile. - Making the game more interesting: when the monster shoots, we start the shooting animation and play the laser sound.
Putting it all together
It is time to connect everything. Define the target and make the monster look at it.In the script ShootEnemies.cs add to Update()
this code: GameObject target = null;
Consider this code step by step.- Determine the purpose of the monster. We start with the maximum possible distance in
minimalEnemyDistance
. We go around in the cycle of all enemies in the radius of action and make the enemy a new target, if its distance to the cookies is less than the current smallest. - Call
Shoot
if the elapsed time is more than the frequency of the monster's shooting and set the lastShotTime
value of the current time. - Calculate the angle of rotation between the monster and its target. Rotate the monster at this angle. Now he will always look at the goal.
Save the file and run the game in Unity. Monsters will begin to desperately protect cookies. We are finally done!Where to go next
The finished project can be downloaded
from here .
We have done a great job in this tutorial and now we have a great game.Here are some ideas for further development of the project:- More types of enemies and monsters
- Different routes of enemies
- Different levels of the game
. , , , , .
tower defense
.