Fighting mechanics implementation in tiny Unity project

 by • Reading Time: 15 minutes

Introduction

In this tutorial we are going to implement a tiny unity project with basic fighting mechanics that can be commonly found in any type of action genre game. Last time, we have looked at the implementation of a top-down movement and attack mechanics in Unity 2019.3. We are going to expand this by giving our character a sword so that he will be able to defend himself from blood-thirsty evil bunnies roaming our game world! Let’s jump right to it!

Table of contents:

  1. Game sprites and animations
  2. Extending character’s fighting animations
  3. Populating game world with evil bunnies
  4. Binding fighting mechanics components together with scripts

1. Game sprites and animations

Since we are going to extend attack animations we created previously, we have to consider the size of the sprites. The resolution, 16 x 16, doesn’t give us too much area to manoeuvre to equip our character with a weapon, let alone animate it! To overcome this, I will create a completely isolated sprite containing only the weapon we want our character to wield. Later in Unity, I’ll simply combine two animations to create a uniform one. As for the evil bunny, I’ll follow the same method I did during the character creation but make it even cuter than before. Both sprites will be of the same size as before, 16 x 16. First let’s start with a sword.

Sword weapon sprite

As previously, we need to create 4 different sword animations that will be the core of our fighting mechanics. This time only sword swings will be made to complement the character attack animations in all 4 directions. Usually, you would like to play the attack clip almost immediately right after the player presses the attack button to achieve pleasant gameplay experience. For the sake of this tutorial I’m going to create 4 frame swings with higher sample rate to illustrate the method of enriching previous character sprites.

  • start by making a vertical or horizontal line, depending on which side you decided to take care of first
  • create a tip of the sword
  • make a hilt of the sword by adding perpendicular lines to the blade at side opposite to the tip
  • colour the sword and add shadows to give it more depth
Process of creating a simple sword sprite in Aseprite

To make a swing, select the sword and rotate it by a desired angle using the rotation transform tool. After this you’ll notice that the sword looks jagged and indeed – in pixel art world whenever you rotate pixels they will usually look not very appealing to say the least. This is only a starting point and we have to ‘clean’ the individual pixels composing the sword at this particular angle to make it look good.

Rotating the sword sprite

Keep on erasing and adding new pixels until you reach the point when the sword maintains its shape. Add few pixels to indicate that the sword ‘cuts’ the air. Continue increasing the angle on consecutive frames. As before, you can use ‘Flip Horizontal / Vertical’ to create frames for opposite directions.

Making the Sword Swing Sprite Animation in stages

The sword swing animations are now done! Let’s move on to making our ultimate nemesis – the evil bunny sprite!

Evil Bunny enemy sprite

For the bunny sprite, I’ll go with a simple head instead of entire body. That way it will be much easier to keep our opponent within 16 x 16 size boundaries while still maintaining its ‘cute’ appearance.

  • start by drawing a flattened circle at the bottom
  • erase the top line and draw ears
  • colour the bunny white and add vertical eyes
  • add shadows to give it more depth
Process of creating an evil bunny sprite front in stages

Making bunny’s back is really simple. All that needs to be done here is to duplicate the frame and erase the eyes. As for the side, start by duplicating the frame and erase the ears. Try to make only one ear as if the bunny was ‘seen’ from the side. Remove one eye and you may wish to move the other one by a pixel to the side but it’s not necessary and add shadows.

Process of creating evil bunny sprite side in stages

We are now going to make our bunny do the ‘running hop’. Firstly duplicate the first front frame and move entire body up by a pixel. Secondly, make ears to appear slightly flattened, as if they were hit by the air. Thirdly, add eyebrows to the eyes to indicate slight effort. Do the same for the back animation, but remove the eyes.

Creating two frame bunny front movement animation

We’ll repeat the same process for the side. Simply duplicate the side frame and move it up by a pixel. After that, again make the ear appear a bit flattened and add eyebrow to an eye. Finish up by adding shadows.

