開発者コンソール
アクセスいただきありがとうございます。こちらのページは現在英語のみのご用意となっております。順次日本語化を進めてまいりますので、ご理解のほどよろしくお願いいたします。

Hitboxes and Hurtboxes

Written in October 2017 by Nathan Ranney, the founder of game development studio Gutter Arcade.

Hitboxes and hurtboxes are specialized collision checks (collision checks allow you to determine when objects come in contact or overlap). A hitbox is usually associated with some form of attack, and describes where that attack is effective. A hurtbox is usually associated with a character (or any other "hittable" object in your game). Whenever the two collide, we consider the attack to have "landed" and we apply its effect on the target.

I am going to be using fighting games as the main example for this entry. In my opinion, fighting games offer the clearest examples of hitboxes and hurtboxes.

See the example below from Ultra Street Fighter IV.

Here we see Makoto performing one of her special moves, Fukiage. This is an upward-angled punch used as an anti-air attack, hitting an opponent that is jumping at you. The red rectangle is the hitbox of the attack, while the green rectangle is the hurtbox. If Makoto were to touch someone else's hurtbox with her hitbox, the other player would be "hit."

Let's get started with the setup.

Hurtbox Setup

First, we need a sprite for our hurtbox. Create a new sprite, name it sprHurtbox, make it just a single pixel, and color it green. We only need a single pixel because we are going to scale it to whatever size we need whenever we instantiate a hurtbox. The alternative would be to create a custom-sized hurtbox sprite for every game object that might need one, which would be tedious and wasteful.

Now that you have your sprite, let's create the object. Create a new object, name it oHurtbox, and assign the sprHurtbox sprite to it. Add the create event, and add the following code.

Copied to clipboard.

image_alpha = 0.5;
owner = -1;
xOffset = 0;
yOffset = 0;

This is all the code we need for the hurtbox. We want to set the image_alpha to 0.5 so that the hurtbox is transparent. The owner variable will be tied to the id of whatever object created it, such as the oPlayer object. More on that in a bit. Finally, the xOffset and yOffset is used to line up the hurtbox with its owner. Now we need to create the hurtbox and give it an owner.

Create a new script and name it hurtbox_create. Add the following code.

Copied to clipboard.

_hurtbox = instance_create(x,y,oHurtbox);
_hurtbox.owner = id;
_hurtbox.image_xscale = argument0;
_hurtbox.image_yscale = argument1;
_hurtbox.xOffset = argument2;
_hurtbox.yOffset = argument3;

return _hurtbox;

This script looks complicated, but it's fairly simple. First, we create an oHurtbox object and store the ID of that object in the _hurtbox variable. Then, using the _hurtbox variable, we pass in the owner, which will be whatever object is calling this script. From there we define the scale, and offset of the hurtbox. Now that the script is created we can put it into action. Open the oPlayer object and add the following code to the create event.

Copied to clipboard.

//hurtbox
hurtbox = hurtbox_create(18,24,-9,-24);

//hitbox
hitbox = -1;

Using the hurtbox_create script we just made, we are able to set the scale and offset really easily, and store the ID of the oHurtbox object in a variable that the oPlayer object can use. The numbers used in the script are measured in pixels. The hurtbox we are creating is 18 pixels wide, 24 pixels tall, offset 9 pixels to the left of the player sprite, and offset 24 pixels above the player sprite. If you run the game now, you will notice that your hurtbox isn't following your character around, so let's fix that before moving on. Open the end step event in your oPlayer object and add the following code.

Copied to clipboard.

//hurtbox
with(hurtbox){
    x = other.x + xOffset;
    y = other.y + yOffset;
}

This little chunk of code makes sure the hurtbox is following our player around. Using with and other may be a new concept, so let me explain. When you use with followed by an object name (or specific object ID) the code following runs as if that object were running it. So when we say with(hurtbox) we are updating the x and y position from that particular oHurtbox object that we have stored in our hurtbox variable.

Since we are using with we can also use other. When using other in this context, it is referring back to the original object this code is running from. In this case, that is our oPlayer object.

Now you can see your hurtbox following the player around in the game.

When play-balancing your game, there is really only one rule to consider for hurtboxes – The smaller the hurtbox, the better it is. It is a lot harder to hit something that is 2x2 pixels than it is 50x50 pixels.

