We create a fighting game in Unity: implementation of Hitbox and Hurtbox

Original author: Strangewire
  • Transfer
image

Explanation


What are hitbox and hurtbox? Isn't that the same thing?

The answer may depend on who you ask the question, but in the article we will be of the opinion that hitbox and hurtbox are two different concepts with different uses, as is the case in any decent fighting game.

A Hitbox is an invisible rectangle (or sphere) that determines where the attack hits.

Hurtbox is also an invisible rectangle (or sphere), but defining the place where a player or object can hit using Hitbox.


In this image from Street Fighter IV, the red box is the hitbox and the green box is hurtbox.

It is worth noting that the size and position of the hitboxes and hurtboxes depends on the animation frame being played:

Gif from Killer Instinct. Note that hitboxes appear only in shot frames and move with the sword.

In the example from Killer Instinct, we also see another type of rectangle, namely Pushbox (yellow rectangle; Hurtbox are empty green rectangles). Pushbox is an area that denotes the space physically occupied by the character, not allowing them to overlap.

In most games of the fighting and beat 'em up genre, there are two more types of areas that we will not consider for simplicity:

Grab or throw box defines the area by which the character can be grabbed or thrown, and the block box determines the area in which the player being attacked pushing the back button, begins to block the attack instead of moving back.

From a design point of view, all these areas are very important. Hitboxes and Hurtboxes of attacks determine not only the number of frames in which the attack causes damage, but also the blind spots of this attack, as well as the player’s vulnerabilities.

A good explanation of this on the example of Street Fighter is presented in the video .

So, having understood the terminology, let's get to work.

What do we want


Let's look at each type of area we want to work with and say what we want from them:

Pushbox: we need two pushboxes to touch, but not overlap (that's why they are called “push areas” (pushbox ) - they push another character). Pushboxes should only interact with other pushboxes.

Hurtbox: it can record a hit, but should not perform collisions in the physical sense. Hurtboxes should only interact with Hitboxes.

Hitbox: we should be able to check whether it overlays on the Hurtbox in arbitrary frames. It should only interact with Hurtbox.

Using Unity Standard Components


First of all, we can try to bind different types of areas to standard Unity components. The obvious choice would be some Collider .

Pushbox can be directly implemented as Collider plus Rigidbody . It will behave exactly as we need - to respond to collisions with objects and not to overlap with other Pushboxes.

The only thing we need to worry about (except for the proper Rigidbody setting) is the implementation of the property " can have collisions only with other Pushboxes ". If you are familiar with the Unity physics system, then you already know that the solution is to use layers and a layer collision matrix. For clarity, we can create a layer called Pushbox, assign it to our object and configure the collision matrix so that Pushbox only collides with Pushbox .



For Hurtbox we can take Collider using isTrigger . So we guarantee that he will not perform collisions in the physical sense and will only register other colliders falling into his area. To record the impact, we need to add a script to the same object that implements the OnTriggerEnter event , possibly by checking the tag of the incoming collider to make sure that the collider that triggered the event is necessary for us, and then perform the damage and health calculations necessary for the game. You are probably familiar with this approach.

We will also need to create the Hurtbox and Hitbox layers, and we will again use the Layer Collision Matrix so that Hurtbox only collides with Hitbox and vice versa.

  • Note that we don’t need a Rigidbody, but only because I believe that every trigger we add will be a child of a Pushbox object that already has a Rigidbody. This is important because Colliders without a Rigidbody inside themselves or any of their parent objects will be defined by the Unity engine as Static and moving them will not lead to any action .
  • In addition, we will probably need to distinguish a Hitbox player from a Hitbox of one of the enemies. The same goes for Hurtbox. Thanks to this, a player’s Hitbox can only hit Hurtbox enemies, and enemy Hitboxs can only hit a player’s Hurtbox. If you want to allow damage to allies, but here you need to be careful so that the player can not hit his own Hurtbox.

Hitboxes are the hardest to implement. To avoid physical collisions, we can use Collider with isTrigger, in fact, we don’t have any colliders that are included in Hitbox. There is another way to solve this problem: Hitbox "enters" ( or checks for overlays with ) Hurtbox. Be that as it may, we need Collider, otherwise Unity will never call OnTriggerEnter in our Hurtbox.

To cause damage to Hurtbox, we need to add a script to the same object so that our Hurtbox can use GetComponentand got it to find out how much damage needs to be done. We can do this also in a different way: called for both OnTriggerEnter Colliders. We also need to find a way to make our Hitbox active only when we want, and not in every frame and not when the character does not attack. To do this, we can simply turn off the script, since, according to the documentation, Trigger events are sent to disabled MonoBehaviours to allow Behaviours to be included in response to collisions .

We can enable and disable the collider or add a Boolean value to the script, indicating whether it should hit or not.

Problems


  • Hierarchy: we need to have a script in each object with Collider in order to be able to respond to OnTriggerEnter. If you prefer to keep all scripts in one place for the sake of order, then you need to create a script just to delegate a call to all other objects.
  • Excessiveness: with this approach, our Hitbox has huge functionality that we do not need.
  • Events: our functionality is based on OnTriggerEnter. Using Unity events may not be a problem. but there are reasons at least to think about their necessity. To know more, it's also worth exploring this (in the Avoiding expensive calls to the Unity API section ).
  • Visual noise: if you want to use different Hitboxes for different attacks, then not only the problems mentioned above appear again, but there is also a visual noise in the editor window.
  • Low flexibility: when using Colliders as Hitboxes, it means that when changing the shape of the collider, for example, from Box to Sphere, you will have to manually delete all BoxCollider and add SphereCollider (or write an editor script that does this job for you)

We create everything ourselves


As you probably understood from the above, Pushboxes and Hurtboxes are quite conveniently implemented by standard Unity components.

Hurtboxes still have the above problems, and we will solve some of them, but the main entity that needs to be abstracted is Hitbox.

If you create a fighting game with many attacks and combos, then you probably want all the attacks to be well ordered in the object and have the ability to create several combinations of Hitboxes for each of them. To do this, you need a script that strictly delegates OnTriggerEnter calls to an active attack or does something similar.



image

We do not want to create a separate Hitbox object for each of the attacks, and here we can use the same ones, changing their size!

Hitbox


We need the new component to solve the following problems:

  1. To have Hitbox behavior: it should be able to check the overlay on the Hurtbox in arbitrary frames. He is obliged to interact only with Hurtbox-s.
  2. Have a visual representation in the Scene window.
  3. Be customizable and flexible.
  4. Ideally, do not depend on Unity API events.
  5. Be independent enough so that you can use the script for another object.
  6. Do not be attached to a specific attack. Hitboxes should be applicable to several different attacks.

Behavior


First, how do we verify that an area overlays a collider? The answer is to use UnityEngine.Physics .

Physics has many methods that can accomplish this task. We can specify the form we need (Box, Sphere, Capsule), and also, if desired, get the Colliders, which we hit (if any) as an array, or pass an array to fill it with these Colliders. While we will not think about it, but in the first case, a new array is allocated, and in the second, we simply fill in the existing one.

Let's start by checking to see if a rectangular area hits something. For this we can use OverlapBox .

We need to set the dimensions of the checked rectangle. To do this, we need the center of the rectangle, its half size, rotation and the layers that it must hit. Half size is half the size in each direction, for example, if we have a rectangle with a size of (2, 6, 8) then its half size will be equal to (1, 3, 4).

As the center, we can use the transform position of the GameObject, and for rotation we can use the transform rotation of the GameObject, or add general variables to set specific values.

The half-size is just Vector3, so we will make it common and use it.

For layers that can be hit, we will create a public property of type LayerMask . This will allow us to select layers in the inspector.

    Collider[] colliders = Physics.OverlapBox(position, boxSize, rotation, mask);
    if (colliders.Length > 0) {
        Debug.Log("We hit something");
    }

In the case of the correct setting and when the projected rectangle is superimposed on the Collider in the corresponding mask during the call, we should see a message in the console.

Visual presentation


All this is great ... but not too functional. Until we can see the rectangle defined somewhere, it will be very difficult to specify the appropriate size and location of the Hitbox.

So how do we draw a rectangle in the Scene window, but not in the game itself? Using OnDrawGizmos .

As stated in the documentation , Gizmos are designed for visual debugging or auxiliary builds in the scene window . Exactly what we need!

We will give our Gizmo color and transformation matrix . That is, we just create a matrix with the position, rotation and scale of the transform.

    private void OnDrawGizmos() {
        Gizmos.color = Color.red;
        Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, transform.localScale);
        Gizmos.DrawCube(Vector3.zero, new Vector3(boxSize.x * 2, boxSize.y * 2, boxSize.z * 2)); // Потому что размер - это половинная величина
    }