Creating two frame bunny side movement animation

Adding ‘hurt’ and ‘die’ animations (optional)

At this stage it is entirely up to you what other animations you’d like to add. I am creating a game in which the character can defend himself against bunnies with a sword. Therefore, I have added two more animations, namely ‘hurt’ and ‘die.

First, let’s look into a ‘hurt’ animation. Select the bunny’s body apart from the pixel bar at the bottom and move it up by a pixel. Raise ears a bit. Add the screaming mouth and slightly close the eyes to indicate pain.

Creating two frame bunny hurt animation

The ‘die’ animation consist of three frames. Duplicate the first front frame and again move the bunny up by a pixel. Raise one ear and slightly close the eyes. At the last frame, make bunny look a bit flattened. To that end, make his ear to be lying on his head. I also tried to be a bit funny here and replaced eyes with crosses.

Creating three frame bunny dying  animation

When you finish, click on the newly created ‘hurt’ and ‘die’ animations that are present in your project assets and untick their ‘Loop Time’ in the ‘Inspector’ panel since we want to play those animations just once.

2. Extending character’s fighting animations

To enrich the character animations, first export the sword swings sprites and import them into Unity. It is done exactly the same way as we did in the first tutorial. After that, drag & drop first frame sprites for each attack side under ‘Player’ asset in ‘Project Hierarchy’ panel.

  • open the attack clip in ‘Animation’ panel and start the recording by pressing a red circle button next to playback controls
  • press the ‘Add Property’ button and select ‘Sprite Renderer’ of the sword sprite that is now a child asset of the ‘Player’
  • change ‘Sprite Renderer’ to ‘Active’ on the first frame
  • drag & drop all sprites composing a full swing animation for a given side
  • disable ‘Sprite Renderer’ on the last frame

Continue doing this for each side until you end up with the whole set of attack animations.

Swinging sword combining two sprite animations in Unity

Congratulations! We now have a character who is attacking with his sword inside the game world. Our fighting mechanics is getting shape! However, there’s not that many enemies to fight with yet. Let’s change that!

3. Populating game world with evil bunnies

To create an army of bunny opponents we are going to make something that is called a ‘prefab’ in Unity. According to official documentation, “Unity‚Äôs Prefab system allows you to create, configure, and store a GameObject complete with all its components, property values, and child GameObjects as a reusable Asset”. In other words, it is possible to define all properties of the game object once and instantiate it inside the scene as many times as we want without having to redo the whole process. This is perfect for the purpose of our fighting mechanics since we want to define our bunny enemy and its behaviour only once and later just drop it into our game world or instantiate it on the fly.

Drag the first sprite of your bunny to ‘Hierarchy’ panel. Click on it and in the ‘Animation’ panel click on ‘Create’ button to start remaking all the animations from sprite frames. As before, we are going to recreate the ‘idle’, ‘running’ as well as ‘hurt’ and ‘die’ animations here in a similar manner as we did with our character.

  • select ‘Create New Clip…’ from the dropdown menu in the ‘Animation’ panel
  • give the clip a meaningful name so that it is easily identified later on, e.g. ‘Bunny_Run_Left’
  • drag all exported sprite frames of a given animation to a timeline of a given clip
  • set the ‘Sample Rate’ to a low number (e.g. 6)
  • repeat the process for every animation
Making bunny prefab by drag & drop asset to 'Assets' panel

Once you finish, drag the bunny asset from the ‘Hierarchy’ panel back to your project assets. You will notice that it’s icon has turned blue – an indication that it is now a prefab resource! It is now possible to simply drag & drop as many bunnies with the same presets into a game world as we want!

Setting bunny’s movements blend tree