Hitbox Setup

Now that we have our hurtbox, we need to hit it. The setup required for a hitbox is pretty similar to that of the hurtbox, but it has a bit more going on. The hitbox is what actually checks for collisions and determines what to do after a collision is detected.

Just like the hurtbox, we need to create a sprite and an object. Create a one-pixel sprite named sprHitbox and color it red. Then create the oHitbox object and assign the sprHitbox sprite. Add the create, step, end step, and destroy events to this object. Open the create event and add the following code.

Copied to clipboard.

image_alpha = 0.5;
owner = -1;
xOffset = 0;
yOffset = 0;
life = 0;
xHit = 0;
yHit = 0;
hitStun = 60;
ignore = false;
ignoreList = ds_list_create();

Like with our hurtbox, we need to set an owner and offset. However unlike a hurtbox, a hitbox doesn't exist at all times. It only exists during an attack. The life variable will be used to determine how many frames the hitbox will exist and remain active. xHit and yHit are our knockback variables. hitStun determines how long the character we hit is put into hit stun. More on that below. Finally, the ignore and ignoreList variables will be used to ensure we don't hit a character too many times. You'll see how that works in a bit.

Hit Stun is how long, in frames, that a character is stunned after being hit. This is the cornerstone of performing combos in a fighting game. If your next attack starts up before your opponent recovers from hit stun, you can hit them again.

Open your destroy event and add the following code.

Copied to clipboard.

owner.hitbox = -1;
ds_list_destroy(ignoreList);

This ensures that the owner of the hitbox stops trying to interact with it once it has been deleted, and it deletes the ignoreList when it is no longer needed. If the list is not deleted, it can cause memory leaks, which we want to avoid.

Moving on to the step event – open that up and add the following line.

Copied to clipboard.

life --;

This will subtract from the life of the hitbox while it is active. When the life variable reaches zero, the hitbox will be deleted. Which brings us to the end step event. This is where our last bit of code will go. Open it up and add the following code.

Copied to clipboard.

if(life <= 0){
    instance_destroy();
}

When an object is destroyed, like we are doing above, the destroy event will be called (if present). The hitbox setup is complete (for the actual object). There is still a lot to do. Much like the hurtbox, we need a hitbox_create script. Create a new script, name it hitbox_create, and add the following code.

Copied to clipboard.

_hitbox = instance_create(x,y,oHitbox);
_hitbox.owner = id;
_hitbox.image_xscale = argument0;
_hitbox.image_yscale = argument1;
_hitbox.xOffset = argument2;
_hitbox.yOffset = argument3;
_hitbox.life = argument4;
_hitbox.xHit = argument5;
_hitbox.hitStun = argument6;

return _hitbox;

This works exactly like our hurtbox_create script, although we are passing in a bit more information. Other than the scale and offset, we also need to set the life, xHit, and hitStun of the hitbox.

Go back into the end step of your oPlayer object and add the following lines right below your hurtbox code.

Copied to clipboard.

//hitbox
if(hitbox != -1){
    with(hitbox){
        x = other.x + xOffset;
        y = other.y + yOffset;
    }
}

This is slightly different than the hurtbox code, in that we always want to make sure we actually have a hitbox in game at the time. We do this by first checking to see if our hitbox variable does NOT equal -1.

Now, the final step, we need to actually create the hitbox at the right time during our attack. But before we do that, I need to give you a brief rundown on the anatomy of an attack in a fighting game. All attacks are broken up into three parts – start up, active, and recovery. Each of these parts lasts a certain number of frames. Check out the diagram below:

Start up is how long it takes for your attack to become active. It is the wind up to your punch or kick. Active is how long the hitbox is able to actually hit someone. Recovery is how long it takes for your character to finish out their attack and return to a neutral state, after which they are able to perform other actions again. Let's take a look at our character sprite to determine where our start up, active, and recovery frames should be.

Our start up frames are frames zero to two. These are the wind up of the attack. Active frames are three to four, and recovery five to seven. We need to create our hitbox on frame three, and it needs to be active until the start of frame five. In my project, my sprites are animating at about four frames per second, given that my frameSpeed variable is 0.15 and the game is running at 60 fps. This means the life of my hitbox needs to be eight frames.

Open up the attack_state script and add the following lines.

