In part 4 of this tutorial series we animated and prepared the enemy game objects.
In this part we are going to make the enemies move, spawn them in the game and create the gameplay mechanism.
The Enemy Script
public float moveSpeed = 5f;
private Rigidbody2D myBody;
The variables are self explanatory, the moveSpeed is going to determine how fast the enemy is moving and we are going to move the enemy with the help of Rigidbody2D component.
In the Awake function we are going to get the reference to myBody variable:
void Awake()
{
myBody = GetComponent();
}
void FixedUpdate()
{
myBody.velocity = new Vector2(moveSpeed, myBody.velocity.y);
}
Attach the enemy script on all 3 enemy monsters:
When we test the game, you will see how all 3 enemy monsters are going to move to the right direction:
If you want to make the enemies move to the left side, just change the moveSpeed variable to a negative value.
Don’t forget to apply the new changes to every enemy so that the changes will be applied to their prefabs:
EnemySpawner Script
Rename the new game object to Enemy Spawner and reset it’s position at 0 for X, Y and Z inside the transform component.
Next, create two new empty game objects as children of the Enemy Spawner object, add an icon to them so that you can see them better in the Scene view and move them to the edge of the level but make sure that they are above the ground so that when an enemy object is spawned it lands on the ground and doesn’t fall below it:
Next, open the EnemySpawner script and add the following lines of code below the class declaration:
[SerializeField]
private GameObject[] enemyReference;
private GameObject spawnedEnemy;
[SerializeField]
private Transform leftSide, rightSide;
private int randomIndex;
private int randomSide;
Before we proceed further with the EnemySpawner script, attach the enemy prefabs in the enemyReference array variable in the Inspector tab:
Also, delete any enemy game object that is in the Hierarchy tab, because the EnemySpawner is going to spawn new enemies and we don’t need to add them manually in the game.
With this we finished the Enemy script and I will leave the full Enemy script as a reference below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
public float moveSpeed = 5f;
private Rigidbody2D myBody;
// Start is called before the first frame update
void Awake()
{
myBody = GetComponent();
}
// Update is called once per frame
void FixedUpdate()
{
myBody.velocity = new Vector2(moveSpeed, myBody.velocity.y);
}
}
Spawning Enemies In The Game With The Help Of Coroutine
To make the game interesting, we are going to spawn enemies over and over in the game. This means that we need to have some kind of time interval or a delay effect that we will use to spawn new enemies in the game.
In Unity, there are multiple ways how we can delay the execution of our code. One of the ways is using the Invoke function, which we saw in the Rainy Knifes tutorial.
This time, we are going to use a coroutine to create the delay effect. If you don’t know what is a coroutine, then, before you proceed with this tutorial, I highly suggest that you learn about them by clicking here.
Go back in the EnemySpawner script and create the following function:
IEnumerator SpawnMonsters()
{
while (true)
{
yield return new WaitForSeconds(Random.Range(1, 5));
randomIndex = Random.Range(0, enemyReference.Length);
randomSide = Random.Range(0, 2);
spawnedEnemy = Instantiate(enemyReference[randomIndex]);
// left side
if (randomSide == 0)
{
spawnedEnemy.transform.position = leftSide.position;
spawnedEnemy.GetComponent().moveSpeed = Random.Range(4, 10);
}
else
{
// right side
spawnedEnemy.transform.position = rightSide.position;
spawnedEnemy.GetComponent().moveSpeed = -Random.Range(4, 10);
spawnedEnemy.transform.localScale = new Vector3(-1f, 1f, 1f);
}
} // while loop
}
Because array’s index is zero(0) based, that is why we can use enemyReference.Length which will return how many elements we have in the enemyReference array, in our case 3, and since Random.Range will not include the max parameter, the function will never return 3 and we will not get array index out of bounds exception.
If the value of randomSide variable is not equal to 0 e.g. it is 1 because we are using else to test the second condition, then we set the position of the enemy to the right by using rightSide.position, we change the moveSpeed variable of the Enemy script to the negative value by adding – sign in front of the Random.Range and we change the Scale’s X value to -1.
The reason why we set the speed to a negative value is because the enemy will be placed on the right side. Which means, the enemy will run from the right to the left side.
We know that in Unity, the left side is the negative side, that is why we set the speed to a negative value, and you can easily test this out by setting a positive value for the moveSpeed variable.
We also need to change the Scale’s X to a negative value to make the enemy face the left side because it is running towards the left side. If we don’t change this, then it would look like the enemy is running with its back facing the left side.
You can also use the same technique we applied to the player object when we changed its Scale X value using Mathf.Abs function. Or you can use the sprite component and its flipX property to flip the enemy and make it face the left side.
Before we can test the EnemySpawner, we need to run our coroutine in the Start function:
void Start()
{
StartCoroutine(SpawnMonsters());
}
Collision Layers
As you can see the Layer property is a drop down list and every game object is set on the Default layer when you create them except for the UI elements which are on the UI layer.
We can use the Layer property to determine which game objects can collide with each other and which game objects will ignore collisions between them.
First, we are going to create a new layer, so select any game object in the Hierarchy, and click on the Layer drop down list then click on Add Layer:
This will open Tags & Layers in the Inspector. Create a new Enemy layer by typing Enemy in any empty layer field:
You can name the layers however you want and you can create up to 31 layers(if Unity doesn’t change this in the future).
Now in the Assets -> Prefabs -> Enemy Prefabs folder, select all 3 enemies, and from the Layer drop down list select the Enemy layer for all 3 enemies:
Now click on Edit -> Project Settings:
If the Project Settings tab is floating dock it anywhere in the editor, and then select Physics 2D on the left side and scroll down until you see all the available layers that form a grid:
As you can see, the enemies are passing through each other without hugging as we saw the first time.
The Collector Script
While we fixed the hugging problem between the enemies we have another problem which is when the enemies get to the edge of the level they fall down from the ground and they continue falling down as long as the game is running.
While we don’t see the particular enemy game object, that game object is still in the game and it fills up the computer memory and the more enemies we create the slower and slower our game is going to be.
To fix this, we need to create a collector game object that will remove the unnecessary enemies from the game when we don’t need them anymore.
In the Assets -> Scripts folder, Right Click -> Create -> Folder and name it Gameplay Helper Scripts.
Inside the Gameplay Helper Scripts, Right Click -> Create -> C# Script and name it Collector.
In the Hierarchy tab, create an empty game object and name it Left Collector. Attach a Box Collider 2D component and attach the Collector script on him:
Before we edit the Collector script, inside the TagManager add the following line of code:
public static string ENEMY_TAG = "Enemy";
Inside the Collector script, add the following code block:
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag(TagManager.ENEMY_TAG) || collision.CompareTag(TagManager.PLAYER_TAG))
{
Destroy(collision.gameObject);
}
}
In the code above I am not only testing if the collector object collides with the enemy object, but I am also testing if the collector collides with the player object.
The reason for that is we didn’t define any bounds for the player’s movement, so the player can move freely in the level which means that he can potentially go outside the level bounds, if that happens he will collide with the collector and be destroyed which will end the game.
Another thing that I want to mention which is related to the CameraFollow script. If you remember, in the LateUpdate function in the CameraFollow script the first lines of code are:
if (!playerTarget)
return;
The reason for the lines of code above is because if the player object collides with the collector he will be destroyed and since the camera is using the reference of the player to follow him, when the player is destroyed the reference is destroyed along with him, and if we don’t perform a test like the one in the code above, we will get a null reference exception.
I already mentioned why we are performing that test in the lecture about the CameraFollow script, but I always like to revert back and explain the parts of code that are connected or have something to do with each other.
Since we are using the OnTriggerEnter2D function to detect collision between the collector game object and the enemy or the player, we need to check the Is Trigger checkbox for the Box Collider 2D component attached on the collector game object.
We also need to resize the collider so that when we position the collector object at the end of the level, the collider will be big enough so that the game object that falls off the level will collide with it:
Let’s test the game and see this in action:
Now when the player or any of the enemy game objects collides with the collector object it will be destroyed and removed from the game.
Now when it comes to Destroy function, if its overused it can lead to optimization problems and we already talked about that. So you can already assume that there is a better solution for that which is the SetActive function. You can read more about these two functions by clicking here.
Player And Enemy Collision
private void OnCollisionEnter2D(Collision2D collision)
{
// detecting collision with the ground
if (collision.gameObject.CompareTag(TagManager.GROUND_TAG))
isGrounded = true;
// detecting collision with the enemies
if (collision.gameObject.CompareTag(TagManager.ENEMY_TAG))
Destroy(gameObject);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag(TagManager.ENEMY_TAG))
Destroy(gameObject);
}
1 thought on “Create A 2D Game With Unity Engine Part 5: Enemies, Spawners, And Gameplay Mechanism”
Bro i have a issue
The Enemies are seen in the scene view but when i click the play button they are not visible in the game view.
But still they are moving in the scene view