After making a prefab, jump right into its ‘Animator’ panel as we are going to set up the transitions. They will be much simpler than they were in case of our character. Begin by deleting all the animation nodes we have just created from the panel except the ‘Bunny_Idle’, ‘Bunny_Hurt’ and ‘Bunny_Die’. Right-click on an empty space and select ‘Create State -> From New Blend Tree’. Double-click the new node and create 3 parameters that will control movement of our bunny. Similarly, we are going to need a ‘Horizontal’ and ‘Vertical’ as well as ‘Speed’ float-type parameters to do just that. In addition, two more boolean parameters will be created to communicate the ‘hurt’ and ‘death’ of our bunny to the animator from the script level. I’ve added a ‘B_’prefix to all names to distinguish these parameters from the ones set in the ‘Player’ animator.

Once you finished setting up the parameters, click on the blend tree node and in the ‘Inspector’ panel start by renaming it to ‘Movement’. Then follow the similar setup that was made for a character.

Movement blend tree setup for bunny enemy

Once you’re done with setting up the blend tree, head back to ‘Base Layer’. It is now time to set all the necessary transitions!

Making animations transitions for the bunny

Start by right-clicking on the ‘Bunny_Idle’ node and select ‘Set as Layer Default State’.

  • right-click on the node again and this time choose ‘Make Transition’.
  • drag the arrow to ‘Movement’ node and click on it. In the ‘Inspector’ panel untick ‘Has Exit Time’ and ‘Fixed Duration’ fields. Set ‘Transition Duration’ and ‘Transition Offset’ to ‘0’. Add ‘B_Speed Greater 0.01’ to the list of conditions.
  • right-click on ‘Movement’ node and drag the arrow back to ‘Bunny_Idle’ state node. Click on the arrow and make the same setup as before but this time add ‘B_Speed Less 0.01’ condition to the list.

All that’s left is to make transitions to ‘hurt’ and ‘die’ animations. Right-click on the ‘Bunny_Idle’ node and make a transition arrow to ‘Bunny_Hurt’. Click on the arrow and make the exact same settings as before but this time place ‘B_isHurt is true’ condition in the list. The same transition needs to be made from ‘Movement’ node. Repeat the process for ‘die’ animation. You should end up with a similar node setup in your ‘Animator’ panel:

The transitions between different animations for bunny enemy

Well done! You have now correctly set the bunny animations. Let’s now write a script that will control them in response to what is happening inside our game world!

4. Binding fighting mechanics components together with scripts

There are few things we need to do before we jump into the code. Firstly, we have to add ‘RigidBody2D’ component to bunny prefab. In the ‘Inspector’ panel:

  • set ‘Body Type’ to ‘Dynamic’
  • set ‘Gravity Scale’ to ‘0’
  • tick ‘Freeze Rotation’ on ‘Z axis’ in the ‘Constraints’ section

Secondly, add the ‘Box Collider 2D’ component and tick ‘Is Trigger’ checkbox. This will allow us to detect collisions between the bunny and character as well as his swinging sword. Note: when the ‘Is Trigger’ field is ticked, the game object will not register a collision with an incoming Rigid Body’. In other words, no physical collisions will take place but they will still be detected and registered by the script. The collider box is highlighted with a green outline in the ‘Scene’ window and you may wish to edit its boundaries with ‘Boundary Volume’ editor to better match the game object shape. You can easily enter it by clicking on the icon located next to ‘Edit Collider’ label.

Editing the bounding volume inside the Box Collider 2D component

Thirdly, create a new tag by clicking on a dropdown menu just below the name in the ‘Inspector’ panel. Define a meaningful name like ‘Enemy’ and tag our bunny with it. This will be used later on in this tutorial.

Defining a new tag inside Unity

Make sure that both components are also present in ‘Player’ asset with the same settings. We can now move onto the coding part!

Naive Enemy AI script

I want to introduce some intelligence component to our fighting mechanics at this stage. Therefore let’s add an AI script responsible for chasing the player by bunnies. Add a new ‘Script’ component to a bunny prefab and name it ‘Movement_AI’. Open it up using your favourite text editor and start by defining 3 public fields.