If desired, you can use OnDrawGizmosSelected to draw a rectangle only when selecting an object.

Customizability and flexibility


Customizability is an extensive topic, it largely depends on the type of game being created and the necessary functionality.

In our case, we will allow you to make quick changes to the shape and color of the hitbox. If you use Anima2D or some kind of skeletal animation, then you may need to. so that the Hitbox scales to fit the scale of the bones.

To change the form, just add a Boolean property and change OverlapBox to any other form, for example, OverlapSphere . To set up a sphere, you will need to add a public radius property. Do not forget that to draw a new form, you will need to change the OnDrawGizmos event (in our example, this will be DrawSphere ).

It should be noted that we do not add a new component and do not delete anything, but simply created a Boolean value that will select the overlapping form when checking for collisions. It allows us to change the shape of the hitbox depending on the attack (or even for the same attack).

As for color, I want the Hitbox to change color under the following conditions: it is inactive, it checks for collisions, or it detects a collision with something. Later we will need these states for logic, so let's add them.

We will create an enum for the state and three colors that we add as properties of our Hitbox.

    public enum ColliderState {
        Closed,
        Open,
        Colliding
    }

Your class might look something like this:

    public class Hitbox: MonoBehaviour {
        public LayerMask mask;
        public bool useSphere = false;
        public Vector3 hitboxSize = Vector3.one;
        public float radius = 0.5f;
        public Color inactiveColor;
        public Color collisionOpenColor;
        public Color collidingColor;
        private ColliderState _state;
        /*
            здесь методы
        */
    }

Now we can update gizmos by replacing the string Gizmos.color = Color.red;with a new method call:

    private void checkGizmoColor() {
        switch(_state) {
        case ColliderState.Closed:
            Gizmos.color = inactiveColor;
            break;
        case ColliderState.Open:
            Gizmos.color = collisionOpenColor;
            break;
        case ColliderState.Colliding:
            Gizmos.color = collidingColor;
            break;
        }
    }

So where are we going to change the state? We need three elements:

  1. way to tell hitbox to start collision checking
  2. way to tell him to stop
  3. collision check method

The first two are obvious:

    public void startCheckingCollision() {
        _state = ColliderState.Open; 
    }
    public void stopCheckingCollision() {
        _state = ColliderState.Closed; 
    }

Now that Hitbox is active, we want to check in each frame whether it has collisions with anything while it is active. In doing so, we move on to the next item.

Unity Events API Independence


As you may know, to check something in each frame, we can use Update (for the sake of simplification, I do not add a check for resizing):

    private void Update() {
        if (_state == ColliderState.Closed) { return; }
        Collider[] colliders = Physics.OverlapBox(position, boxSize, rotation, mask);
        if (colliders.Length > 0) {
            _state =  ColliderState.Colliding;
            // Здесь мы должны сделать что-то с коллайдерами
        } else {
            _state =  ColliderState.Open;
        }
    }

As you can see, we only return if the current state is “Closed”. This means that we still check for collisions if the Hitbox encounters something, which allows the Hitbox to hit several objects at once, and not just the first one hit. In your game, you can perform processing differently.

We use Update, but do not want to depend on the Unity Events API! The solution may be to create your own public update method, which can be called hitboxUpdate (its contents will be the same as the Update method), and only be called in the Hitbox used in the current attack.

Obviously, we will need to call Update () in some objects higher in the hierarchy, but we definitely do not need to use them in every Hitbox all the time simply because they exist.

Hitbox using a script in another object


Remember - the problem of using Collider was that to implement OnTriggerEnter we needed a script in the same GameObject? Since we use our own script and can add it to anything, the solution is quite obvious.

We will add an object as a property so that we can call some method for it when Hitbox collides.

To solve this problem, you can use different approaches:

  • We can add a public GameObject and use SendMessage (this method has very low speed).
  • You can do the same with Monobehaviour, using the method that is called when Hitbox collides. This method has its drawback: if we want the Hitbox to use different scripts, then we will need to add all these properties or inherit from the base script that contains the called method
  • You can create an interface with the method you want to call and implement it in each class that Hitbox should use.

