In the days of the neighborhood Arcade and the early days of console gaming, Beat ’Em Up games were one of the most popular genres. Born from the marriage of the platformer and the Fighting Game, Beat ’Em Ups were combo-based action games where the player fought off hordes of enemies while progressing through mostly horizontal environments.
In modern gaming, Beat ’Em Up classics like Turtles In Time and X-Men the arcade game have been replaced by 3D Brawlers like Shadow of Mordor and Arkham Knight, the AAA evolution of these quarter-guzzling classics.
In this article series, we‘re going to look back and explore how to make a classic Beat ’Em Up game.
Getting Ready
Before moving forward, let’s talk about the skills necessary for the tutorial. The game we’re making will be built in Game Maker Studio Pro, which is available for free from the YoYo Games website.
You should also have at least a basic understanding of GML, Game Maker’s built-in coding language. While I will be taking the time to explain how the code works and what everything does, we will be talking about some moderately advanced programming concepts, including AI, so it’ll be easier to understand if you already have a strong foundation to work from.
Also make sure you download the Part 1 Assets zip file that’s included at the top of this article, since it includes the graphics and sounds you'll need to complete this article This zip file also includes a completed Game Maker project for this part of the series, so you can refer to it if you run into any issues.
Now open Game Maker, and let’s get started.
Importing Graphics
In this article we’re going to create a player object and an enemy object we can kill, and we'll give the player the ability to move around and attack. Before we can do any of that, though, we need to add some graphics we can use for the player and the enemy. Let’s start with the Player Idle animation.
- Right-click the Sprites folder and choose Create Sprite.
- Name the sprite SPR_PlayerIdle.
- Use the Load Sprite option, and navigate to the image files you downloaded for this tutorial.
- Choose Assets > Images > Player > PlayerIdle.png. This is the only frame we’ll need for this animation.
Now that the image is imported, we need to set the origin. If you take a look at all of the player animations, you’ll notice that the images are not all the same size. This means that the character’s position is not the same in each image either. If we used these images together as-is, it could cause the character to shift position as they animate, and throw off our bounding boxes. In the image below, you can see the first frames of three different player animations, and how their positioning compares.
If we transitioned between these animations without adjusting the origins, the player’s position would clearly change from one to the next. To resolve this, we can manually set the origin so that it’s in roughly the same position for every animation, and the Player won't shift.
When doing this, it helps to have a reference point on the character to use as a guide so you can easily tell if the origin needs to be adjusted. For all of the character graphics, I will be using the large spot on the chest as my reference point. I’ll position the origin at the very bottom of the image and on the left side of the spot. I've increased the saturation on the image below so you can easily see what I'm referring to.
For the Idle animation, the origin will be at x = 40, y = 117.
Now we need to import the rest of the sprites we’ll use in this article. Use the table below to see which images to import, what to name the sprite, and where the origin should be located.
Sprite Name | Images | Origin |
---|---|---|
SPR_PlayerWalking | PlayerWalking1.png, PlayerWalking2.png, PlayerWalking3.png, PlayerWalking4.png, PlayerWalking5.png, PlayerWalking6.png | X = 43, Y = 117 |
SPR_PlayerBasicPunch | PlayerPunchA1.png, PlayerPunchA2.png, PlayerPunchA3.png, PlayerPunchA4.png, PlayerPunchA5.png | X = 55, Y = 122 |
SPR_PlayerStrongPunch | PlayerPunchB1.png, PlayerPunchB2.png, PlayerPunchB3.png, PlayerPunchB4.png, PlayerPunchB5.png | X = 60, Y = 124 |
SPR_PlayerHit | PlayerHit.png | X = 43, Y = 117 |
We also need to import some of the Enemy graphics. We can do this the same way. These images will be found in the Assets > Images > Standard Enemy folder.
Sprite Name | Images | Origin Position |
SPR_EnemyIdle | RedEnemyIdle.png | X = 40, Y = 117 |
SPR_EnemyWalking | RedEnemyWalking1.png, RedEnemyWalking2.png, RedEnemyWalking3.png, RedEnemyWalking4.png, RedEnemyWalking5.png, RedEnemyWalking6.png | X = 45, Y = 117 |
SPR_EnemyHit | RedEnemyHit.png | X = 46, Y = 117 |
Creating Our First Game Objects
Now that we have all the graphics we’ll need, we’ll start building our game by creating a Player object and an Enemy object.
- Right-click the Objects folder and choose Create/Insert Object.
- Name the new object OBJ_Player.
- Set the object’s sprite to SPR_PlayerIdle.
- Use Add Event > Create.
- Under the Control tab, add an Execute Code action.
- Add the following code:
///Create Player Speed = 12; SpeedMod = 1; XSpeed = 0; YSpeed = 0; IsAttacking = false; AttackType = 0; MaxHP = 100; CurrentHP = MaxHP; IsHit = false; OnGround = true; GroundY = y; image_speed = .75;
The code we have above is setting some basic variables for our Player object. As we develop our game further we’ll add more variables, but let’s look at what we have so far.
Speed
is the movement speed of the player, while SpeedMod
will modify the player’s speed based on powerups, and debuffs.XSpeed
and YSpeed
will be used to actually set the Player's speed in each direction when they're moving. IsAttacking
and IsHit
tell us whether the player is attacking or hit so we can stop him from moving, and AttackType
tells us what attack he’s using. Finally, MaxHP
and CurrentHP
keep track of the player’s health.
You should also notice the OnGround
and GroundY
variables. These variables aren't as important now, but they'll be used later when we add things like Knockback.
The last thing this code does is set the image_speed
, which controls how quickly our animations will play.
While we're at it, let's make the Enemy object too.
- Right-click the Objects folder and choose Create/Insert Object.
- Name the new object OBJ_Enemy.
- Set the object’s sprite to SPR_EnemyIdle.
- Use Add Event > Create.
- Under the Control tab, add an Execute Code action.
- Add the following code:
Speed = 5; SpeedMod = 1; IsHit = false; MaxHP = 80; CurrentHP = MaxHP; OnGround = true; GroundY = y; image_speed = .75; SideMod = 1;
As you can see, our Enemy is currently very similar to our player, and uses many of the same variables. The one one extra variable is SideMod
, which we will eventually use for our Enemy's AI.
Adding a Room
With our basic enemy and player objects set up, let’s add a room to our game for them to run around in.
- Right-click the Rooms folder and choose Create/Insert Room.
- Leave the room size as is for now.
- Go to the Objects tab.
- Select the OBJ_Player and add it to your room on the left side.
- Select the OBJ_Enemy and add a few enemies on the right side.
Below you can see what the basic room could look like, but it’s alright if you make yours slightly different than mine.
Towards the end of this series we will make a more interesting level, but this should do for now.
Adding Shadows
Before we add movement, let’s see if we can improve the look of the game a bit. Right now, things look very flat since the characters are standing in front of a uniform gray background. We can fix this by adding shadows under the characters to simulate depth and make it look as if the characters are standing in an environment, rather than a blank gray room.
- Open the Player object and choose Add Event > Draw > Draw.
- Under the Control tab, add an Execute Code action.
- Add the following code:
//Set the opacity to 60% and then draw a dark gray oval for the shadow. draw_set_alpha(.6); draw_set_color(c_dkgray); draw_ellipse(x-40,y-8,x+40,y+8,false); //Draw my own sprite. draw_set_alpha(1); draw_self();
All this code does is draw a slightly transparent, dark grey circle under the player character. It’s very simple, but when we see it in-game, it will do a lot to make the character feel as if he’s standing on the ground. When you go in-game, the character should now look like this:
Much better!
Now that the player character looks good, though, we should do the same for the Enemies. Follow the same steps to add a shadow for the enemy characters.
When you’re done, if you test your game, it should look like this:
Little changes like this can really improve a game and help it feel much more interesting. In this case the shadows did a lot to help "sell" the environment and give it depth, even if it's not perfect.
Now that our game is a bit more interesting looking, we can add some actual gameplay.
Player Movement
So far we’ve set up our characters, but we haven’t actually told them how to move or attack. Since our player can’t attack the enemy from all the way on the other side of the field, we’ll start with movement. This is a pretty long piece of code, so we’re going to make it incrementally and add it in piece by piece. Let’s add the first piece.
- Go to the Player Object.
- Use Add Event > Step > Step.
- Under the Control tab, add an Execute Code action.
- Add the following code:
if(CurrentHP > 0){ }else{ instance_destroy(); }
All this code does is check that the player’s HP is greater than 0. If it’s not, the player is destroyed.
Now add this chunk of code into the if
part of the statement:
//Checks if either the A or D buttons are pressed to make the player move Left or Right. XSpeed = 0; if(keyboard_check(ord('A'))){ XSpeed = -1*Speed; }else if(keyboard_check(ord('D'))){ XSpeed = Speed; }
This portion of code checks whether the A or D keys are being pressed, and sets the XSpeed of the Player accordingly. If the A key is down the Player's XSpeed is negative, meaning they move backwards, and if the D key is down, it's positive, meaning they move forward.
We can't just move forward and back, though. We need to be able to move up and down as well, so let's add another piece of code to handle W and S.
Add this chunk of code into the if
part of the statement, after the previous chunk:
//Checks if either the W or S buttons are pressed to make the player move Up or Down. YSpeed = 0; if(OnGround == true){ if(keyboard_check(ord('W'))){ YSpeed = -1*Speed; }else if(keyboard_check(ord('S'))){ YSpeed = Speed; } }
This code does the same thing as the previous one, except with the YSpeed.
You might be thinking to yourself, "This is great, my character is ready to move." In reality, though, all we've done so far is set their speed, and we still need to actually move them. Since the next block of code is a bit longer, we are going to again break it into smaller chunks. Let's start out by adding a stubbed version so you can see what it will do.
Add this chunk of code into the if
part of the statement, after the previous chunk.
if(IsAttacking == false && IsHit = false){ //If the player is on the ground move them with XSpeed and YSpeed, otherwise ignore YSpeed //Change the direction of the Player's sprite based on the direction they're moving //Animate the Player based on what they're doing. }
So as you can see, we are going to do three things.
First, we physically move the player using the XSpeed and YSpeed variables; however, we ignore the YSpeed if the Player is not on the ground.
Next, we change the direction of the Player's sprite based on the direction that the Player is moving in. This way if the Player is moving left, they are facing to the left, and if they're moving right, they are facing to the right.
Finally, we animate the Player so that they are using the Idle sprite when they're not moving and the walking sprite when they are.
Let's replace these comments one at a time. Replace the comment //If the player is on the ground...
with the following code:
if(OnGround==true){ if(XSpeed != 0 && YSpeed != 0){ x+=XSpeed*SpeedMod*.7; y+=YSpeed*SpeedMod*.7; }else if(XSpeed != 0 || YSpeed != 0){ x+=XSpeed*SpeedMod; y+=YSpeed*SpeedMod; } }else if(OnGround == false ){ x+=XSpeed*SpeedMod; }
The main if statement for this code checks whether the Player is on the ground. If they are, it then has an interior if statement that determines whether the Player is moving diagonally, and slows them down to prevent them from moving further than they were if they moved in one of the cardinal directions. If the player is not on the ground, only their X movement is considered.
Next, replace the //Change the direction of...
comment with the following code:
if(XSpeed != 0){ image_xscale = sign(XSpeed*SpeedMod); }
This code is much simpler than the last block. All it does is set the Xscale of the player's sprite based on the direction they're moving. The reason we check whether XSpeed is equal to 0 is that if we didn't the Player would be 0 pixels wide whenever they weren't moving.
Finally, replace the //Animate the Player...
comment with the following code:
//Animates the Player based on their speed if(XSpeed == 0 && YSpeed == 0 && OnGround == true){ SpeedMod = 1; sprite_index = SPR_PlayerIdle; }else if((XSpeed!=0 || YSpeed != 0) && sprite_index!=SPR_PlayerWalking && OnGround == true){ sprite_index = SPR_PlayerWalking; }
This is another pretty simple piece of code. If both the XSpeed and YSpeed of the Player are 0, and the player is on the ground, it must mean that the Player is not moving, and it sets their animation to Idle. Otherwise, if the XSpeed or the YSpeed is not equal to 0, the player is on the ground, and they are not already using the Walking sprite, it sets their animation to Walking.
The one really important part about this code is that we verify they are not already using the walking animation before we set it as their animation. If we didn't do this, then every frame the Player is moving this code would come back as true, and it would constantly try to restart the walking animation. By checking to make sure they're not already using that animation, we avoid this issue and allow the animation to loop normally.
The very last thing we need before our basic movement is complete is to add this code to the very end of the Step event, after the if/else
statement:
//If the player is on the ground, this sets their GroundY variable to their current y position if(OnGround == true){ GroundY = y; } //Sets the Players' depth based on their GroundY. We're using GroundY instead of y so that even when they're in the air, they will display in fornt of and behind the right objects. depth = -1*GroundY;
This code sets the GroundY for the player and sets their depth on the screen based on their GroundY. The GroundY value will be used when the player gets knocked back or thrown to keep track of where the player’s shadow is on the ground and what height they started at. This variable also helps the player character keep track of how high they are and when they should hit the ground.
The depth statement after that makes it so that the higher on the screen the player gets, the further back they will be drawn. This way, when the player is running past enemies and moving around the field, they are always drawn correctly relative to other objects/characters.
If you go in-game and start testing, you should now be able to run around the game area with the player. You may notice some issues, though, when you walk up to enemies.
The reason the depth isn’t working as I described above is because we need to add a similar statement to the Enemy’s code so that their depth is set correctly as well.
- Open the Enemy object and choose Add Event > Step > Step.
- Under the Control tab, add an Execute Code action.
- Add the following code:
if(OnGround == true){ GroundY = y; } depth = -1*GroundY;
Now if you go into the game, you’ll see that the depth is working correctly. When you move around near the enemies, you should pass in front of and behind them correctly.
Preparing for Attacks: Layers
Now that our initial movement code is complete, we can move on to attacking. Before we can get to the actual attack object, though, we need to add two things.
First, we need a new constant which tells us how close characters must be to hit each other. In a Beat 'Em Up like this, we are simulating 3D depth. We do this in a few ways, and one way is by keeping track of player depth using the depth variable as we did above. The other way we'll do this is by defining how far apart two objects can be on the Y-plane, before they can no longer interact. This way, even if the sprites overlap/collide, the collisions will only count if the characters are close enough in the game space to allow that interaction and the perspective won’t cause any problems.
If you’re still not sure what I mean, take a look at the image below:
In this image, the player and enemy are technically colliding, but you can tell from the perspective that they are not really close enough to interact. By creating a constant called LayerSize
, we can set the maximum vertical distance to allow before two objects can no longer interact.
- On the sidebar under Macros, choose All Configurations.
- In the very first macro, enter the name LayerSize and set the value to 35.
- Press Okay.
This was a value I came up with over time which I thought worked well, but if you'd like you can play around with increasing or decreasing this value to find something that works better for your game.
As we make our attacks, this constant will help determine whether the attacks hit.
Preparing for Attacks: Stunning Enemies
The other thing we need to do before making any attacks is set up the enemy's hit animation, so that they will react when we attack them. For this we will use the EnemyHit
animation, which we imported earlier, and the IsHit
variable.
First, we’ll go into the step event and modify the code so that the hit animation gets used.
- Open OBJ_Enemy.
- Select the Step event, and open the code action.
- Add the following code to the beginning of the event:
if(IsHit==true){ sprite_index = SPR_EnemyHit; }else{ sprite_index = SPR_EnemyIdle; }
Just to make sure this code works, go into the Enemy's create event and change their IsHit
variable to true
. Then go in-game, and take a look at the Enemy's animation. You should see something like this:
While the Hit animation is definitely working, you'll probably notice that it never ends. The reason for this is because we don't have any code that resets the Enemy's IsHit state after a certain amount of time. So once we set it to true, it stays that way forever.
To make this work, we are going to set up an alarm which will reset the IsHit variable, and make the enemy Idle again when it goes off. Then, whenever the enemy gets hit, each attack will contain a statement that tells the Enemy how long they should be stunned before returning to idle, and will start the alarm with that amount of time.
- Go to the OBJ_Enemy and choose Add Event > Alarm > Alarm 0.
- Under the Control tab, add an Execute Code action.
- Add the following code:
IsHit = false;
All it needs to do is reset IsHit, and the Step event will take care of the rest.
You can’t really test this to make sure it works yet, but if you add the code below to the end of the Create event, you should briefly see what we just set up in action when the game first starts. Make sure you remove this code, though, and set IsHit back to false, before you move on.
alarm[0] = 15;
Creating the Attack Object
Now that we have all that prep work out of the way, we can start on the actual attack objects.
First, we’ll need to make a parent Attack Object to contain all of the attack’s stats, such as how much damage it does, what the hitbox looks like, what particle effects should be used, etc. Making a unique object for each attack is better practice than forcing the Player or Enemy objects to store all the relevant information, and having a parent object makes doing that much easier. It also makes the attack code more versatile, and allows us to program a single attack which can be used by multiple characters.
To make the first attack object, follow these steps:
- Right-click the Objects folder and choose Create/Insert Object.
- Name the new object ATK.
- Use Add Event > Create.
- Under the Control tab, add an Execute Code action.
- Add the following code:
depth = -1*y; Damage = 10; StunLength = 4; Owner = "Player"; DMGFrame = 3;
The very first line of code sets the Attack's depth the same way we set the depth for the Player. The rest of the code, though, establishes some important variables for the Attack. Let’s look at what these variables do. Damage
is how much damage the attack does. StunLength
is the number of frames that the enemy will be stunned for with the IsHit variable. Owner
is who made the attack. This can be either the Player or Enemy, and prevents enemies from hurting each other. DMGFrame
allows us to say when the attack will deal damage.
In Beat ’Em Ups, timing is important, and attacks should only deal damage if they hit at the right time. Take a look at this attack:
If you took damage before the enemy’s hand came down, you’d feel cheated, since it would look as if it shouldn’t have hurt yet. The DMGFrame
variable tells the attack object which frame should deal damage, so that collisions during every other frame are ignored. Things like this form the basis for all timing-based combat games, from Turtles in Time to Dark Souls, and make it possible to “read” your opponents' attacks and dodge before they hit.
Now let’s add the collision code for the attack so it deals damage when it hits.
- In the Atk object, go to Add Event > Collision > OBJ_Enemy.
- Under the Control tab, add an Execute Code action.
- Add the following code:
if(image_index == DMGFrame && abs(depth - other.depth) <= LayerSize && abs(y - other.y) <= LayerSize && Owner == "Player"){ other.CurrentHP -= Damage; other.IsHit = true; other.alarm[0] = StunLength; }
The code we have above fires whenever the attack collides with an enemy object, and uses a simple if statement to determine whether the attack hits. The if statement tests four things:
- Is the current frame equal to the damage frame?
- Is the difference between the depth of the attack and the depth of the enemy less than the LayerSize?
- Do the attack and the enemy have close Y values? In other words, are they both on the ground, or both close to each other in the air?
- Was the attack made by the Player? Since this event applies to collisions with enemies, we check to make sure the Player made the attack. You could remove this check if you want enemies to be able to hurt each other.
If all of these conditions are true, then the attack was successful, and the enemy takes damage and gets stunned.
We also need to do something similar for collisions with the Player.
- In the Atk object, go to Add Event > Collision > OBJ_Player.
- Under the Control tab, add an Execute Code action.
- Add the following code:
if(image_index == DMGFrame && abs(depth - other.depth) <= LayerSize && abs(y - other.y) <= LayerSize && Owner == "Enemy"){ other.CurrentHP -= Damage; other.IsHit = true; other.alarm[3] = StunLength; }
This is the same code as the Enemy collision, except that it checks if the attack was made by the Enemy, rather than the Player, and prevents the Player from taking damage from their own attacks.
Now we need to make a hitbox for the attack as well. Hitboxes are the portion of the sprite that is considered "collidable", and they are used to ensure that only certain portions of the sprite cause a collision and cause the character to get hurt. For example, if I go back to the Yellow character's attack, I would only want a small portion, outlined in red below, to be collidable.
If anything other than that arm caused the Player to take damage, they'd feel upset and the game would seem unfair. To solve this problem, I can use a hitbox that only registers that portion of the sprite and ignores everything else.
You can import the hitbox for our attack using the table below. Then assign the hitbox as the ATK object’s sprite once you're done.
Sprite Name | Images | Origin Position |
SPR_BasicPunch_Hitbox | BasicPunchHitbox1.png, BasicPunchHitbox2.png, BasicPunchHitbox3.png, BasicPunchHitbox4.png, BasicPunchHitbox5.png | X = 55, Y = 122 |
When the Player attacks, they will set their sprite accordingly, but using the hitbox sprite for the ATK's sprite ensures that only that portion of the attack animation will register the collision.
The last thing we have to do for the attack is make sure it is destroyed when the attack is over. If we don't destroy it, the attack will continue running infinitely and kill our enemy almost right away. To ensure this doesn't happen, we are going to use an Animation End event.
- Go to the ATK object and choose Add Event > Other > Animation End.
- Under the Control tab, add an Execute Code action.
- Add the following code:
instance_destory();
With this event, the attack object will immediately destroy itself once it’s finished running, and it won’t stick around to cause us more issues later.
Now you just have to uncheck the Visible checkbox on the ATK Object so that the hitbox can’t be seen.
That completes our base Attack object, but we still need to build our first actual attack by extending the base object. Using the ATK object as a base for other attacks makes it easy to quickly build a large variety of unique attacks.
- Make a new Object called ATK_BasicPunch.
- Set the Parent to the ATK object.
- Assign the Sprite SPR_BasicPunch_Hitbox.
- Use Add Event > Create.
- Under the Control tab, add an Execute Code action.
- Add the following code:
event_inherited();
The Basic punch is now technically complete since it extends the ATK object and inherits all of the properties laid out in the Create event. If you wanted your basic punch to be more powerful or stun for a longer period of time, though, you could change those properties by setting them again here.
For example, you could use:
event_inherited(); Damage = 15; StunLength = 2;
This would make the attack deal more damage, but stun for fewer frames.
You could also use:
event_inherited(); Damage = 8; StunLength = 7;
This code would deal less damage, but stun the enemy longer.
You can feel free to modify the base variables Damage
, StunLength
, and even DMGFrame
(make sure that this number is no more than 5, since the attack is only 5 frames long) however you want, and make many different variations on the same type of attack. No matter what you do, though, make sure that your new attack has the ATK object set as the Parent, and has the event_inherited()
line at the beginning of any event that the Parent class also utilizes.
Making the Player Attack
Now that our attack is finished, our player needs to be able to use it. For our combat system, we are going to utilize three possible control schemes to accommodate many different players.
We are going to let players use IJKL, the numpad's 4856 keys, and even the arrow keys, as possible layouts for the attack buttons. This gives players the ability to use any setup they prefer based on their keyboard size/layout. On top of that, since all three of those sets have the same basic layout, it’s easy to make them all work. J, 4, and Left Arrow will be light attacks, I, 8, and Up Arrow will be strong attacks, K, 5, and Down Arrow will be Grabs, and L, 6, and Right Arrow will be Specials.
- In the Player object, choose Add Event > Key Press> Any Key.
- Under the Control tab, add an Execute Code action.
- Add the following code:
AttackType = ""; if(keyboard_check(vk_numpad4) || keyboard_check(ord('J')) || keyboard_check(vk_left)){ AttackType = "Basic Punch"; } if(OnGround == true){ event_user(2); }
First, this code checks if the Player is pressing any of the valid Light Attack buttons we discussed before, the Left Arrow, the 4 key on the numpad, or the J key. If so, it sets the Player's AttackType to a Basic Punch. After determining the AttackType, it checks to make sure the user is on the ground, and calls the Attack event if they are.
The Attack event will create the attack object based on the attack the player used. It may seem strange that we’re making this a separate event, since our code is so simple, but as we add more attacks, and eventually implement combos, it’ll make our code much easier to manage if these elements are separated.
- In the Player object, choose Add Event > Other > User Defined > User 2.
- Under the Control tab, add an Execute Code action.
- Add the following code:
var MyAttack = 0; if(IsHit == false && CurrentHP > 0){ if(AttackType == "Basic Punch"){ sprite_index = SPR_PlayerBasicPunch; MyAttack = instance_create(x,y,ATK_BasicPunch); } } if(MyAttack != 0){ SpeedMod = 0; IsAttacking = true; MyAttack.image_xscale = image_xscale; MyAttack.image_speed = image_speed; MyAttack.Owner = "Player"; }
This code is actually pretty simple, even though it looks complex at first.
First, we make a temporary object to store the attack itself. Then, as long as the player hasn’t just been hit, and they’re not Dead, we determine which attack is being used, set the animation, and make an attack object of that type. Finally, if an attack object was created, it sets the player to be attacking, sets the direction and speed of the attack to match the player’s, and sets the Owner value of the attack appropriately.
At this point, we can go into the game and test this attack. If you walk up to the Enemy and use one of the light attack buttons, you should see the player attack and the Enemy get hit.
You may notice an issue, though. No matter how long you wait, the Player’s animation never switches off the attack animation, even though the Enemy is no longer taking damage. You'll also notice that you can no longer move.
This is the same issue we dealt with earlier with the Enemy and their hit animation. Just like with the Enemy, we need to set up a system that resets the Player's IsAttacking
variable when the attack completes. We’re going to do this with another Animation End event.
- Go to OBJ_Player and choose Create Event > Other > Animation End.
- Under the Control tab, add an Execute Code action.
- Add the following code:
if(IsAttacking == true){ IsAttacking = false; SpeedMod = 1; }
Now when you go in-game, the attack should work exactly as you expected, and the animation should end when it's supposed to.
Adding Sound
Our attack works well in-game, but it doesn’t do a good job of drawing the player in. Part of the problem is that we don’t have any sound effects to give the player feedback when they hit or miss with an attack. Let’s resolve this issue before we call the Attack object complete.
We’ll need to import two sounds for the attack, a Hit sound and a Miss sound. Follow these steps to import the first sound.
- Right-click the Sounds folder, and choose Create Sound.
- Set the Name to SND_BasicPunch1.
- Import the sound file LightPunch1.wav from the project’s assets.
- Set the Sample Rate to 48000.
- Set the Bit Rate to 320.
- Press Ok to save your sound.
Great work! You’ve now imported your first sound effect.
Now we’ll do the same thing for the Miss sound.
- Right-click the Sounds folder, and choose Create Sound.
- Set the Name to SND_MissedPunch.
- Import the sound file Miss.wav from the project’s assets.
- Set the Sample Rate to 48000.
- Set the Bit Rate to 320.
- Press Ok to save your sound.
Now that we’ve imported our sounds, we need to add some new variables to our attack object so it can use the sounds correctly.
- Go into OBJ_Attack and open the Create Event.
- Add the following code to the end of the Execute Code action:
HitSound = SND_BasicPunch1; MissSound = SND_PunchMiss; Hit = false;
These three variables are pretty simple. The HitSound
and MissSound
variables allow us customize the hit and miss sounds for each attack in their create event, the same way we can customize the other properties of each attack we create. The Hit
variable allows us to check whether the attack was successful before playing the Miss sound.
With the variables in place, we need to implement them. First we’ll implement the Hit sound.
- With OBJ_Attack, open Collision with OBJ_Enemy.
- Add the following code to the end of the if statement that confirms the collision is valid:
audio_play_sound(HitSound,10,false); Hit = true;
All this code does is play the HitSound and set the Hit variable to true when the attack collides with an Enemy.
We can use the same code for the Player collision event as well.
- With OBJ_Attack, open Collision with OBJ_Player.
- Add the following code to the end of the if statement that confirms the collision is valid:
audio_play_sound(HitSound,10,false); Hit = true;
Finally, we need to play the Miss sound if the attack does not hit anything. Unlike the Hit sound, which plays at the moment of collision, the Miss sound will only play if the Attack object is destroyed without hitting anything, so this sound will need to be played in a Destroy event.
- With OBJ_Attack, choose Add Event > Destroy.
- Under the Control tab, add an Execute Code action.
- Add the following code:
if(Hit == false){ audio_play_sound(MissSound, 10, false); }
As you can see, all that this code does is play MissSound
if Hit is equal to false.
You can now go in-game and test your attack one last time, and the sound effects should work perfectly.
Making a Strong Attack
Now that our Basic attack is done, let’s make one more attack type, a Strong Attack. For this attack, use the table below to set up the hitbox:
Sprite Name | Images | Origin Position |
SPR_StrongPunch_Hitbox | StrongPunchHitbox1.png, StrongPunchHitbox2.png, StrongPunchHitbox3.png, StrongPunchHitbox4.png, StrongPunchHitbox5.png | X = 60, Y = 124 |
We should also import a unique HitSound for this attack since it's supposed to be more powerful than a standard attack.
- Right-click the Sounds folder, and choose Create Sound.
- Set the Name to SND_StrongPunch1.
- Import the sound file HeavyPunch1.wav from the project’s assets.
- Set the Sample Rate to 48000.
- Set the Bit Rate to 320.
- Press Ok to save your sound.
Now that we have all of the assets ready, let's make the actual attack object.
- Make a new Object called ATK_StrongPunch.
- Set the Parent to the ATK object.
- Uncheck the Visible checkbox.
- Assign the Sprite SPR_StrongPunch_Hitbox.
- Use Add Event > Create.
- Under the Control tab, add an Execute Code action.
- Add the following code:
event_inherited(); Damage = 20; StunLength = 10; HitSound = SND_StrongPunch1;
Now go back to the Player object, and add this code into the Keyboard Press > Any Key event after the Light Attack if so that it can also detect when the player uses the Strong Attack:
if(keyboard_check(vk_numpad8) || keyboard_check(ord('I’)) || keyboard_check(vk_up)){ AttackType = “Strong Punch”; }
Finally, go into User Defined 2, and add this code at the end of the if statement that makes the Light Attack object, so it can also make the Strong Attack. Make sure that it is contained within the if that checks if the Player is Dead:
else if(AttackType == "Strong Punch”){ sprite_index = SPR_PlayerStrongPunch; MyAttack = instance_create(x,y,ATK_StrongPunch); }
Your code should now look like this:
Now if you go into the game, you should be able to use a Light Attack and a Strong Attack on an enemy. As you can see, while the two attacks are essentially very similar, they are differentiated by the animation associated with them, their hitboxes, and their basic properties.
Conclusion
You should now be able to freely edit and remix these attacks as you see fit. In the next article we will look at giving the Enemies the ability to fight back, and create more advanced features for the camera.