public Transform target;   // target position
public Animator animator;  // bunny 'Animator' component
public float speed = 0f;   // bunny speed

Next, add two vectors that will hold the information about the position of both – player (target) and bunny.

Vector2 playerPos, enemyPos;

Delete the ‘Start()’ function and in the ‘Update()’ determine if the bunny is still alive by checking the ‘B_isDead’ in the animator and whether it has a target.

void Update()
{
    if(!animator.GetBool("B_isDead") && target != null)
    {
    }
}

Then, find out the positions of both game objects and assign them to the vectors we created earlier. We will need them in a moment.

playerPos = new Vector2(target.localPosition.x,
                        target.localPosition.y);
enemyPos = new Vector2(this.transform.localPosition.x,
                       this.transform.localPosition.y);
 

Let’s now use those values to determine the distance between two game objects and act accordingly. If the bunny is within a specific distance to the player then move closer to him and slowly run away otherwise.

if (Vector3.Distance(transform.transform.position, 
                     target.transform.position) > 1.3)          
{
    transform.position = Vector2.MoveTowards(enemyPos,
                                             playerPos, 
                                             2 * Time.deltaTime);
}
if (Vector3.Distance(transform.transform.position,
                     target.transform.position) < 1.15)         
{
    transform.position = Vector2.MoveTowards(enemyPos,
                                             playerPos, 
                                             -1 * Time.deltaTime);
}

After that, we are going to make our bunny face the correct direction while on the move by calculating the absolute difference between the player and bunny on ‘Y axis’ and use it in a conditional statement. If certain conditions are met, the bunny will face either left, right, up or down directions by setting the corresponding parameters that have been defined inside the ‘Animator’ component earlier.

float delta_pos_y = Mathf.Abs(Mathf.Abs(target.position.y) -
                    Mathf.Abs(transform.position.y));

if (target.position.x > transform.position.x)
{
    if (delta_pos_y > 0.5f)
    {
        if (target.position.y > transform.position.y)
        {
            animator.SetFloat("B_Vertical", 1f);
            animator.SetFloat("B_Horizontal", 0f);
        }

        if (target.position.y < transform.position.y)
        {
            animator.SetFloat("B_Vertical", -1f);
            animator.SetFloat("B_Horizontal", 0f);
        }
    }
    else
    {
        animator.SetFloat("B_Vertical", 0f);
        animator.SetFloat("B_Horizontal", 1f);
    }
    
    animator.SetFloat("B_Speed", 1f);

}

if (target.position.x < transform.position.x)
{
    if (delta_pos_y > 0.5f)
    {
        if (target.position.y > transform.position.y)
        {
            animator.SetFloat("B_Vertical", 1f);
            animator.SetFloat("B_Horizontal", 0f);
        }

        if (target.position.y < transform.position.y)
        {
            animator.SetFloat("B_Vertical", -1f);
            animator.SetFloat("B_Horizontal", 0f);
        }
    }
    else
    {
        animator.SetFloat("B_Vertical", 0f);
        animator.SetFloat("B_Horizontal", -1f);
    }
    
    animator.SetFloat("B_Speed", 1f);
    
}

Once we have finished editing the ‘Update()’ loop function, we just need to add one more function that will be called when we defeat the bunny.

void Destroy()
{
    Destroy(this.gameObject);
}

This concludes the Movement_AI script. Let us now move onto the scripts responsible for monitoring health of our characters and registering collisions between different game objects!

Monitoring character health status as part of fighting mechanics

We are going to start with defining a ‘scriptable object’ inside our ‘Assets’. These objects are perfect for defining a data that is to be saved, stored and updated during the gameplay. They will be used for monitoring enemy and character health so that we will know when to trigger certain events. By doing so, the relationships between different types of our game can be established in a convenient way. For instance, if the collider component detects another game object of type ‘enemy’ within its bounding box volume, the player health amount should decrease, the ‘hurt’ animation played and possibly character sprite should blink to communicate that to the player.

  • create a ‘Scriptable Object’ script storing the information on character status – health and mana. To keep things simple, the integer values will be used to represent both amounts
