Tower defense games are becoming increasingly popular, and this is not surprising - not much can be compared with the pleasure of observing your own lines of defense, destroying evil enemies! In this two-part tutorial, we will create a tower defense game on the
Unity engine!
You will learn how to do the following:
- Create waves of enemies
- Make them follow route points.
- Build and upgrade towers, as well as teach them how to break enemies into small pixels
At the end we will get a game frame that can be developed further!
Note : you need some basic knowledge of Unity (for example, you need to know how assets and components are added, what prefabs are) and the basics of the C # language. To learn all of this, I recommend that you go through the Unity tutorials on Sean Duffy or the Beginning C # with Unity series by Brian Mockley.
I will work in the Unity version for OS X, but this tutorial is also suitable for Windows.
Through the windows of the ivory tower
In this tutorial, we will create a tower defense game in which enemies (little bugs) crawl to the cookies belonging to you and your minions (of course, these are monsters!). The player can place monsters in strategic points and improve them for gold.
The player must kill all the bugs until they get to the cookie. Every new wave of enemies is harder to beat. The game ends when you survive all the waves (victory!) Or when five enemies crawl to the cookies (loss!).
Here is a screenshot of the finished game:
Monsters, unite! Protect the cookie!Getting Started
Download this project
stub , unpack it and open the
TowerDefense-Part1-Starter project in Unity.
In the preparation of the project there are graphics and sound assets, ready-made animations and several useful scripts. The scripts are not directly related to the tower defense games, so I’m not going to talk about them here. However, if you want to learn more about creating 2D animations in Unity, then learn this
tutorial on Unity 2D .
The project also contains prefabs, which we later add to create characters. Finally, there is a scene in the project with a background and a customized user interface.
Open
GameScene , located in the
Scenes folder, and set the Game mode to
4: 3 aspect ratio so that all labels match the background correctly. In Game mode, you will see the following:
Authorship:- Graphics for the project is taken from the free pack of Vika Wenderlich! Other graphic works can be found on her gameartguppy site.
- Great music taken from the BenSound website, which has other amazing soundtracks!
- Thanks also to Michael Jesper for a very useful camera shake function .
.
Place marked with a cross: the location of the monsters
Monsters can only be put on dots marked with
x .
To add them to the scene, drag
Images \ Objects \ Openspot from the
Project Browser to the
Scene window. While the position for us is not important.
After selecting
Openspot in the hierarchy, click on
Add Component in the
Inspector and select
Box Collider 2D . In the Scene window, Unity displays a rectangular collider with a green line. We will use this collider to recognize mouse clicks on this place.
Similarly, add the
Audio \ Audio Source component to
Openspot . For the Audio Source component's
AudioClip option,
select the tower_place file in the
Audio folder and turn off
Play On Awake .
We need to create 11 more points. Although there is a temptation to repeat all these steps, there is a better solution in Unity:
Prefab !
Drag
Openspot from the
Hierarchy to the
Prefabs folder inside the
Project Browser . His name will become blue in the Hierarchy, which means that it is attached to the prefab. Like that:
Now that we have a prefab blank, we can create as many copies as we like. Simply drag and drop
Openspot from the
Prefabs folder inside the
Project Browser to the
Scene window. Repeat this 11 times, and we will have 12 Openspot objects in the scene.
Now use the
Inspector to set the following coordinates to these 12 Openspot objects:
- (X: -5.2, Y: 3.5, Z: 0)
- (X: -2.2, Y: 3.5, Z: 0)
- (X: 0.8, Y: 3.5, Z: 0)
- (X: 3.8, Y: 3.5, Z: 0)
- (X: -3.8, Y: 0.4, Z: 0)
- (X: -0.8, Y: 0.4, Z: 0)
- (X: 2.2, Y: 0.4, Z: 0)
- (X: 5.2, Y: 0.4, Z: 0)
- (X: -5.2, Y: -3.0, Z: 0)
- (X: -2.2, Y: -3.0, Z: 0)
- (X: 0.8, Y: -3.0, Z: 0)
- (X: 3.8, Y: -3.0, Z: 0)
When you do this, the scene will look like this:
Placing monsters
To simplify placement, there is a
Monster prefab in the
Prefab project folder.
Prefab Monster is ready to use.At the moment it consists of an empty game object with three different sprites and shooting animations as child elements.
Each sprite is a monster with different levels of power. Also in the prefab contains the component
Audio Source , which will be launched to play the sound when the monster shoots a laser.
Now we will create a script that will be placed by
Monster on
Openspot .
In the
Project Browser, select the
Openspot object in the
Prefabs folder. In the
Inspector, click on
Add Component , then select
New Script and name the script
PlaceMonster . Select
Sharp as the
C language and click on
Create and Add . Since we added the script to the
Openspot prefab, all Openspot objects in the scene will now have this script. Fine!
Double-click the script to open it in the IDE. Then add two variables:
public GameObject monsterPrefab; private GameObject monster;
We will create an instance of the object stored in
monsterPrefab
to create a monster, and save it in
monster
so that you can manipulate it during the game.
One monster per point
To place only one monster on one point, add the following method:
private bool CanPlaceMonster() { return monster == null; }
In
CanPlaceMonster()
we can check whether the
monster
variable is still
null
. If so, then there is no monster at the point, and we can place it.
Now add the following code to place the monster when the player clicks on this GameObject:
This code places the monster when you click the mouse or touch the screen. How does he work?
- Unity automatically calls
OnMouseUp
when the player touches the physical collider GameObject. - When called, this method puts a monster if
CanPlaceMonster()
returns true
. - We create a monster using the
Instantiate
method, which creates an instance of a given prefab with the specified position and rotation. In this case, we copy monsterPrefab
, set the current position of the GameObject and the absence of rotation to it, transfer the result to the GameObject
and save it to monster
- At the end, we call
PlayOneShot
to play a sound effect attached to the AudioSource
component of the object.
Now our
PlaceMonster
script may have a new monster, but we still need to specify the prefab.
Using the right prefab
Save the file and return to Unity.
To set the
monsterPrefab variable, first select the
Openspot object from the
Prefabs folder in the project browser.
In the
Inspector, click on the circle to the right of the
Monster Prefab field of the
PlaceMonster (Script) component and select
Monster in the dialog that appears.
That's all. Run the scene and create monsters in different places by clicking the mouse or touching the screen.
Fine! Now we can create monsters. However, they look like weird porridge, because all the child sprites of the monster are drawn. Now we fix it.
Raise the level of monsters
The figure below shows that as the level rises the monsters look more and more frightening.
What a sweetheart! But if you try to steal his cookies, this monster will turn into a killer.The script is used as the basis for the implementation of the system of levels of monsters. He tracks the power of the monster at each level and, of course, the current level of the monster.
Add this script.
Select the
Prefabs / Monster prefab in the
Project Browser . Add a new
C # script called
MonsterData . Open the script in the IDE and add the following code
above the
MonsterData
class.
[System.Serializable] public class MonsterLevel { public int cost; public GameObject visualization; }
So we create
MonsterLevel
. It groups the price (in gold, which we will support below) and the visual representation of the monster level.
We add on top
[System.Serializable]
so that instances of the class can be changed in the inspector. This allows us to quickly change all values of the Level class, even when the game is running. This is incredibly useful for balancing the game.
Setting monster levels
In our case, we will store the specified
MonsterLevel
in
List<T>
.
Why not just use
MonsterLevel[]
? We need the index of a particular
MonsterLevel
object several times. Although it is easy to write code for this, we still have to use
IndexOf()
, which implements the
Lists
functionality. It makes no sense to reinvent the wheel.
Reinventing a bicycle is usually a bad idea.At the top of
MonsterData.cs, add the following
using
construct:
using System.Collections.Generic;
It gives us access to generalized data structures so that we can use the
List<T>
class in the script.
Note : generalizations are a powerful concept for C #. They allow you to set type-safe data structures without adhering to the type. This is useful for container classes such as lists and sets. To learn more about generalized structures, read the book Introduction to C # Generics .
Now add the following variable to
MonsterData
to store the
MonsterLevel
list:
public List<MonsterLevel> levels;
Thanks to generalizations, we can guarantee that the
List
from
level
will contain only
MonsterLevel
objects.
Save the file and switch to Unity to configure each level.
Select
Prefabs / Monster in the
Project Browser . The
Inspector now displays the
Levels field of the
MonsterData (Script) component. Set the
size parameter to
3 .
Next, set the
cost for each level:
- Element 0 : 200
- Element 1 : 110
- Element 2 : 120
Now assign the values of the visual display fields.
Deploy the
Prefabs / Monster in the Project browser to see its children. Drag a child
Monster0 into the
visualization Element 0 field.
Next, set
Element 1 to
Monster1 , and
Element 2 to
Monster2 . The GIF shows this process:
When you select
Prefabs / Monster , the prefab should look like this:
Setting current level
Go back to
MonsterData.cs in the IDE and add another variable to
MonsterData
.
private MonsterLevel currentLevel;
In the private variable
currentLevel
we will store the current level of the monster.
Now we will set
currentLevel
and make it visible for other scripts. Add the following lines to
MonsterData
along with the declaration of the instance variables:
Quite a big piece of C # code, right? Let's sort it out in order:
- Set the property of the private variable
currentLevel
. By setting a property, we can call it like any other variable: either as CurrentLevel
(inside the class) or as monster.CurrentLevel
(outside). We can define any behavior in a getter or setter method, and by creating only a getter, setter, or both, we can control the characteristics of the property: read-only, write-only, and write / read. - In the getter, we return the
currentLevel
value. - In the setter, we assign the
currentLevel
new value. Then we get the index of the current level. Finally, we loop through all levels and turn on / off the visual display depending on the currentLevelIndex
. This is great because when you change the currentLevel
sprite is updated automatically. Properties is a very handy thing!
Add the following
OnEnable
implementation:
void OnEnable() { CurrentLevel = levels[0]; }
Here we set the
CurrentLevel
when placing. This ensures that only the desired sprite will be shown.
Note : It is important to initialize the property in OnEnable
, and not in OnStart
, because we call ordinal methods when creating prefab instances.
OnEnable
will be called immediately when creating a prefab (if the prefab was saved in the enabled state), but OnStart
not called until the object starts to run as part of the scene.
We need to verify this data before placing the monster, so we initialize it to OnEnable
.
Save the file and return to Unity. Run the project and position the monsters; they now display the correct sprites of the lowest level.
Monster upgrade
Return to the IDE and add the following method to
MonsterData
:
public MonsterLevel GetNextLevel() { int currentLevelIndex = levels.IndexOf (currentLevel); int maxLevelIndex = levels.Count - 1; if (currentLevelIndex < maxLevelIndex) { return levels[currentLevelIndex+1]; } else { return null; } }
In
GetNextLevel
we get the
currentLevel
index and the highest level index; if the monster has not reached the maximum level, the next level is returned. Otherwise, it returns
null
.
You can use this method to find out if monster upgrades are possible.
To increase the level of the monster, add the following method:
public void IncreaseLevel() { int currentLevelIndex = levels.IndexOf(currentLevel); if (currentLevelIndex < levels.Count - 1) { CurrentLevel = levels[currentLevelIndex + 1]; } }
Here we get the index of the current level, and then we are convinced that this is not the maximum level, checking that it is less than
levels.Count - 1
. If so, then assign the
CurrentLevel
value to the next level.
Checking upgrades functionality
Save the file and return to
PlaceMonster.cs in the IDE. Add a new method:
private bool CanUpgradeMonster() { if (monster != null) { MonsterData monsterData = monster.GetComponent<MonsterData>(); MonsterLevel nextLevel = monsterData.GetNextLevel(); if (nextLevel != null) { return true; } } return false; }
First we check if there is a monster that can be improved by comparing the
monster
variable with
null
. If this is true, then we get the current level of the monster from its
MonsterData
.
Then we check if the next level is available, that is, whether
GetNextLevel()
returns a
null
value. If elevation is possible, then we return
true
; otherwise, return
false
.
Implement improvements for gold
To enable the upgrade option, add an
else if
branch to
OnMouseUp
:
if (CanPlaceMonster()) {
Check for upgradeability with
CanUpgradeMonster()
. If possible, we access the
MonsterData
component using
GetComponent()
and call
IncreaseLevel()
, which increases the monster level. Finally, we run the monster's
AudioSource .
Save the file and return to Unity. Start the game, place and improve
any number of monsters (but this is for now).
Pay Gold - Game Manager
While we can immediately build and improve any monsters, but will it be interesting in the game?
Let's look at the question of gold. The problem with tracking it is that we have to transfer information between different game objects.
The figure below shows all the objects that should take part in this.
All selected game objects must know how much gold the player has.To store this data, we will use a shared object that other objects can access.
Right-click on the
Hierarchy and select
Create Empty . Name the new
GameManager object.
Add a new
C # script to
GameManager called
GameManagerBehavior , and then open it in the IDE. We will display the total amount of player gold in the label, so add the following line at the top of the file:
using UnityEngine.UI;
This will allow us to access UI classes like
Text
, which is used for labels. Now add the following variable to the class:
public Text goldLabel;
It will contain a link to the
Text
component used to display the amount of gold the player has.
Now that
GameManager
knows about the label, how do we synchronize the amount of gold stored in a variable and the value displayed in the label? We will create a property.
Add the following code to
GameManagerBehavior
:
private int gold; public int Gold { get { return gold; } set { gold = value; goldLabel.GetComponent<Text>().text = "GOLD: " + gold; } }
Does he seem familiar? The code is similar to
CurrentLevel
, which we set in
Monster
. First we create a private variable
gold
to store the current amount of gold. Then we set the
Gold
property (unexpectedly, right?) And implement the getter and setter.
Getter simply returns the value of
gold
. Setter is more interesting. In addition to setting the value of a variable, it also sets the
text
field for
goldLabel
to display the new gold value.
How generous will we be? Add the following line to
Start()
to give the player
1000 gold, or less if you feel sorry for the money:
Gold = 1000;
Assigning a Label Object to a Script
Save the file and return to Unity. In the
Hierarchy, select the
GameManager . In the
Inspector, click on the circle to the right of the
Gold Label . In the
Select Text dialog box, select the
Scene tab and select
GoldLabel .
Run the scene and
Gold: 1000 appears in the label.
Check the player's wallet
Open the
PlaceMonster.cs script in the IDE and add the following instance variable:
private GameManagerBehavior gameManager;
We will use the
gameManager
to access the
GameManagerBehavior
component of the
GameManagerBehavior
object. To set it, add the following to
Start()
:
gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
We get a GameObject called GameManager using the
GameObject.Find()
function, which returns the first found game object with that name. Then we get its
GameManagerBehavior
component and save it for the future.
Note : you can do this by specifying a field in the Unity editor or by adding a static method to GameManager
, which returns a singleton instance from which we can get GameManagerBehavior
.
However, in the block of code shown above, there is a dark horse: the Find
method, which works slower during the execution of an application; but it is convenient and can be used in moderation.
Take my money!
We are not subtracting gold yet, so we
OnMouseUp()
add this line to
OnMouseUp()
twice , replacing each of the comments
// TODO:
:
gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost;
Save the file and return to Unity, upgrade a few monsters, and look at updating the Gold value. Now we subtract gold, but players can build monsters as long as they have enough space; they just borrow money.
Infinite credit? Fine! But we cannot allow it. The player must be able to put monsters, as long as he has enough gold.Gold check for monsters
Switch to IDE with
PlaceMonster.cs and replace the contents of
CanPlaceMonster()
following:
int cost = monsterPrefab.GetComponent<MonsterData>().levels[0].cost; return monster == null && gameManager.Gold >= cost;
MonsterData
placement price of the monster from
levels
in its
MonsterData
. Then we check that
monster
not
null
, and that
gameManager.Gold
greater than this price.
The task for you is to add a
CanUpgradeMonster()
to
CanUpgradeMonster()
yourself if the player has enough gold.
Solution insideReplace the line:
return true;
on this:
return gameManager.Gold >= nextLevel.cost;
She will check if the player has more
Gold than the upgrade price.
Save and run the scene in Unity. Now try-how to add unlimited monsters!
Now we can build only a limited number of monsters.Tower politics: enemies, waves and waypoints
It is time to “pave the way” to our enemies. Enemies appear on the first point of the route, move to the next and repeat the process until they reach the cookie.
You can make the enemies move like this:
- Ask the way for the enemies to walk
- Move the enemy along the way
- Turn the enemy so that he looks forward
Creating a road from route points
Right-click on the
Hierarchy and select
Create Empty to create a new empty game object. Call it
Road and position it at
(0, 0, 0) .
Now right-click on the
Road in
Hierarchy and create another empty game object as a child of the Road. Call it
Waypoint0 and place it at
(-12, 2, 0) - from here the enemies will begin their movement.
Similarly, create five more route points with the following names and positions:
- Waypoint1: (X: 7, Y: 2, Z: 0)
- Waypoint2: (X: 7, Y: -1, Z: 0)
- Waypoint3: (X: -7.3, Y: -1, Z: 0)
- Waypoint4: (X: -7.3, Y: -4.5, Z: 0)
- Waypoint5: (X: 7, Y: -4.5, Z: 0)
The screenshot below shows the route points and the resulting path.
Making enemies
Now create several enemies so that they can move along the road. In the
Prefabs folder
there is an
Enemy prefab. Its position is
(-20, 0, 0) , so new instances will be created off-screen.
Otherwise, it is configured in much the same way as the Monster prefab, has an
AudioSource
and a child
Sprite
, and we will be able to rotate this sprite without turning the health bar.
Move the enemies along the way
Add a new
C # script called
MoveEnemy to the
Prefabs \ Enemy prefab . Open the script in the IDE and add the following variables:
[HideInInspector] public GameObject[] waypoints; private int currentWaypoint = 0; private float lastWaypointSwitchTime; public float speed = 1.0f;
A copy of the route points is stored in the
waypoints
in the array, and the
[HideIn inspector ]
above the
waypoints
ensures that we cannot accidentally change this field in the
Inspector , but still will have access to it from other scripts.
currentWaypoint
keeps track of where in the route the enemy is heading at the current time, and the
lastWaypointSwitchTime
keeps
lastWaypointSwitchTime
the time when the enemy has passed through it. In addition, we keep the
speed
enemy.
Add this line to
Start()
:
lastWaypointSwitchTime = Time.time;
So we initialize
lastWaypointSwitchTime
with the current time value.
To make the enemy move along the route, add the following code to
Update()
:
Let's sort the code step by step:
- From the array of route points we get the starting and ending positions of the current route segment.
- We calculate the time required to travel the entire distance using the formula time = distance / speed , and then we determine the current time on the route. Using
Vector2.Lerp
, we interpolate the current enemy position between the starting and ending exact segments. - Check whether the enemy has reached
endPosition
. If yes, then we process two possible scenarios:
- The enemy has not yet reached the last point of the route, so we increase the value of
currentWaypoint
and update lastWaypointSwitchTime
. Later we will add a code to rotate the enemy so that he looks in the direction of his movement. - The enemy has reached the last point of the route, then we destroy it and launch the sound effect. Later we will add a code that reduces the player’s
health
.
Save the file and return to Unity.
We inform the enemy the direction of movement
In their current state, the enemies do not know the order of the route points.
Select
Road in the
Hierarchy and add a new
C # script called
SpawnEnemy . Open it in the IDE and add the following variable:
public GameObject[] waypoints;
We will use
waypoints
to store links to the waypoint in the scene in the correct order.
Save the file and return to Unity. Select
Road in the
Hierarchy and set the
Size of the
Waypoints array to
6 .
Drag each of the Road children into the fields by inserting
Waypoint0 into
Element 0 ,
Waypoint1 into
Element 1, and so on.
Now we have an array containing the route points in the correct order - note, the enemies never retreat, they persevere in a sweet reward.
Checking how it all works.
Open the
SpawnEnemy IDE and add the following variable:
public GameObject testEnemyPrefab;
It will contain a link to the
Enemy testEnemyPrefab
in
testEnemyPrefab
.
To create an enemy when the script starts, add the following code to
Start()
:
Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
So we will create a new copy of the prefab stored in
testEnemy
, and assign it a route to follow.
Save the file and return to Unity. Select the
Road object in
Hierarchy and select the
Enemy prefab for the
Test Enemy parameter.
Run the project and see how the enemy is moving along the road (in GIF for greater clarity, the speed is increased by 20 times).Noticed that he does not always look where he goes? It's funny, but we're trying to make a professional game. Therefore, in the second part of the tutorial we will teach the enemies to look ahead.Where to go next?
We have already done a lot and are moving quickly to create our own game in the tower defense genre.Players can create a limited number of monsters, and the enemy is running along the road, heading for our cookie. Players have gold and they can upgrade monsters.Download the finished result from here .In the second part, we consider the creation of huge waves of enemies and their destruction. See you!