Copied to clipboard.

//create hitbox on the right frame
if(frame == 3 && hitbox == -1){
    hitbox = hitbox_create(20 * facing,12,-3 * facing,-16,8,3 * facing,45);
}

We are checking to see if we are on the right frame, and that we don't already have a hitbox. If so, we create the hitbox using our hitbox_create script. When creating the hitbox, we need to multiply the horizontal values (both scale and offset) by the direction the character is facing. This ensures that the hitbox is always lined up with the orientation of the character. Then we set our eight frames of life, followed by horizontal knockback and hitstun. If you run the game and start attacking, you should see the hitbox appear and disappear as intended. Now we need to make it hit something.

Enemy Setup

Before we can do any punching, we need an enemy to punch. This is going to be pretty easy, as the enemy is going to use a lot of the same code as our player. We will, however, need to add some new sprites. You can use any sprites you want, or download the same sprites that I am using.

Create the sprites the same way as we created the player sprites. Make sure the sprite origin is (16, 32) just like last time. You should have two sprites: sprEnemy_Idle and sprEnemy_Hurt.

Duplicate the oPlayer object and name it oEnemy. Assign the sprEnemey_Idle sprite to the object, and then open up the create event. We need to add some new variables.

Copied to clipboard.

hit = false;
hitStun = 0;
hitBy = -1;

Hit is a simple boolean we will use when applying hit effects. Next, hitStun is how long the enemy will remain in hitStun after being hit. Finally, hitBy will be the ID of the object that hit them.

Moving on to the step event. Open that up, and delete the lines pertaining to player buttons and the state switching. We don't want the enemy to perform actions when we push buttons, and we need to re-write the state switching. Add the following code.

Copied to clipboard.

//state switch
switch currentState {
    case states.hit:
        hit_state();
    break;
}

Since our enemy is only going to stand still or be hit, we don't need any other states at the moment. However we do need to create the hit_state script. Do that now and add the following code.

Copied to clipboard.

xSpeed = approach(xSpeed,0,0.1);

hitStun --;

if(hitStun <= 0){
    currentState = states.normal;
}

This should look pretty familiar to you if you have been following along. First, we reduce the horizontal speed of the enemy until it reaches zero. Next, we count down hitStun, and return the enemy to their default normal state when hitStun reaches zero.

Moving right along to the end step event. First, replace animation_control(); with animation_control_enemy(); and then add this below the hurtbox code.

Copied to clipboard.

//get hit
if(hit){
    squash_stretch(1.3,1.3);
    xSpeed = hitBy.xHit;
    hitStun = hitBy.hitStun;
    facing = hitBy.owner.facing * -1;
    hit = false;
    currentState = states.hit;
}

This is where we apply hit effects like knockback, squash and stretch, screenshake (if we had it), and so on. It also changes the enemy state to the hit state, which locks them out of performing any other actions while they are in hit stun.

Before we stray too far, we need to create the animation_control_enemy script. This is the same kind of script that the player uses, but simplified, considering the enemy has fewer animations and behaviors than the player. Check out the code below and make sure your animation_control_enemy script matches.

Copied to clipboard.

xScale = approach(xScale,1,0.03);
yScale = approach(yScale,1,0.03);

//animation control    
switch currentState {
    case states.normal:
        sprite = sprEnemy_Idle;
    break;

    case states.hit:
        sprite = sprEnemy_Hurt;
    break;
}

//reset frame to 0 if sprite changes
if(lastSprite != sprite){
    lastSprite = sprite;
    frame = 0;
}

All we are doing is setting the sprite based on the state just like we did with the player.

Now, the enemy setup is complete. Place an enemy or two in the room. Now we are moving on to the hard part: checking for hitbox/hurtbox collision (overlap), and resolving that collision.

Hit Check and Resolve

Now, we are going to use with and other again, but nested within itself. Telling an object what to do from inside of another object that is inside of another object.

Let's go back into the oPlayer object and open the end step event where you put the hitbox code earlier. Update it to look like this.

Copied to clipboard.

//hitbox
if(hitbox != -1){
    with(hitbox){
        x = other.x + xOffset;
        y = other.y + yOffset;

        //check to see if the hurtbox is touching your hitbox
        with(oHurtbox){
            if(place_meeting(x,y,other) && other.owner != owner){
            //do some stuff
           }
        }
    }
}

