Taking damage should feel dangerous, but it should also feel fair. When a player touches an enemy, they expect to lose health, receive clear feedback, and get a brief chance to escape.
Without that protection, one collision can drain an entire health bar in a fraction of a second. That is why many action games use invincibility frames, often shortened to i-frames.
After the player is hit, the game temporarily ignores additional damage. The character may flash, become transparent, move backward, or play a sound to show that the hit was registered.
Learning how to build a damage and invincibility system in GameMaker gives you a reusable foundation for platformers, action RPGs, shooters, and top-down adventures. The same structure can work for players, enemies, bosses, and destructible objects.
In this tutorial, you will create health variables, a reusable damage function, collision-based attacks, temporary protection, knockback, and visual feedback using beginner-friendly GML.
Set Up the Player’s Health Variables
Start by opening your player object and adding the following code to its Create Event:
max_hp = 100;
hp = max_hp;
is_dead = false;
is_invincible = false;
invincibility_steps = 45;
The Create Event is designed for setup code that should run when an instance is first created. It is commonly used to initialise variables, alarms, and other starting properties.
The max_hp variable stores the player’s maximum health, while hp holds the current amount. Keeping these values separate allows you to add health upgrades, healing items, and percentage-based health bars later.
The Boolean variable is_invincible determines whether the player can currently receive another hit. invincibility_steps controls how long that protection lasts.
GameMaker alarms count down in game steps. For example, if your game runs at 60 steps per second, a value of 30 lasts approximately half a second.
Create a Reusable Damage Function
Avoid subtracting health directly inside every enemy, projectile, and hazard. Instead, create one function that controls the entire damage process.
Add this function beneath the variables in the player’s Create Event:
take_damage = function(_amount)
{
if (is_dead || is_invincible)
{
return;
}
hp = clamp(hp - _amount, 0, max_hp);
is_invincible = true;
alarm[0] = invincibility_steps;
if (hp <= 0)
{
is_dead = true;
}
};
The first condition prevents damage when the player is already defeated or temporarily protected. The return statement immediately stops the function.
Next, the damage amount is subtracted from hp. The clamp() function keeps the result between zero and max_hp, preventing negative health or values above the intended maximum.
Finally, the function activates invincibility and starts Alarm 0. When health reaches zero, it changes the player’s state to dead.
You can now damage the player with one simple call:
take_damage(20);
Centralising the rules makes future changes easier. Armour calculations, difficulty modifiers, sound effects, and screen shake can all be added to this one function.
Apply Contact Damage Through Collisions
Create an enemy object named obj_enemy. In its Create Event, give it a damage value:
contact_damage = 15;
Now add a Collision Event with obj_enemy to the player object:
take_damage(other.contact_damage);
Inside a Collision Event, other refers to the specific instance involved in the collision. GameMaker handles each collision as an interaction between the current instance and one other instance.
This means different enemies can deal different damage without requiring separate player code:
// Weak enemy
contact_damage = 10;
// Strong enemy
contact_damage = 30;
The first collision activates is_invincible. Any additional collision checks that call take_damage() during the protection period will immediately return without changing health.
Collision masks still matter. If the player or enemy has an inaccurate mask, damage may occur too early or fail to register. Make sure the mask represents the object’s physical body rather than every decorative pixel.
End Invincibility with an Alarm
When take_damage() runs, it sets Alarm 0:
alarm[0] = invincibility_steps;
Add an Alarm 0 Event to the player and enter:
is_invincible = false;
image_alpha = 1;
image_blend = c_white;
Every object instance has 12 built-in alarms. An alarm counts down in steps and runs its associated Alarm Event when it reaches zero.
Resetting image_alpha and image_blend ensures that any temporary damage effect disappears when protection ends.
Alarms provide a simple solution for short gameplay timers. However, they are based on game steps rather than real elapsed time. A more advanced frame-independent system can subtract delta_time, which reports the time between frames in microseconds.
For a beginner project using a stable game speed, an alarm is usually easier to understand and maintain.
Add Flashing Visual Feedback
Players need to know when damage has occurred and when temporary protection is active. One simple approach is to make the sprite flash by changing its transparency.
Add this code to the player’s Step Event:
if (is_invincible)
{
if ((alarm[0] div 4) mod 2 == 0)
{
image_alpha = 0.35;
}
else
{
image_alpha = 1;
}
}
else
{
image_alpha = 1;
}
The code alternates the sprite between partially transparent and fully visible while the alarm counts down.
GameMaker’s image_alpha variable accepts values from zero to one. Zero is completely transparent, while one is fully opaque.
You can also tint the character briefly:
image_blend = c_red;
The image_blend variable applies a colour tint to an instance’s assigned sprite. Its default appearance is equivalent to white, which leaves the original colours unchanged.
These effects are visible when GameMaker draws the sprite automatically or when custom drawing uses compatible functions such as draw_self() or draw_sprite_ext().
Add Knockback to Make Hits Feel Stronger
Damage feels more convincing when it affects movement. A small knockback pushes the player away from the attacker and creates space during the invincibility period.
Update the damage function so it can receive the attacker’s position:
take_damage = function(_amount, _source_x, _source_y)
{
if (is_dead || is_invincible)
{
return;
}
hp = clamp(hp - _amount, 0, max_hp);
var _direction = point_direction(
_source_x,
_source_y,
x,
y
);
knockback_x = lengthdir_x(6, _direction);
knockback_y = lengthdir_y(6, _direction);
is_invincible = true;
alarm[0] = invincibility_steps;
if (hp <= 0)
{
is_dead = true;
}
};
Initialise the knockback variables in the Create Event:
knockback_x = 0;
knockback_y = 0;
Then call the function from the Collision Event:
take_damage(
other.contact_damage,
other.x,
other.y
);
In the Step Event, apply the stored force and gradually reduce it:
x += knockback_x;
y += knockback_y;
knockback_x *= 0.8;
knockback_y *= 0.8;
This is a simple arcade-style solution. Games with wall collisions should pass the knockback movement through the same collision system used for normal player movement.
Handle Projectiles and Area Attacks
Contact damage is only one possibility. A projectile can call the same player function before destroying itself.
Inside the projectile’s Collision Event with the player:
other.take_damage(damage, x, y);
instance_destroy();
Because the event runs from the projectile, other refers to the player. The projectile sends its damage value and current position to the player’s function.
A trap or explosion can follow the same pattern:
with (obj_player)
{
take_damage(25, other.x, other.y);
}
Using a shared function means every damage source follows the same rules. The player receives the same protection period, health limits, feedback, and death check regardless of whether the hit came from an enemy, bullet, spike, or explosion.
You can later add a _damage_type argument for fire, poison, physical attacks, or environmental hazards.
Connect Damage to the Health Interface
The player should be able to see the result of each hit. Add a Draw GUI Event and calculate the remaining health percentage:
var _percentage = (hp / max_hp) * 100;
draw_healthbar(
32,
32,
232,
52,
_percentage,
c_black,
c_red,
c_lime,
0,
true,
true
);
GameMaker’s draw_healthbar() function expects a percentage between zero and 100. A percentage can be calculated by dividing the current value by the maximum and multiplying by 100.
You can also display the numbers:
draw_text(
32,
60,
string(hp) + " / " + string(max_hp)
);
Damage feedback should be immediate. The health bar should change on the same hit that triggers flashing, knockback, sound, or particles.
Test Important Damage Scenarios
Begin by testing one enemy with a low damage value. Confirm that the first contact reduces health and that continuous overlap does not repeatedly drain the bar.
Next, place several enemies close together. The player should still receive only one hit during each invincibility period.
Check what happens when the player has less health than the incoming damage. The clamp() function should keep the result at zero, while the death state should activate correctly.
You should also test knockback near walls, healing during invincibility, projectiles hitting simultaneously, and transitions between rooms.
Avoid making the protection period too long or too short. A very short duration may feel unfair, while excessive invincibility can remove tension from combat. Adjust the timer while playing rather than choosing the final value from code alone.
A solid GameMaker damage system needs more than subtracting points from a health variable. It should reject repeated hits, limit health values, provide visual feedback, apply optional knockback, and enter a clear death state when health reaches zero.
A reusable take_damage() function keeps those rules in one place. Collision Events and projectiles can call it with different damage values, while Alarm 0 controls the temporary invincibility period. Flashing, tinting, sound, and movement feedback then make every hit easier to understand.
Build the basic version in a small test room before adding armour, elemental damage, shields, or status effects. Once every hit behaves consistently, you will have a dependable combat foundation for a much larger game.