using UnityEngine;

[CreateAssetMenu(fileName = "HealthStatusData", 
menuName = "StatusObjects/Health", order = 1)]
public class Health : ScriptableObject
{
    public int health = 100;
    public int mana = 100;
}

The one liner in square brackets above the ‘class’ declaration tells Unity that we want to have the ability of instancing this object via the editor.

  • create an instance of ‘Health’ by right-clicking on empty space in ‘Assets’ panel and select ‘Create -> StatusObjects -> Health’
  • create a ‘StatusManager’ script, attach it to the ‘Player’ and open it up in a text editor

This script is responsible for monitoring health status of our protagonist. Create a public field of a ‘Health’ type (this is the type of our scriptable object we have just defined and instantiated). After than drag the ‘Health’ instance to this field in the editor.

Add the Health object to status field in StatusManager

Inside the script we must define the ‘OnTriggerEnter2D()’ function to deduct desired points from health each time the enemy touches our character. We’ll use the ‘Enemy’ tag that we created earlier. Make sure that our character dies if his health drops to 0.

void OnTriggerEnter2D(Collider2D other)
{
    if (other.tag == "Enemy")
    {
        this.healthStatus.health -= 10;
        if(this.healthStatus.health <= 0)
            Destroy(this.gameObject);
    }
}

You can check the effect by inspecting the health object in the editor!

Health is now reduced by 10 points every time bunny touches the character.

Making the sprite blink when hurt (optional)

To enhance the visuals of our game, we can make our character sprite blink for a moment after he’s been hit. In order to achieve this we’ll use a ‘Coroutine’ concept embedded into Unity. ‘A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame.’

Let’s define a coroutine function that will control the blinking behaviour. For this to work we are going to need few arguments:

  • the array holding references to all character sprites
  • the number of ‘blinks’ we want to execute
  • the delay between each blink
  • one boolean argument controlling the blinking flow
IEnumerator BlinkAllSprites(SpriteRenderer[] sprites, 
                            int blinkTimes, 
                            float blinkDelay, 
                            bool disable = false)
{      
}

We now have to write a loop that will go control the stages of our blinking according to function arguments.

for (int blinkStep = 0; blinkStep < blinkTimes; blinkStep++)
{

}

Inside the loop we specify how the sprite should appear visually during the gameplay if it reaches certain stage of our blinking animation. First we are going to define how all character sprites should look like when they are ‘disabled’. We will simply set their transparency to a low number. In this case 0.2. After that we are going to wait for an amount of seconds specified by ‘blinkDelay’ parameter and return.

for (int i = 0; i < sprites.Length; i++)
{
    if (disable)
        sprites[i].enabled = false;
    else
        sprites[i].color = new Color(sprites[i].color.r,
                                     sprites[i].color.g,
                                     sprites[i].color.b,
                                     0.2f);
}

yield return new WaitForSeconds(blinkDelay);

We are going to do the analogical loop but this time considering the stage when the character sprites are ‘enabled’. Make sure you increase the transparency of our character here!

for (int i = 0; i < sprites.Length; i++)
{
    if (disable)
        sprites[i].enabled = true;
    else
        sprites[i].color = new Color(sprites[i].color.r,
                                     sprites[i].color.g,
                                     sprites[i].color.b, 
                                     1);
}

yield return new WaitForSeconds(blinkDelay);

We are now going to start coroutine every time our character is hit inside the ‘OnTriggerEnter2D()’ function defined earlier. You can adjust the parameters to your preference. Congratulations! You’ve created a nice FX for your game!

public class StatusManager : MonoBehaviour
{
    public Health healthStatus;

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Enemy")
        {
            SpriteRenderer[] sprites = 
            GetComponentsInChildren<SpriteRenderer>();
            
            StartCoroutine(BlinkAllSprites(sprites, 
                                           5, 0.05f));
            this.healthStatus.health -= 10;
            if(this.healthStatus.health <= 0)
                Destroy(this.gameObject);
        }
    }

    IEnumerator BlinkAllSprites(SpriteRenderer[] sprites, int blinkTimes, float blinkDelay, bool disable = false)
    {
        for (int blinkStep = 0; blinkStep < blinkTimes; blinkStep++)
        {
            for (int i = 0; i < sprites.Length; i++)
            {
                if (disable)
                    sprites[i].enabled = false;
                else
                    sprites[i].color = new Color(sprites[i].color.r, sprites[i].color.g, sprites[i].color.b, 0.2f);
            }

            yield return new WaitForSeconds(blinkDelay);

            for (int i = 0; i < sprites.Length; i++)
            {
                if (disable)
                    sprites[i].enabled = true;
                else
                    sprites[i].color = new Color(sprites[i].color.r, sprites[i].color.g, sprites[i].color.b, 1);
            }

            yield return new WaitForSeconds(blinkDelay);
        }
    }
}

The fighting mechanics core

Because we want our sword to actually hit the enemies, we are going to need a script responsible for updating the game world. Before we jump into code, let’s make sure that the ‘RigidBody2D’ and ‘Box Collider 2D’ components are attached to each side of our attack sprites. They are currently children of ‘Player’ asset in ‘Project Hierarchy’ panel. Set the ‘Body Type’ to ‘Kinematic’ and tick ‘Is Trigger’ field. After that, create a new script and name it ‘WeaponEffector’.

Setting up weapon effector script for the character's sword

Let’s start by defining fields we are going to use to execute the hit logic whenever a collision is registered by physics engine. To that end, we need a reference to our player’s ‘Animator’ to have the ability of checking whether player pressed the ‘attack’ button and information about the hit enemy (if there is one).

public class WeaponEffector : MonoBehaviour
{

    public Animator animator;
    private GameObject enemy;

}

We won’t need the ‘Start()’ function so delete it from the script. Add ‘OnTriggerStay2D()’ and within its body assign the hit game object tagged ‘Enemy’ to a field we already defined earlier.

private void OnTriggerStay2D(Collider2D collision)
{
    if (collision.gameObject.tag == "Enemy")
    {
        enemy = collision.gameObject;
    }
}

The bunny shall die upon 1 hit of the sword, but obviously this can be easily expanded with a life bar. In the ‘Update()’ function we have to check few things to play the ‘die’ animation:

  • if the player has pressed the ‘attack’ button
  • if the ‘enemy’ has been assigned and contains the game object data
  • whether the bunny we hit is still ‘alive’

If the hit enemy is still alive then all we have to do is to set ‘B_isDead’ boolean parameter in the bunny animator to ‘true’.

private void Update()
{
    if (animator.GetBool("isAttack"))
    {
        if (enemy != null)
        {
            if (!enemy.GetComponent<Animator>().GetBool("B_isDead"))
            {
                enemy.GetComponent<Animator>().SetBool("B_isDead", true);
            }
            enemy = null;
        }
    }
}

Assign the ‘Player Animator’ to animator field in editor so that we have access to our animations and transitions.

Select Player Animator for animator field defined in WeaponEffector script

Congratulations! You have now finished the implementation of a very simple mechanics where you can eliminate enemies with your sword!

Conclusion

In this tutorial we have investigated the implementation of basic fighting mechanics in a tiny Unity project, which is an expansion of the movement mechanics we did last time. We made additional sprite animations and used them to make our game world more interesting. I have then equipped our character with a sword and populated the scene with evil bunnies. After that we programme a naive AI behaviour for the bunnies so that they could chase or run away from the character during gameplay. In addition, the enemies may hurt the character, which is highlighted by blinking sprites for a short period of time. We finalised the fighting mechanics by giving our character (and player) the ability to defend himself and attack evil bunnies. In the next tutorial we are going to look into how to create even more visual assets and structures for our game world.