Introducción al Desarrollo de Videojuegos con GameMaker · Module 7: Enemies, Basic AI, and Gameplay Mechanics
Implementing Lives and a Health Bar
Lesson 33 of 60 · 55 min
Also available in: ES
Introduction to Health and Lives in 2D Platformers
In classic 2D platformer development, player survival is almost always managed through two layers of persistence: short-term durability (health or hit points) and overall game persistence (lives or total attempts). While health dictates how many minor hits the avatar can withstand before momentarily falling, lives determine how many attempts the player gets to clear the stage before confronting a definitive Game Over screen.
In the previous lesson, we established collision detection between the player and patrolling enemies. However, if we simply restarted the room upon every hit or deducted health every single frame, the game would feel completely unfair or unplayable: because GameMaker runs its Step event 60 times per second by default, continuous contact for just half a second would inflict up to 30 damage ticks instantaneously. For this reason, a robust health system requires not only numeric deduction variables, but also a temporary invulnerability window (industry-standard 'invincibility frames' or 'i-frames') and a disciplined relocation or respawn routine.
Essential Variables for Health, Lives, and Position Tracking
To build this system without scattering global variables that make code maintenance difficult, we will keep state inside our player instance (`obj_jugador`), also storing the spatial coordinates where the character should rematerialize if defeated. Below are the core parameters that govern our survival logic:
| Variable Name | Data Type | Recommended Initial Value | Mechanic Purpose |
|---|---|---|---|
| salud_maxima | Real (Numeric) | 100 | Absolute upper limit for the health bar. |
| salud_actual | Real (Numeric) | 100 | Current amount of vitality active on each frame. |
| vidas | Integer (Numeric) | 3 | Total number of global attempts before Game Over. |
| es_invulnerable | Boolean | false | State flag that blocks incoming damage. |
| tiempo_invulnerable | Integer (Numeric) | 0 | Countdown timer in steps before immunity expires. |
| duracion_invulnerable | Integer (Numeric) | 60 | Total protection steps after a hit (1 second at 60 fps). |
| origen_x | Real (Numeric) | Initial x | Safe horizontal respawn coordinate (starting point). |
| origen_y | Real (Numeric) | Initial y | Safe vertical respawn coordinate (starting point). |
It is critical to distinguish between `salud_maxima` and `salud_actual`. Hardcoding fixed values to refill health upon respawning is considered poor practice. If you later allow the player to purchase vitality upgrades or collect heart containers, modifying `salud_maxima` will automatically adapt all percentage calculations and healing routines without side effects.
Setting Up the Create Event in obj_jugador
Open your main player object, `obj_jugador`. In previous lessons, we declared movement variables like `velocidad_x`, `velocidad_y`, `gravedad`, and `fuerza_salto`. Now we will integrate the physical state and respawn variables at the bottom of the Create event to ensure the character spawns with all attributes ready from the very first frame the room loads.
/// @description Health, lives, and starting point initialization
// Physical resistance parameters
salud_maxima = 100;
salud_actual = salud_maxima;
vidas = 3;
// Temporary immunity control against continuous hits
es_invulnerable = false;
tiempo_invulnerable = 0;
duracion_invulnerable = 60; // Equivalent to 1 second if the room runs at 60 FPS
// Position tracking for respawn
origen_x = x;
origen_y = y;By assigning `x` and `y` to `origen_x` and `origen_y`, GameMaker captures the exact coordinates where you placed the instance inside the Room Editor. This avoids arbitrary hardcoded coordinates, allowing you to position the player anywhere in the level via the visual editor without modifying a single line of code.
The Damage and Temporary Invulnerability Loop
A classic beginner mistake when programming enemy collisions is deducting damage directly inside the collision event without checking if the player is already in a hurt state. If you do this and the player bumps into an enemy dealing 25 damage, their health drops from 100 to 0 in just four frames (less than a tenth of a second), making it feel as if the enemy killed the player in a single hit.
To solve this, we use an immunity window. When an enemy hits the player, a structured sequence must execute: first, check that `es_invulnerable` is false; if it is, deduct the damage, toggle `es_invulnerable = true`, and initialize `tiempo_invulnerable = duracion_invulnerable`. For every frame while the timer remains above zero, any other damaging collision is completely ignored.

