Add a Player Attack
In this lesson, you will give the player an attack so that they can actively defeat enemies in the dungeon.
Create the Projectile
The attack we are going to create will allow the player to use the arrow keys to shoot a fireball in any direction. First we need to create the actual fireball object that the player will be able to shoot.
In the Sprites folder, create a new Circle Sprite and rename it to Projectile. Then drag the Projectile sprite into the Hierarchy window to create an object with that sprite.
Reset the Transform of the Projectile object and reduce the scale so it is smaller. Then change the sprite color on the object to a red color so it looks more like a fireball.
Add a Circle Collider 2D component to and check Is Trigger. This way when the player shoots the Projectile, it will pass through the player.
Also add a Rigidbody 2D component which will be used to move the fireball. Make sure to set the Body Type to Kinematic
Script the Projectile
Go into the Scripts folder and create a new script named ProjectileController.
Then attach the script to the Projectile object.
Open the script and remove the Start() and Update() methods as these are not needed.
Then create two variables:
[SerializeField] int damage = 10; - This will set the damage that the fireball does to the enemy.
[SerializeField] float speed = 10f; - This will set the speed that the fireball moves.
Rigidbody2D rb; - This will be used to set the velocity of the fireball so it can move.
[SerializeField] float speed = 10f; - This will set the speed that the fireball moves.
Rigidbody2D rb; - This will be used to set the velocity of the fireball so it can move.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ProjectileController : MonoBehaviour { [SerializeField] int damage = 10; [SerializeField] float speed = 10f; Rigidbody2D rb;
Now create a private void Awake() method to initialize the rb variable by getting the Projectile's Rigidbody2D component.
public class ProjectileController : MonoBehaviour { [SerializeField] int damage = 10; [SerializeField] float speed = 10f; Rigidbody2D rb; private void Awake() { rb = GetComponent<Rigidbody2D>(); }
Now we need to create a method that will set the velocity of the Projectile. This will be called when the Projectile is created by the player's attack.
Create a public void method named SetVelocity() which takes in a Vector2 as an argument.
Inside the method we want to set the rb.velocity to the velocity passed into the method, multiplied by the Projectile speed.
private void Awake() { rb = GetComponent<Rigidbody2D>(); } public void SetVelocity(Vector2 velocity) { rb.velocity = velocity * speed; }
Next we need to create an OnTriggerEnter2D() method that takes in a Collider2D named collision.
Inside we will create an if statement to check if the collison object's tag is Enemy. If so, then we want to get the EnemyController component from the object and call a method named TakeDamage() and pass in the damage variable as an argument. We will also destroy this Projectile object after it has done damage.
Note that the TakeDamage() method for the EnemyController does not exist yet but we will create it afterwards.
public void SetVelocity(Vector2 velocity) { rb.velocity = velocity * speed; } private void OnTriggerEnter2D(Collider2D collision) { if(collision.CompareTag("Enemy")) { collision.GetComponent<EnemyController>().TakeDamage(damage); Destroy(this.gameObject); } }
Finally, we will create a another if statement to check if the Projectile collides with a wall.
If the collision object's tag is Wall then we will just Destroy this Projectile object.
private void OnTriggerEnter2D(Collider2D collision) { if(collision.CompareTag("Enemy")) { collision.GetComponent<EnemyController>().TakeDamage(damage); Destroy(this.gameObject); } if(collision.CompareTag("Wall")) { Destroy(this.gameObject); } }
Now save the script and go back into Unity Editor.
In order for the Player to create a new Projectile every time they shoot, we need to create a prefab of the Projectile object.
To do this, simply drag the Projectile object from the Hierarchy Window into the Prefabs folder in the Project Window.
We need to make sure that the Walls Tilemap on each level is tagged with Wall.
To do this, select the Walls Tilemap inside of the Grid object. Then in the Inspector Window, click on the Tag dropdown menu and select Add Tag...
Now in the Tags & Layers settings under the Tags section, click the + button and to add a new Tag and name it Wall. Then click on the Walls Tilemap again and in the Inspector click on the Tag dropdown menu. This time, you will see the Wall Tag that you created. Select this to assign it to the object.
You must then apply this tag to the Wall Tilemap of each level you have created or will create.
We also need to make sure that the enemy objects have the Enemy Tag.
To do this, select an enemy object. Then in the Inspector Window, click on the Tag dropdown menu and select Add Tag...
In the Tags & Layers settings under the Tags section, click the + button and to add a new Tag and name it Enemy. Now go into the Prefabs folder and select your enemy prefab. We want to change the tag on the prefab so that all enemies will have the tag.
In the Inspector click on the Tag dropdown menu. This time, you will see the Enemy Tag that you created so select this to assign it to the prefab.
Now we need to add that TakeDamage() method to the enemy that we referenced earlier. Go into the Scripts folder and open the EnemyController script.
First create a new int variable named health with an initial value of 50. Make sure to add the [SerializeField] attribute so the variable appears in the Inspector.
public class EnemyController : MonoBehaviour { [SerializeField] int damage = 20; [SerializeField] int health = 50;
Then create a public void TakeDamage() method which will take in an int argument named damage.
Inside the function we will reduce the health by the damage passed into function.
We will then check if the health is less than or equal to 0. If so, we want to destroy this enemy object.
public class EnemyController : MonoBehaviour { [SerializeField] int damage = 20; [SerializeField] int health = 50; public void TakeDamage(int damage) { health -= damage; if (health <= 0) { Destroy(this.gameObject); } }
Save the script and go back into the Unity Editor.
Script the Attack
Finally we need to add code to the Player Controller so that when we press one of the arrow keys, we shoot a projectile.
Open up the PlayerController script and start by creating 3 new variables:
[SerializeField] GameObject projectile; - This will reference the Projectile prefab so we can create the object when the player shoots.
[SerializeField] float attackInterval = 0.5f; - This sets the delay before the player can fire another projectile.
float attackTimer = 0; - This will count down and keep track of the time left before the player can fire another projectile.
[SerializeField] float attackInterval = 0.5f; - This sets the delay before the player can fire another projectile.
float attackTimer = 0; - This will count down and keep track of the time left before the player can fire another projectile.
public class PlayerController : MonoBehaviour { [SerializeField] float speed = 7f; [SerializeField] GameObject projectile; [SerializeField] float attackInterval = 0.5f; Rigidbody2D rb; Animator animator; float attackTimer = 0;
In the update function, create an if statement that checks if the attackTimer is less than or equal to 0. If so, that means the player may fire a projectile, so we will call the Attack() method (which we will create next).
If the attackTimer is not less than or equal to 0, then we will just reduce the attackTimer by 1 second using attackTimer -= Time.deltaTime
private void Update() { [... previous code here ...] if (attackTimer <= 0) { Attack(); } else { attackTimer -= Time.deltaTime; } }
Now create the private void Attack() method.
Inside this method we will check if an arrow key is pressed down. If so, we will call a SpawnProjectile() method (which we will create next) that takes in a Vector2 as an argument.
The Vector2 will correspond to the direction we want to shoot based on the arrow key pressed. E.g. if the up arrow key is pressed, we will pass in Vector2.up which is equivalent to new Vector2(0,1). Therefore the projectile will have an x velocity of 0 and a y velocity of 1, causing it to move upwards.
private void Update() { [... previous code here ...] if (attackTimer <= 0) { Attack(); } else { attackTimer -= Time.deltaTime; } } private void Attack() { if (Input.GetKey(KeyCode.UpArrow)) { SpawnProjectile(Vector2.up); } else if (Input.GetKey(KeyCode.DownArrow)) { SpawnProjectile(Vector2.down); } else if (Input.GetKey(KeyCode.LeftArrow)) { SpawnProjectile(Vector2.left); } else if (Input.GetKey(KeyCode.RightArrow)) { SpawnProjectile(Vector2.right); } }
Finally, create a private void SpawnProjectile() method that takes in a Vector2 named direction.
Inside the method, we will instantiate (create) the projectile at the position of the player and keep its original rotation (using Quarternion.identity).
We will then assign the instantiated object to a temporary GameObject variable named instance.
Next we will get the ProjectileController component from the instance and call the SetVelocity() method, passing in the same direction that was originally passed into this SpawnProjectile method.
Finally, we will reset the attackTimer variable to the attackInterval value so that the player has to wait before they can fire another projectile.
private void Attack() { if (Input.GetKey(KeyCode.UpArrow)) { SpawnProjectile(Vector2.up); } else if (Input.GetKey(KeyCode.DownArrow)) { SpawnProjectile(Vector2.down); } else if (Input.GetKey(KeyCode.LeftArrow)) { SpawnProjectile(Vector2.left); } else if (Input.GetKey(KeyCode.RightArrow)) { SpawnProjectile(Vector2.right); } } private void SpawnProjectile(Vector2 direction) { GameObject instance = Instantiate(projectile, transform.position, Quaternion.identity); instance.GetComponent<ProjectileController>().SetVelocity(direction); attackTimer = attackInterval; }
Save the script and go back into Unity Editor.
Delete the Projectile object from the Hierarchy as we will be instantiating projectiles from prefabs, so none should be in the Scene by default.
On the Player object, you will see the Projectile field on the Player Controller component where you must attach the Projectile prefab. When you do, you will see a blue line next to the field meaning that there are changes to an object of a prefab (in this case the Player prefab).
Since we want all Player objects to have the Projectile prefab attached, we can apply this change to the main Player prefab by right clicking on the field name and selecting Apply to Prefab 'Player'.
Now play the game and press the arrow keys to shoot projectiles.
If you shoot the enemy they will take damage and if they take too much damage, they will be destroyed.