Howdy, folks. Due to the overwhelming response from the hitbox and hurtbox tutorial I posted, and due to a lot of questions from you folks about how to do this particular thing, I’m going to show you how to set up a basic combo system. I will be working from the aforementioned post about hitboxes and hurtboxes, so if you have followed along with that you can pick right up where you left off for this entry. If not, you can read it here.
Before we get started, I want to point out that there are different kinds of combos. In general, a combo is when one attack connects right after another, without your opponent being able to do anything in between. However, the way you get from your first attack to your second attack can be different.
A “chain” combo is when your second attack interrupts, or cancels, your current attack. Your first attack stops right where it is, and the second attack comes out. A “link” combo is when your first attack completely finishes, your character returns to their neutral state, and your opponent is still stunned long enough for you to start up a second attack and hit them.
Chain combos are generally much easier to perform, and require less strict timing than a link combo; in other words, a good place to start for a fledgling combo system programmer such as yourself. In this blog, we are going to be working with chain combos.
Assuming you are working from the previous blog post (you should be!), we need to make a few changes to allow for combos.
This is a pretty easy process. First of all, we need some more attack sprites. Here are two new attacks to add to your project.
sprPlayer_Attack_2
sprPlayer_Attack_3
These are a bit teeny, but that is because they are in the native resolution of the game. I also made some changes to the original attack animation from the first blog. It was a little too long to work with our new attacks, so I deleted a bunch of frames to make it fit in with our new sprites.
sprPlayer_Attack
Go ahead and put those in your game. When importing the sprite strips, you can use a 32x32 canvas size. Name the sprites using the names typed above. Set your sprite origins to 16x32, so they are in line with the rest of the player sprites.
As you may have guessed, we will need a couple of new variables to get started. Open the create event in your oPlayer object, and add the following code to the bottom.
oPlayer Create Event
currentAttack = attacks.right;
hitLanded = false;
currentAttack is going to be used to define different behavior, and animation, based on the attack our player is currently doing. hitLanded will be our boolean we use to know when we can cancel our current attack into the next attack.
Open the enum_init script, and let's add some new enums for our new attacks. Add this code to the bottom of the script.
enum_init script
enum attacks {
right,
left,
upper
}
Finally, we need to expand our hitbox_create script to include one more argument. Open that script and copy/paste the following code over it.
hitbox_create
_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.yHit = argument6;
_hitbox.hitStun = argument7;
return _hitbox;
We added the _hitbox.yHit = argument6; line, and changed _hitbox.hitStun from argument6 to argument7. Doing this will allow us to hit enemies into the air instead of just backwards across the ground.
Open normal_state script and add a line to the bottom of the script.
normal_state script
//reset attack
currentAttack = attacks.right;
This ensures our attacks always start from the first attack in the chain of attacks.That's all for initializing new variables and enums.
Now that we have all of these new attacks, we will need to set up animation for each of them. This is easily achieved using the new enums we just initialized, and a nested switch statement. Open the animation_control script and let's change the states.attack case to include the new sprites.
animation_control script
case states.attack:
switch(currentAttack){
case attacks.right:
sprite = sprPlayer_Attack;
break;
case attacks.left:
sprite = sprPlayer_Attack_2;
break;
case attacks.upper:
sprite = sprPlayer_Attack_3;
break;
}
break;
We will be doing the same thing in the attack_state script.
attack_state
switch(currentAttack){
case attacks.right:
//create hitbox on the right frame
if(floor(frame) == 1 && hitbox == -1){
hitbox = hitbox_create(20 * facing,12,-3 * facing,-16,8,1 * facing,0,60);
}
//cancel into next attack
if(attack && hitLanded){
currentAttack = attacks.left;
hitLanded = false;
hitbox = -1;
frame = 0;
}
break;
case attacks.left:
//create hitbox on the right frame
if(floor(frame) == 1 && hitbox == -1){
hitbox = hitbox_create(20 * facing,12,-3 * facing,-16,8,1 * facing,0,45);
//also move forward
xSpeed = 3 * facing;
}
//cancel into next attack
if(attack && hitLanded){
currentAttack = attacks.upper;
hitLanded = false;
hitbox = -1;
frame = 0;
}
break;
case attacks.upper:
//create hitbox on the right frame
if(floor(frame) == 1 && hitbox == -1){
hitbox = hitbox_create(20 * facing,12,-3 * facing,-16,8,1 * facing,-4,45);
//also move forward
ySpeed = -3;
}
break;
}
I have a lot to explain here. First, we did the exact same thing we did in the animation_control script, and added a switch statement with cases for each attack type. The hitbox_create scripts were updated to include our new yHit argument. This is the second to last value. You’ll notice this has been set to zero for the first two attacks. That is because we don’t want to knock the enemy into the air for either of these attacks.
Secondly, I added a check to allow canceling into the next attack. We check to see if the attack button is being pressed AND hitLanded is true. If both conditions are met, change to another attack, reset the animation frame to zero, and set hitbox to -1. If the hitbox variable was not reset to -1, a new hitbox would not be created when the next attack happens. We also set currentAttack to whatever attack we want to cancel into.
Lastly, when hitboxes are created in the second and third attacks, we also apply some force to the player character. You’ll see xSpeed and ySpeed are being set. In the attacks.left case, this will move the player forward a bit to make sure this attack connects with the enemy after the first attack knocks the enemy backward. The ySpeed set in the attacks.upper case will launch the player into the air, and really sell the idea of a powerful uppercut attack.
We added yHit to the hitbox_create script, but it hasn’t been applied to the enemy yet. In the oEnemy object, open the end step event and let's add one line to the hit check at the bottom.
oEnemy End Step
//get hit
if(hit){
squash_stretch(1.3,1.3);
xSpeed = hitBy.xHit;
ySpeed = hitBy.yHit;
hitStun = hitBy.hitStun;
facing = hitBy.owner.facing * -1;
hit = false;
currentState = states.hit;
}
The new line ySpeed = hitBy.yHit; was added right after the xSpeed = hitBy.xHit; line.
The last thing we need to do before our combos will work is set hitLanded to true when our opponent get hits by our hitbox. This is handled in the oPlayer End Step event.
oPlayer End Step
//hitbox
if(hitbox != -1){
with(hitbox){
x = other.x + xOffset;
y = other.y + yOffset;
//collision check
//checking the collision from the hurtbox object
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;
owner.hitLanded = true;
ds_list_add(ignoreList,other.owner);
}
}
}
}
}
}
I’ve bolded the line that has to be added. When a hitbox connects to an enemy, the hitLanded bool is set to true, which allows us to combo.
Open the normal_state script and add this to the end of the script.
normal_state script
//reset hit landed
hitLanded = false;
This will ensure that hitLanded is always set to false before an attack can happen, since our attacks are always initiated from the normal_state script.
Run the game and start punching your opponent! If you mash the attack button, your character should execute a chain combo consisting of all three attacks.
That about covers it for chain combos. Thank you for taking the time to read over this, and I’ll catch you next time. As always, you can reach me on Twitter or visit my website for more gamedev stuff.
You can also download the project file here.
Nathan Ranney is the founder of game development studio Gutter Arcade. He's best known for the creation and development of Knight Club, an online indie fighting game.