In terms of structure, the obvious choice for me is interface. The only problem with Unity is that the default editor does not display interfaces as public properties, so we cannot assign it in the editor. In the next paragraph, I will explain why this is not a serious problem.

Let's create and apply the interface:

    public interface IHitboxResponder {
        void collisionedWith(Collider collider);
    }

Add it as a property to our Hitbox ...

    public class Hitbox : MonoBehaviour {
        ...
        private IHitboxResponder _responder = null;
        ...
        /*
            и оставшаяся часть класса
        */
    }

Also, if desired, you can use an array instead of one respondent.

Let's use the respondent:

    public void hitboxUpdate() {
        if (_state == ColliderState.Closed) { return; }
        Collider[] colliders = Physics.OverlapBox(position, boxSize, rotation, mask);
        for (int i = 0; i < colliders.Length; i++) {
            Collider aCollider = colliders[i];
            responder?.collisionedWith(aCollider);
        }
        _state = colliders.Length > 0 ? ColliderState.Colliding : ColliderState.Open;
    }

If you are unfamiliar with the "?" Operator, then read this .

Excellent! But how do we set a property _responder. Let's add a setter and consider it in the next paragraph.

    public void useResponder(IHitboxResponder responder) {
        _responder = responder;
    }

Hitboxes should be applicable to several different attacks, and not tied to one


In this section, I will explain why it doesn’t matter that we cannot set HitboxResponders using an editor.

First, let's talk about these “respondents”. In the approach we choose to implement Hitbox, the respondent is any class that should do something when Hitbox is in conflict with Collider, that is, it implements IHitboxResponder . For example, you can take an attack script: we want it to cause damage to what we hit.

Since we want it to not be associated with any particular attack and it can be used repeatedly, assigning respondents in the editor would not give us anything, because we want to be able to change respondents on the fly.

Suppose we have two types of attacks - a direct hit and an uppercut with the same hand, and each of them has its own script that tells in which frames of the animation it should hit, how much damage it does and the like. Since both of these attacks are carried out by the same limb, let's use one Hitbox.

    public class Attack: Monobehaviour, IHitboxResponder {
        ...
        public int damage;
        public Hitbox hitbox;
        ...
        public void attack() {
            hitbox.setResponder(this);
            // выполняем всё остальное для атаки
        }
        void collisionedWith(Collider collider) {
            Hurtbox hurtbox = collider.GetComponent();
            hurtbox?.getHitBy(damage);
        }
    }

Excellent! We created working hitboxes. As you can see from the code above, we added the Hurtbox method getHitBy(int damage). Let's see if we can improve it.

Hurtbox Enhancements


Ideally, for Hurtboxes, we want to implement more or less the same points as for Hitboxes. This should be easier, because Collider has the functionality we need. We also need to use Collider, otherwise Physics.Overlap ... will not report a hit.

Note that due to the way we structured the code, we do not need to use OnTriggerEnter for anything, we get the script using GetComponent.

This gives us customizability and flexibility. To provide the same flexibility as Hitbox's, we need to add and remove colliders on the fly, and for customization, we can draw color on the collider depending on its state.

    public class Hurtbox : MonoBehaviour {
        public Collider collider;
        private ColliderState _state = ColliderState.Open;
        public bool getHitBy(int damage) {
            // Делаем что-то с уроном и состоянием
        }
        private void OnDrawGizmos() {
            // Можно просто снова использовать код из хитбокса,
            // но получая размер, поворот и масштаб из коллайдера
        }
    }

Adding and removing Colliders on the fly is not as easy as Hitboxes. I have not found a satisfactory way to accomplish this task. We can add several different Colliders to the script and select the desired one using a boolean value, as we did in Hitbox. The problem here is that you need to add every needed Collider as a component, and as a result we get a strong visual noise in the editor and on the object.

Another approach would be to add and remove components through code, but such a solution will add a lot of unnecessary garbage and will probably not be as accurate.

It would be ideal for Hurtbox to inherit from Collider, and all its logic of forms was internal and we could only render the form that we are currently using, but I could not get this system to work as I wanted.

What's next?


If you repeated carefully followed and repeated operations in the post, now you have hitboxes, hurtboxes and pushboxes implemented in Unity. But more importantly, we now know these abstractions, and it will greatly simplify the work if you build something on top of them.

Your script inspector probably looks awful right now, but don’t worry, we’ll cover this in the next post:



We can turn this ...


Into something like this!

Also popular now: