Introducción al Desarrollo de Videojuegos con GameMaker · Module 7: Enemies, Basic AI, and Gameplay Mechanics

Player-Enemy Collision System

Lesson 32 of 60 · 60 min

Also available in: ES

Introduction: The Continuous Contact Dilemma in 2D Video Games

In the previous lesson, we brought our first enemy to life with a basic patrol routine. Right now, however, the enemy and player act like ghosts passing through each other on screen without interacting—or worse, if solid collisions are enabled without damage logic, the enemy pushes the player around like a moving wall. The core design principle of a traditional platformer is that unwanted contact with a hazard penalizes the player by reducing their health or costing a life.

For a beginner, the initial instinct is usually: "When the player touches the enemy, subtract 1 hit point." However, GameMaker runs its main loop at 60 frames per second (60 FPS), meaning collision functions return true on every single frame where hitboxes overlap. If the player stays in contact with an enemy for just half a second (30 frames), the system rapidly depletes 30 hit points. This issue is known as uncontrolled continuous contact damage.

To solve this problem and deliver a fair, polished gameplay experience, classic video games rely on three coordinated mechanics: discrete collision detection, applying a knockback impulse that physically separates the characters, and a grace period of temporary immunity (commonly known as "i-frames" or invulnerability frames). During this grace window, the player cannot take further damage and receives clear visual feedback (usually an intermittent blinking effect) signaling their invulnerable state.

Detection Methods in GameMaker: Collision Event vs. Step Code

GameMaker provides two primary ways to handle collisions between objects. The first is the native Collision Event, accessible from the object's event panel. The second is manual verification using collision functions inside the Step event with GML Code. While both approaches work, professional projects and mechanics requiring precise mathematical control (such as knockback vectors and hurt states) benefit from centralizing logic inside the Step event.

Function / MechanismReturn ValueRecommended Use
Collision Event (Native)Automatically runs code upon collisionSimple mechanics like picking up coins or triggering overlap zones.
place_meeting(x, y, obj)Boolean (true / false)Checking for a collision before moving (ideal for solid walls and floors).
instance_place(x, y, obj)Instance ID of the colliding object (or noone)Crucial for combat. Lets you read the exact X and Y position of the specific enemy you hit.

For our damage logic, we will use instance_place(). Unlike place_meeting(), which only tells us whether an overlap exists using a simple yes-or-no check, instance_place() returns the unique instance identifier (id) of the specific enemy touching the player. This information is vital because we need to know where that enemy is located relative to the player to calculate which direction the player should be knocked back after the hit.

Technical diagram illustrating collision overlap detection, the knockback vector separating player and enemy, and an i-frames timeline with blinking.
Hit resolution mechanics: mask overlap, directional knockback impulse, and temporary invincibility window activation. · AI-generated image

Player Variable Setup (obj_player)

Before our character can handle damage, knockback, and invulnerability, we must initialize the appropriate variables in its Create event. Building upon the movement base established in earlier lessons (where variables like hspd, vspd, and walk_speed are already defined), we will add our health and damage management variables.

// --- obj_player Create Event ---

// Player health
hp_max = 3;
hp = hp_max;

// Temporary invincibility system (I-Frames)
is_invincible = false;           // Flag indicating whether player is invulnerable
invincible_timer = 0;            // Countdown timer in frames
invincible_duration = 90;        // Invincibility duration (90 frames = 1.5 seconds at 60 FPS)

// Hit knockback
knockback_speed_x = 4;           // Horizontal hit force
knockback_speed_y = -3;          // Small vertical pop upon taking damage
is_knockbacked = false;          // State flag for temporary loss of control
knockback_timer = 0;             // Duration of control loss
knockback_duration = 12;         // 12 frames (0.2 seconds) of hitstun

Let's break down the role of each variable. The invincible_duration variable sets how many frames the player stays protected. In a project running at 60 FPS, 90 frames equals exactly one and a half seconds (90 / 60 = 1.5 s). During this window, any further contact with enemies is completely ignored.

Meanwhile, is_knockbacked and knockback_timer temporarily disable the player's directional input. If player control were left enabled during knockback, pressing an opposite directional key would instantly cancel out the impact velocity, making hits feel floaty, rigid, and devoid of physical weight.

Detection and Response Logic in the Step Event

The Step event of obj_player needs a clean, orderly structure. First, update active timers; second, verify whether the player currently has movement control; and finally, if not currently invincible, check for enemy contact to trigger the hit sequence.

// --- obj_player Step Event snippet ---

// 1. UPDATE INVINCIBILITY AND KNOCKBACK TIMERS
if (is_invincible)
{
    invincible_timer -= 1;
    if (invincible_timer <= 0)
    {
        is_invincible = false;
    }
}

if (is_knockbacked)
{
    knockback_timer -= 1;
    if (knockback_timer <= 0)
    {
        is_knockbacked = false;
    }
}

// 2. PLAYER INPUT (Only allowed when not in hitstun)
if (!is_knockbacked)
{
    // Standard input reading
    var _key_left  = keyboard_check(vk_left)  || keyboard_check(ord("A"));
    var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
    var _move_dir  = _key_right - _key_left;
    
    hspd = _move_dir * walk_speed;
    
    // Standard jump logic
    var _key_jump = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));
    if (_key_jump && is_grounded)
    {
        vspd = jump_speed;
    }
}

// 3. APPLY GRAVITY AND STANDARD PHYSICS
vspd += grv;

// 4. DETECT COLLISION WITH ENEMY
if (!is_invincible)
{
    // Check for collision with the patrolling enemy object
    var _enemy_hit = instance_place(x, y, obj_enemy_patrol);
    
    if (_enemy_hit != noone)
    {
        // Deduct health
        hp -= 1;
        
        // Trigger temporary invincibility
        is_invincible = true;
        invincible_timer = invincible_duration;
        
        // Trigger knockback hitstun
        is_knockbacked = true;
        knockback_timer = knockback_duration;
        
        // Determine horizontal push direction
        // If player is to the right of enemy, push right (+1)
        // If to the left, push left (-1)
        var _push_direction = sign(x - _enemy_hit.x);
        if (_push_direction == 0) _push_direction = 1; // Safeguard if X positions match exactly
        
        hspd = _push_direction * knockback_speed_x;
        vspd = knockback_speed_y; // Small upward pop
    }
}

Let's examine the directional calculation: var _push_direction = sign(x - _enemy_hit.x);. The sign() function takes any real number and returns 1 if positive, -1 if negative, and 0 if zero. If the player's X position is greater than the enemy's X position (_enemy_hit.x), the subtraction produces a positive value, indicating the player is to the right; sign() returns 1, launching the player rightward. If the player is to the left, the result is negative, pushing them leftward. Adding the fallback if (_push_direction == 0) _push_direction = 1; ensures the player doesn't freeze in place if both origin points happen to land on the exact same pixel.

💡 Remember that horizontal and vertical movement code with solid collision handling must run AFTER computing these velocities. That way, if knockback launches the player into a solid wall, the collision system safely stops the player instead of letting them clip through level geometry.

Classic Platformer Distinction: The Stomp Mechanic

In many traditional platformers (such as Super Mario or Sonic), touching an enemy doesn't always penalize the player. If the player lands directly on top of an enemy from above, the enemy takes damage or gets defeated instead, and the player bounces upward to maintain momentum. This mechanic adds dynamic pacing and rewards precision.

To implement this behavior, we verify two spatial conditions before applying player damage: the player must be falling downward (vspd > 0), and the player's feet must be positioned above the enemy's head or center. When both criteria are met, we destroy or damage the enemy and rebound the player; otherwise, the regular damage and knockback routine runs.

Pay close attention to bbox_bottom - vspd <= _enemy_hit.bbox_top + 8. The built-in variable bbox_bottom represents the Y coordinate of the bottom edge of the player's bounding box. Subtracting vspd tells us where the player's feet were located on the previous frame. If on that previous frame the feet were level with or above the enemy's top bounding edge (bbox_top), we confirm that this was an intentional downward stomp rather than a side collision with late downward drift.

Visual Feedback: The Blinking Effect

A common pitfall when coding temporary invincibility is failing to give players unmistakable visual feedback that the state is active. If a player takes damage, becomes invulnerable, and simply glides through enemies with an unchanged sprite, the player will feel confused and disoriented. We must clearly communicate: "You are temporarily safe."

The standard convention in 2D retro games is blinking transparency (alpha modulation) or visibility flickering. In GameMaker, this can be achieved in two main ways:

TechniqueImplementationKey Considerations
Modifying image_alpha in Stepimage_alpha = 0.5 (or toggling between 0 and 1)Very easy to write, but requires manually resetting image_alpha = 1 as soon as the timer expires.
Manual control in the Draw EventConditional draw_self() combined with modulo operator (%)Highly customizable; leaves the instance's underlying state untouched outside rendering.

We will handle this inside obj_player's Draw event using the modulo operator (%). The modulo operator returns the remainder of an integer division. Evaluating (invincible_timer % 12) < 6 creates a clean repeating cycle every 12 frames: for 6 frames the condition is true, and for the following 6 frames it is false. At 60 FPS, this straightforward math generates a crisp, stable flickering rhythm at 5 blinks per second.

// --- obj_player Draw Event ---

// If player is invulnerable, toggle render transparency
if (is_invincible)
{
    // Blink between semi-transparent (0.3) and fully opaque (1.0)
    // Modulo toggles alpha every 6 frames
    if ((invincible_timer % 12) < 6)
    {
        draw_set_alpha(0.3);
    }
    else
    {
        draw_set_alpha(1.0);
    }
    
    // Draw current sprite with the chosen alpha
    draw_self();
    
    // IMPORTANT: Always reset draw alpha back to 1.0 to avoid affecting other draw calls
    draw_set_alpha(1.0);
}
else
{
    // Default rendering when not invincible
    draw_self();
}

💡 Fundamental GameMaker rule: Adding a Draw event to an object disables its automatic sprite rendering. Because of this, calling draw_self() explicitly inside the Draw event is required. If you omit draw_self(), the instance will still exist and register collisions in the room, but it will be completely invisible.

Handling Game Over: Provisional Defeat

While future lessons will introduce a complete HUD health bar and persistent life counters, our collision system still needs a clean response when hp hits 0. At this stage of development, restarting the current room is a quick, robust approach that lets you test and iterate on gameplay loops rapidly.

// --- At the end of damage resolution or obj_player Step Event ---
if (hp <= 0)
{
    // Restart room when health reaches zero
    room_restart();
}

Testing and Troubleshooting

During early testing of this damage pipeline, unexpected behaviors can crop up. Here are three of the most common issues beginners encounter, along with straightforward fixes:

1. The player gets stuck inside a solid wall after knockback: This happens if knockback directly adjusts coordinates via x += knockback_speed_x instead of modifying the velocity variable hspd. By adjusting hspd, the wall collision check we set up in earlier lessons cleanly intercepts the player before clipping occurs.

2. The blinking gets stuck and the player remains transparent: This occurs when setting the built-in image_alpha variable inside Step without guaranteeing a reset back to 1.0 when is_invincible turns false. Handling alpha inside the Draw event with draw_set_alpha(1.0) after draw_self() avoids this issue entirely.

3. The enemy and player bounce off each other infinitely: Make sure the if (!is_invincible) check wraps your instance_place detection. If collisions register while the player is already in hitstun, the knockback timer continuously resets, locking the player out of movement controls.

Practical Exercises

Complete the following three exercises to reinforce your understanding and test the flexibility of your collision system:

Exercise 1 (Tuning Durations and Knockback): Modify invincible_duration and knockback_speed_x. Test three setups: very short immunity (20 frames / a third of a second), standard immunity (90 frames), and prolonged immunity (180 frames / 3 seconds). Try horizontal knockback speeds of 2, 5, and 8. Notice how each affects game feel. Success criterion: With short immunity durations, cornering against an enemy near a wall rapidly drains health; confirm that player controls re-engage promptly once knockback_duration expires.

Exercise 2 (Stomp Mechanic with Variable Rebound): Integrate the Stomp mechanic code block into your project. Add an input condition: if the player holds down the jump key while stomping an enemy, double the rebound height (set vspd to -9 instead of -5.5). Success criterion: Fall onto an enemy without touching keys and observe the bounce height. Repeat while holding the jump key (Space or W) upon impact and verify that the character launches noticeably higher.

Exercise 3 (Color Tinting Instead of Transparency): Modify the Draw event so that during invincibility, the player flashes colors instead of changing alpha, using image_blend or draw_sprite_ext. Have the sprite toggle between its standard color (c_white) and a warning tint (c_red). Success criterion: Upon taking damage, the player blinks red throughout the 1.5 seconds of invincibility and returns to normal tinting as soon as the timer reaches zero.

Summary

  • Continuous contact damage in games requires brief invulnerability periods (i-frames) to prevent instant, unfair deaths at 60 FPS.
  • The 'instance_place(x, y, obj)' function is ideal for combat because it returns the specific enemy instance ID involved in the collision, allowing accurate directional vector calculations.
  • Physical knockback uses the relative direction between player and enemy via 'sign()', applying horizontal and vertical velocities while briefly locking out player input.
  • The Stomp mechanic verifies that the player is descending (vspd > 0) and that their feet were above the enemy's head before impact, eliminating the hazard instead of hurting the player.
  • Visual blinking in the Draw event using the modulo operator (%) provides clear, non-destructive feedback without altering the instance's core state.

Practise what you learned

Exercise 1 · 1 point

Why is the instance_place() function used instead of place_meeting() when detecting a collision with an enemy in the Step event?

You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 2 · 1 point

If the game runs at 60 FPS and the invincible_duration variable is set to 90 frames, how many seconds does the player's temporary invulnerability (i-frames) last?

segundos
You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 3 · 1 point

Arrange the code blocks in chronological order as they should execute inside obj_player's Step event according to the lesson:

  1. 1Update immunity and knockback timers
  2. 2Check collision with an enemy and apply damage or knockback (if not invincible)
  3. 3Read player input controls (if not currently in knockback)
  4. 4Apply gravity and standard physics
You are practising without an account: nothing is saved. Sign up to keep your progress.

Sign in to save your progress — Sign in