Creating a Tower Defense Game in Unity - Part 2

image

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() { //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 turns the enemy so that he always RotateIntoMoveDirection ahead. He does this as follows:

  1. Calculates the current direction of movement of the beetle, subtracting the position of the current route point from the position of the next point.
  2. 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.
  3. 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() :

 // 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); } 

Let's sort it out step by step:

  1. We obtain the index of the current wave and check whether it is the last one.
  2. 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.
  3. If necessary, spawn the enemy, creating an instance of enemyPrefab . Also increase the value of enemiesSpawned .
  4. 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.
  5. 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:


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!

bugs

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 { // 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); } } } } 

So we manage the health of the player. And again the main part of the code is located in the setter:

  1. 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.
  2. Update the private variable and health label in the upper left corner of the screen.
  3. 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.
  4. 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.

cookie attack

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:


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:

 // 1 void OnEnemyDestroy(GameObject enemy) { enemiesInRange.Remove (enemy); } void OnTriggerEnter2D (Collider2D other) { // 2 if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Add(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate += OnEnemyDestroy; } } // 3 void OnTriggerExit2D (Collider2D other) { if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Remove(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate -= OnEnemyDestroy; } } 

  1. In OnEnemyDestroy we remove the enemy from enemiesInRange . When an enemy OnTriggerEnter2D trigger around a monster, OnTriggerEnter2D is OnTriggerEnter2D .
  2. 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?
  3. 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; 

speeddetermines the speed of the projectiles; The purpose is damageclear from the title.

target, startPositionand targetPositiondetermine the direction of the projectile.

distanceand startTimetrack the current position of the projectile. gameManagerrewards 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>(); 

startTimewe 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:

 // 1 float timeInterval = Time.time - startTime; gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance); // 2 if (gameObject.transform.position.Equals(targetPosition)) { if (target != null) { // 3 Transform healthBarTransform = target.transform.Find("HealthBar"); HealthBar healthBar = healthBarTransform.gameObject.GetComponent<HealthBar>(); healthBar.currentHealth -= Mathf.Max(damage, 0); // 4 if (healthBar.currentHealth <= 0) { Destroy(target); AudioSource audioSource = target.GetComponent<AudioSource>(); AudioSource.PlayClipAtPoint(audioSource.clip, transform.position); gameManager.Gold += 50; } } Destroy(gameObject); } 

  1. We calculate the new position of the projectile, using Vector3.Lerpfor interpolation between the initial and final positions.
  2. If the projectile reaches targetPosition, then we check whether there is more target.
  3. HealthBar damage .
  4. , , .

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 Level

Projectile 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 MonsterLevelfollowing 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; // 1 Vector3 startPosition = gameObject.transform.position; Vector3 targetPosition = target.transform.position; startPosition.z = bulletPrefab.transform.position.z; targetPosition.z = bulletPrefab.transform.position.z; // 2 GameObject newBullet = (GameObject)Instantiate (bulletPrefab); newBullet.transform.position = startPosition; BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>(); bulletComp.target = target.gameObject; bulletComp.startPosition = startPosition; bulletComp.targetPosition = targetPosition; // 3 Animator animator = monsterData.CurrentLevel.visualization.GetComponent<Animator>(); animator.SetTrigger("fireShot"); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); } 

  1. . z z bulletPrefab . z , , .
  2. Create a copy of the new projectile with the bulletPrefabappropriate MonsterLevel. Assign startPositionand targetPositionprojectile.
  3. 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; // 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)); } 

Consider this code step by step.

  1. 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.
  2. Call Shootif the elapsed time is more than the frequency of the monster's shooting and set the lastShotTimevalue of the current time.
  3. 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:


. , , , , .

tower defense .

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


All Articles