We check to see if we actually have a hitbox at the time, and if so, we then check all hurtbox objects to see if any of them are colliding with this particular hitbox instance. When using with it's important to note that if you just use the name of an object, like oHurtbox, instead of the instance ID of an object, you will be running code from within ALL instances of that object. Now we are two layers deep, and are checking the collision from the hurtbox, so when we use other it is no longer referencing the main object (the oPlayer object) that is running all of this code, but instead the object that is one layer above this one (the oHitbox object).

See the diagram below for a visual representation of what is happening.

The oPlayer object is using with to talk to the oHitbox object, which is then using with to talk to the oHurtbox object. Each of these with calls creates a new layer to the code. When an object is using other, it is referring back to the layer above it. It is imperative to understand these layers and how with/other work together to fully understand how these collision checks will work.

Finally, we need to resolve the collision. We have already checked to see if the hitbox and hurtbox have collided, and now we need to decide what happens next. This is where our ignore variable, and our ignoreList comes into play. First we need to check and see if the hitbox has already hit the hurtbox.

Copied to clipboard.

//hitbox
if(hitbox != -1){
    with(hitbox){
        x = other.x + xOffset;
        y = other.y + yOffset;


        //check to see if the hurtbox is touching your hitbox
        with(oHurtbox){
            if(place_meeting(x,y,other) && other.owner != owner){
                //ignore check
                //checking collision from the hitbox object
                with(other){
                    //check to see if your target is on the ignore list
                    //if it is on the ignore list, dont hit it again
                    for(i = 0; i < ds_list_size(ignoreList); i ++){
                        if(ignoreList[|i] = other.owner){
                            ignore = true;
                            break;
                        }
                    }
                }
            }
        }
    }
}

We had to do one additional with function after determining that our hitbox has collided with a hurtbox, and that these two boxes had different owners. The owner check prevents hitboxes from colliding with hurtboxes that belong to the same player, and thus, prevents the player from kicking their own butt.

Next we check through our list of enemies to ignore. If you have never used a for loop before this may be a bit confusing, but it is much more simple than it looks.

A for loop runs a block of code a certain number of times. In this case, it runs as many times as there are instances of data in our ignoreList. It checks each spot in the list, and compares it to the owner of the hurtbox it has just collided with. If any of the data in the list matches the owner of the hurtbox, the owner is ignored, and does not get hit, and we stop checking the list by using break. We do this to prevent the same enemy from being hit every single frame our attack is active. If this ignore check wasn't present, then the enemy would be hit eight times in eight frames.

You may be wondering how the ignoreList gets populated with data. That is the next step. If our first check fails, that is, if the enemy should NOT be ignored, we can hit them and add their data to the list. Make the following changes to your code.

Copied to clipboard.

//hitbox
if(hitbox != -1){
    with(hitbox){
        x = other.x + xOffset;
        y = other.y + yOffset;

        //check to see if the hurtbox is touching your hitbox        
        with(oHurtbox){
            if(place_meeting(x,y,other) && other.owner != owner){
                //ignore check
                //checking collision from the hitbox object
                with(other){
                    //check to see if your target is on the ignore list
                    //if it is on the ignore list, dont hit it again
                    for(i = 0; i < ds_list_size(ignoreList); i ++){
                        if(ignoreList[|i] = other.owner){
                            ignore = true;
                            break;
                        }
                    }

                    //if it is NOT on the ignore list, hit it, and add it to
                    //the ignore list
                    if(!ignore){
                        other.owner.hit = true;
                        other.owner.hitBy = id;
                        ds_list_add(ignoreList,other.owner);
                    }
                }
            }
        }
    }
}

If ignore is false, then the owner of the hurtbox (other.owner) is hit. We need to tell that object it was hit (other.owner.hit = true) and what hit them (other.owner.hitBy = id). Then add them to the ignore list so we don't hit them again on the next frame (ds_list_add(ignoreList,other.owner). You should now be able to run the game and attack your enemy. They should get knocked back and put into hit stun.

This is just one way to set up hitboxes and hurtboxes, and even though this uses GameMaker-specific code, it should be applicable to any programming language as long as you can do simple AABB (axis-aligned bounding-box) collision checks.

Resources