Step Event Update and Visual Feedback
In `obj_jugador`'s Step event, we decrement the invulnerability timer frame by frame until it reaches zero. Beyond pure numeric logic, providing immediate visual feedback is essential: players need to clearly see that their character is hurt and temporarily protected. In classic 8-bit and 16-bit retro games, this is achieved by toggling sprite transparency (the alpha channel) to create a rhythmic flashing effect.
Add the following code block inside the Step event of `obj_jugador`, placing it alongside the movement and gravity code created in earlier modules:
/// @description Invulnerability timer and flicker management
if (es_invulnerable)
{
tiempo_invulnerable -= 1;
// Visual flashing effect by modifying image_alpha every few frames
if ((tiempo_invulnerable mod 8) < 4)
{
image_alpha = 0.3; // Semitransparent
}
else
{
image_alpha = 1.0; // Fully opaque
}
// Restore normal state once time runs out
if (tiempo_invulnerable <= 0)
{
es_invulnerable = false;
image_alpha = 1.0; // Ensure full final opacity
}
}Let's break down `(tiempo_invulnerable mod 8) < 4`. The `mod` (modulo) operator returns the remainder of integer division between the counter and 8. This generates a repeating cycle of integers from 0 to 7. As long as the remainder stays below 4, `image_alpha` drops to 0.3 (making the sprite nearly translucent); when it reaches 4 through 7, it returns to full opacity (1.0). The resulting effect is a clean, steady flicker without visual artifacts.
Enemy Collision and Damage Application
In the previous lesson, you learned how to detect collisions with `obj_enemigo_patrullero`. Now we will refine that detection so the enemy deals specific damage and applies a slight knockback impulse to separate both bodies and prevent them from getting stuck inside each other's collision masks.
Inside the Step event of `obj_jugador`, we will use the built-in function `instance_place(x, y, obj_enemigo_patrullero)`. This function returns the unique instance ID (`id`) of the specific enemy instance currently colliding with the player. If there is no collision, it returns the special keyword `noone`.
/// @description Contact detection with enemies and damage reception
var _enemigo = instance_place(x, y, obj_enemigo_patrullero);
if (_enemigo != noone and !es_invulnerable)
{
// Get damage from enemy or apply a default value if undefined
var _dano_recibido = 25;
if (variable_instance_exists(_enemigo, "dano"))
{
_dano_recibido = _enemigo.dano;
}
// Apply damage and activate protection window
salud_actual -= _dano_recibido;
salud_actual = clamp(salud_actual, 0, salud_maxima);
es_invulnerable = true;
tiempo_invulnerable = duracion_invulnerable;
// Slight knockback impulse
velocidad_y = -4; // Involuntary upward jump
if (x < _enemigo.x)
{
velocidad_x = -3; // Knockback to the left
}
else
{
velocidad_x = 3; // Knockback to the right
}
}💡 The `clamp(val, min, max)` function is a staple in game programming: it constrains a numeric value so it never falls below a minimum nor exceeds an upper bound. Using `salud_actual = clamp(salud_actual, 0, salud_maxima);` guarantees that health never drops below zero or overflows past 100% when collecting healing pick-ups.
Life Deduction and Respawn Mechanics
When `salud_actual` reaches zero, the character's life cycle is over. Rather than destroying the instance via `instance_destroy()`—which would break camera references or halt scripts tied to that object—we perform a controlled respawn sequence:
1. Deduct one life from the global counter (`vidas -= 1`). 2. Instantly teleport the character back to the coordinates stored in `origen_x` and `origen_y`. 3. Cancel all accumulated momentum (`velocidad_x = 0`, `velocidad_y = 0`) to prevent the avatar from sliding or falling uncontrollably upon reappearing. 4. Reset health back to maximum (`salud_actual = salud_maxima`). 5. Grant a brief courtesy window of invulnerability so the player does not take instant damage if respawning near a hazard. 6. Check whether lives have reached zero. If so, trigger the defeat state or restart the room.
Add the following check inside the Step event of `obj_jugador`, placed immediately after the damage calculation block:
/// @description Death check, life deduction, and respawn
if (salud_actual <= 0)
{
vidas -= 1;
if (vidas > 0)
{
// Respawn at checkpoint or origin
x = origen_x;
y = origen_y;
// Completely zero out any movement momentum
velocidad_x = 0;
velocidad_y = 0;
// Restore full health for the new attempt
salud_actual = salud_maxima;
// Grant brief temporary immunity after respawn
es_invulnerable = true;
tiempo_invulnerable = duracion_invulnerable;
}
else
{
// All lives have been depleted
// For now restart room; in Module 8 we will implement the Game Over screen
room_restart();
}
}Visual Representation: Drawing the Health Bar
While the upcoming module (Module 8) explores the Draw GUI event in detail for building full user interfaces, having immediate visual feedback on the gameplay canvas right now is crucial. There are two primary early approaches: a floating health bar following the character in world space via the standard Draw event, or a fixed on-screen HUD bar using basic geometric primitives.
We will implement an overhead floating health bar drawn just above the player's head. To do this, add the Draw event to `obj_jugador`. Keep in mind a fundamental GameMaker rule: **once you write code inside an object's Draw event, GameMaker stops drawing its default sprite automatically**. Consequently, the very first line inside any Draw event should almost always be `draw_self()`, which renders the standard animated sprite before any decorative graphics are layered on top.
/// @description obj_jugador Draw event: sprite rendering and floating bar
// 1. Draw character sprite respecting animation and alpha
draw_self();
// 2. Geometric parameters for the floating bar
var _ancho_barra = 32;
var _alto_barra = 4;
var _desplazamiento_y = 20; // Pixels above the sprite origin
// Coordinates calculated around the player position
var _x1 = x - (_ancho_barra / 2);
var _y1 = y - sprite_yoffset - _desplazamiento_y;
var _x2 = x + (_ancho_barra / 2);
var _y2 = _y1 + _alto_barra;
// 3. Draw dark bar background (missing health / container)
draw_set_color(c_black);
draw_rectangle(_x1 - 1, _y1 - 1, _x2 + 1, _y2 + 1, false);
// 4. Calculate width of the portion proportional to remaining health
var _porcentaje = salud_actual / salud_maxima;
var _x_vida = _x1 + (_ancho_barra * _porcentaje);
// 5. Determine dynamic color: green if healthy, yellow if wounded, red if critical
var _color_salud = c_lime;
if (_porcentaje <= 0.25)
{
_color_salud = c_red;
}
else if (_porcentaje <= 0.5)
{
_color_salud = c_yellow;
}
// 6. Draw current health fill
if (salud_actual > 0)
{
draw_set_color(_color_salud);
draw_rectangle(_x1, _y1, _x_vida, _y2, false);
}
// 7. Reset base draw color to avoid affecting other graphic elements
draw_set_color(c_white);Notice the geometry calculations. By subtracting `sprite_yoffset`, the bar always sits exactly 20 pixels above the physical top edge of the sprite, regardless of where its origin (anchor point) is set (center, bottom-center, or top-left). Furthermore, the dynamic color thresholds (`c_lime`, `c_yellow`, `c_red`) provide intuitive warnings as player health approaches critical levels.
Guided Practical Example: Testing Hits and Respawning
To verify that all components work together seamlessly, follow this structured testing procedure in the IDE:
1. Open your main level room (`Room1` or `rm_nivel_1`). 2. Place an instance of `obj_jugador` on a safe platform on the left side of the room. 3. Place an instance of `obj_enemigo_patrullero` on the same platform, giving it room to patrol toward the right. 4. Run the game by pressing F5 (or the green Play button on the top toolbar). 5. Walk intentionally into the enemy to take the first hit. Notice how the player gets knocked up and to the left, the sprite blinks with semitransparency, and the overhead bar drops by roughly one third while switching color. 6. Try colliding with the enemy again while the flicker is still active. Verify that touching the enemy deals no additional damage during this grace period. 7. Let the flashing end, then take two more hits. Once health reaches zero, the instance immediately leaves the spot and reappears at the starting point where the room began, fully replenished to 100% health and with one fewer life recorded.
Practical Exercises
Here are three progressive exercises to consolidate what you have learned, practice writing clean GML Code, and solve common gameplay implementation challenges:
<strong>Exercise 1: Instant Death Pit (Fall Zone)</strong> In most platformers, falling into a bottomless pit shouldn't leave the player floating forever or dealing fractional damage—it should instantly deduct a life and trigger a respawn. Write a condition inside `obj_jugador`'s Step event checking if `y` exceeds the room's bottom boundary (`room_height + 32`). If true, set `salud_actual` directly to 0 to trigger the standard respawn sequence. <em>Pass criteria:</em> Walking the character into a pit drops a life and respawns them at `origen_x` and `origen_y` without runtime errors.
<strong>Exercise 2: Overhead Lives Counter</strong> Update the Draw event of `obj_jugador` so that, directly to the right of the rectangular health bar, it draws text displaying the player's remaining lives using `draw_text(x, y, string)`. Remember to call `draw_set_color(c_white)` before rendering text to ensure readability. <em>Pass criteria:</em> Clear text such as 'x 3' or 'Lives: 3' appears alongside the bar and decrements to 'x 2' and 'x 1' whenever the player collapses.
<strong>Exercise 3: Manual Checkpoints</strong> Add a quick debugging key check in the Step event (such as the Spacebar or 'C' key) using `keyboard_check_pressed(ord("C"))` that updates `origen_x` and `origen_y` to the player's current position (`origen_x = x; origen_y = y;`). <em>Pass criteria:</em> After navigating to a distant platform and pressing 'C', dying to an enemy or falling into a pit respawns the character at the new saved location instead of the level's beginning.
Summary
- A solid life system separates short-term resilience (health/HP) from long-term attempt tracking (lives).
- Temporary invulnerability frames (i-frames) prevent continuous overlapping collisions from draining health every single frame.
- Clean sprite blinking is achieved mathematically using the modulo operator (mod) applied to the image_alpha property.
- The clamp() function ensures health values remain strictly between 0 and salud_maxima without underflowing or overflowing.
- Respawning should not destroy the player instance; it resets coordinates (x, y) to origin, clears velocity, and restores health.
- When implementing custom graphics in the Draw event, always call draw_self() first so the main character sprite remains visible.
Practise what you learned
Exercise 1 · 1 point
Why can continuous contact with an enemy lasting just half a second deal up to 30 damage instances if a temporary invulnerability window is not programmed?
Exercise 2 · 1 point
To ensure the character's hit points never drop below zero nor exceed the maximum cap when healing, the following line of code is used: salud_actual = (salud_actual, 0, );
Exercise 3 · 1 point
Match each code element discussed in the lesson with its corresponding function within the health and respawn system:
- image_alpha
- origen_x and origen_y
- instance_place
- duracion_invulnerable
Sign in to save your progress — Sign in