Creating a Tower Defense Game in Unity - Part 1

image

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:


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:

.

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:


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:

 //1 void OnMouseUp() { //2 if (CanPlaceMonster()) { //3 monster = (GameObject) Instantiate(monsterPrefab, transform.position, Quaternion.identity); //4 AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); // TODO:   } } 

This code places the monster when you click the mouse or touch the screen. How does he work?

  1. Unity automatically calls OnMouseUp when the player touches the physical collider GameObject.
  2. When called, this method puts a monster if CanPlaceMonster() returns true .
  3. 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
  4. 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:


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:

 //1 public MonsterLevel CurrentLevel { //2 get { return currentLevel; } //3 set { currentLevel = value; int currentLevelIndex = levels.IndexOf(currentLevel); GameObject levelVisualization = levels[currentLevelIndex].visualization; for (int i = 0; i < levels.Count; i++) { if (levelVisualization != null) { if (i == currentLevelIndex) { levels[i].visualization.SetActive(true); } else { levels[i].visualization.SetActive(false); } } } } } 

Quite a big piece of C # code, right? Let's sort it out in order:

  1. 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.
  2. In the getter, we return the currentLevel value.
  3. 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()) { //      } else if (CanUpgradeMonster()) { monster.GetComponent<MonsterData>().IncreaseLevel(); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); // TODO:   } 

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 inside
Replace 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:

  1. Ask the way for the enemies to walk
  2. Move the enemy along the way
  3. 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:


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() :

 // 1 Vector3 startPosition = waypoints [currentWaypoint].transform.position; Vector3 endPosition = waypoints [currentWaypoint + 1].transform.position; // 2 float pathLength = Vector3.Distance (startPosition, endPosition); float totalTimeForPath = pathLength / speed; float currentTimeOnPath = Time.time - lastWaypointSwitchTime; gameObject.transform.position = Vector2.Lerp (startPosition, endPosition, currentTimeOnPath / totalTimeForPath); // 3 if (gameObject.transform.position.Equals(endPosition)) { if (currentWaypoint < waypoints.Length - 2) { // 3.a currentWaypoint++; lastWaypointSwitchTime = Time.time; // TODO:     } else { // 3.b Destroy(gameObject); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); AudioSource.PlayClipAtPoint(audioSource.clip, transform.position); // TODO:   } } 

Let's sort the code step by step:

  1. From the array of route points we get the starting and ending positions of the current route segment.
  2. 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.
  3. Check whether the enemy has reached endPosition . If yes, then we process two possible scenarios:
    1. 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.
    2. 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!

Source: https://habr.com/ru/post/413837/


All Articles