Introducción al Desarrollo de Videojuegos con GameMaker · Module 5: Collisions, Gravity, and Platforms

Ground Contact Detection (Grounded)

Lesson 24 of 60 · 50 min

Also available in: ES

Ground Contact Detection (Grounded): Fundamentals and Purpose

In 2D platformer development, one of the most fundamental questions the game engine must answer on every single frame is: is the character standing on a solid surface, or are they suspended in mid-air? To the human eye, this is obvious simply by looking at the screen; however, to the processor, an object is merely a collection of numerical coordinates (x, y) and a bounding box. The computer never assumes the character is 'standing' unless we write an explicit logical check to verify it.

Knowing whether the player is resting on solid ground—a state universally referred to as being 'grounded'—is the cornerstone upon which nearly every core platformer mechanic rests. Without a reliable way to detect the ground, the character could perform infinite jumps in mid-air simply by mashing the jump button, float erratically, or play a walking animation while plummeting into a pit. Ground detection is also essential for resetting mechanics like double jumps, applying proper horizontal friction, and triggering landing impact sound effects.

In the previous lessons of this module, we configured constant downward acceleration via a gravity variable (`grav`) and resolved vertical physical contact against solid objects (`obj_solid`), zeroing out vertical speed (`vspd = 0`) to prevent the avatar from clipping through platforms. In this lesson, we will take our character's architecture a step further: we will introduce a dedicated boolean flag (`is_grounded`), explain how to query the collision engine one pixel below the player's feet without altering their actual position, and organize the code so that ground state remains consistent throughout the entire frame cycle.

What Is a Boolean Flag in GML?

In computer science and software development, a flag is a variable that can hold only one of two possible states: true (`true`) or false (`false`). In GameMaker Language (GML), booleans are handled internally as numbers where `true` equals 1 and any value less than or equal to 0 evaluates to `false`. Nonetheless, writing clean, readable code requires using the explicit constants `true` and `false`, or evaluating conditional expressions directly.

By creating a boolean flag like `is_grounded`, we abstract a complex geometric condition into a simple binary answer accessible anywhere in our project. Instead of running expensive collision checks every time the code needs to know if the player can jump, play footstep sounds, or switch from a 'falling' sprite to an 'idle' sprite, the geometric check runs only once per frame and stores the result in the boolean variable.

This separation between physics detection and decision-making not only makes the code cleaner and easier for beginners to grasp, but also saves processing power and prevents logical bugs such as race conditions or desynced states within the same frame.

Schematic diagram illustrating the one-pixel downward check (y + 1) between the player's bounding box and the solid ground tile.
Figure 1: Virtual projection of one pixel downward (y + 1) to check for a solid surface immediately beneath the character without causing overlap. · AI-generated image

The Bottom-Pixel Technique: place_meeting(x, y + 1, obj_solid)

To determine whether a body rests on a surface without being embedded inside it, we must run a hypothetical query. If we tested `place_meeting(x, y, obj_solid)`, the function would return `false` while the character is resting properly, because the vertical collision algorithm we developed in the previous lesson ensures the player is placed in tangential contact with the surface without penetrating it by even a single pixel.

If `place_meeting(x, y, obj_solid)` were to return `true`, it would mean the character is stuck inside the floor or wall, which indicates a collision bug. Therefore, the industry-standard technique is to ask what would happen if we shifted the player exactly one pixel downward along the vertical axis: `place_meeting(x, y + 1, obj_solid)`.

The built-in function `place_meeting(x_virtual, y_virtual, object)` does not modify the instance's actual `x` and `y` position variables. Under the hood, it temporarily shifts the instance's collision mask to the specified coordinates, checks whether it overlaps with the mask of any instance of the given object (`obj_solid` in our case), and returns a boolean value (`true` or `false`). Immediately afterward, the collision mask returns to its original position without anything visible changing on screen.

By adding 1 to the vertical coordinate (`y + 1`), we are essentially asking the physics engine: 'If the character were to take a microscopic 1-pixel step downward, would it collide with a solid object?' If the answer is yes, the player is zero pixels away from the floor—meaning they are grounded. If the answer is no, the player is more than one pixel away from the nearest floor—meaning they are airborne.

Declaration and Initial Setup in the Create Event

Continuing the step-by-step architecture of our retro platformer project, open the player object (`obj_player`). In its **Create** event, make sure to initialize the `is_grounded` variable. A golden rule in GameMaker: any variable evaluated in a recurring event like **Step** must first be declared in **Create** to avoid compile-time errors or uninitialized variable crashes.

Here is how `obj_player`'s **Create** event should look, keeping the horizontal movement, vertical movement, and jump variables we built across Module 4 and earlier lessons in Module 5:

/// obj_player Create Event

// --- Horizontal movement variables ---
move_spd = 4;        // Maximum horizontal movement speed in pixels per frame
hspd = 0;            // Current horizontal speed

// --- Vertical movement and physics variables ---
vspd = 0;            // Current vertical speed
grav = 0.3;          // Gravity acceleration applied each frame
jsp = -7.5;          // Jump impulse force (negative moves upward)
max_vspd = 10;       // Terminal fall speed

// --- Logical state flags ---
is_grounded = false; // Indicates whether the player is currently touching the ground

When the player instance is created in the room, we set `is_grounded = false` as a safeguard. We don't assume the character spawns touching the floor; we let the very first frame evaluate the environment and determine the true ground state automatically.

Execution Order in the Step Event

In game development, the order of statements within the **Step** event is paramount. A typical physics pipeline consists of four sequential stages that must execute in this exact sequence:

Pipeline StagePrimary PurposeMath / GML Function
1. Input GatheringCapture the buttons or keys pressed by the user.keyboard_check, keyboard_check_pressed
2. Force CalculationCalculate target speeds, apply acceleration and gravity.hspd = move_dir * move_spd; vspd += grav;
3. Collision ResolutionPrevent the character from penetrating solids on X and Y.place_meeting(x + hspd, ...), place_meeting(x, y + vspd, ...)
4. State Update (Grounded)Evaluate the final contact state once coordinates are resolved.is_grounded = place_meeting(x, y + 1, obj_solid);

If we attempted to evaluate `is_grounded` before calculating and resolving vertical collisions, the variable would hold the previous frame's state. This causes an imperceptible one-frame lag that can lead to subtle glitches in jumping or animations.

Full Implementation in the Step Event

Below is the complete **Step** event code for `obj_player`. This integrates input reading, gravity, jumping restricted strictly by `is_grounded`, pixel-by-pixel collision checks against `obj_solid`, and the final flag update after all movement is resolved.

/// obj_player Step Event

// 1. KEYBOARD INPUT GATHERING
var _key_left  = keyboard_check(vk_left)  || keyboard_check(ord("A"));
var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _key_jump  = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));

// 2. HORIZONTAL MOVEMENT CALCULATION
var _move_input = _key_right - _key_left;
hspd = _move_input * move_spd;

// 3. GRAVITY APPLICATION
// Gravity is added to vertical speed every frame, capped at terminal velocity
vspd += grav;
if (vspd > max_vspd) {
    vspd = max_vspd;
}

// 4. JUMP ACTION CONTROLLED BY GROUNDED FLAG
// Jumping is only allowed if the player is firmly standing on the ground
if (is_grounded && _key_jump) {
    vspd = jsp;
    is_grounded = false; // Jumping immediately cancels the grounded state
}

// 5. HORIZONTAL COLLISION RESOLUTION
if (place_meeting(x + hspd, y, obj_solid)) {
    // If an obstacle lies ahead, step pixel by pixel until flush against it
    var _step_h = sign(hspd);
    while (!place_meeting(x + _step_h, y, obj_solid)) {
        x += _step_h;
    }
    hspd = 0;
}
x += hspd;

// 6. VERTICAL COLLISION RESOLUTION
if (place_meeting(x, y + vspd, obj_solid)) {
    // If colliding with ceiling or floor, move flush pixel by pixel
    var _step_v = sign(vspd);
    while (!place_meeting(x, y + _step_v, obj_solid)) {
        y += _step_v;
    }
    vspd = 0;
}
y += vspd;

// 7. FINAL GROUNDED FLAG UPDATE
// Check if, at the new physical position, a solid exists exactly 1 pixel below
is_grounded = place_meeting(x, y + 1, obj_solid);

Let's take a close look at steps 4 and 7. In step 4, jumping requires `is_grounded && _key_jump`. If the player is falling off a ledge or in the middle of an existing jump arc, `is_grounded` is `false`, immediately bypassing `vspd = jsp`. This completely eliminates infinite mid-air jumping.

Additionally, whenever a jump succeeds, we immediately force `is_grounded = false`. Even though step 7 at the end of the event will re-evaluate the state, invalidating the flag the moment the command executes adds an extra layer of logical safety, preventing unwanted side effects in any dependent scripts running later in that same frame.

False Negatives and the Jittering Issue

One of the most frequent hurdles beginner developers encounter when programming a platformer is character 'jittering' while standing still. This happens when the character continuously alternates between being grounded and airborne on alternating frames (for instance: frame 1 grounded = true, frame 2 grounded = false, frame 3 grounded = true).

Why does this happen? The primary cause is poor synchronization between gravity application and collision resolution. If gravity pulls the player downward every frame, but collisions aren't resolved cleanly until the sprite embeds into the surface and a sloppy script pushes it back upward, the distance between the player and the ground fluctuates chaotically.

With our pixel-by-pixel approach using `while (!place_meeting(x, y + sign(vspd), obj_solid))`, we guarantee that when `vspd` is zeroed out (`vspd = 0`), the gap between the bottom of the player's mask and the top of `obj_solid` is exactly zero pixels. Thus, evaluating `place_meeting(x, y + 1, obj_solid)` produces a mathematically and physically consistent result: `true` on 100% of frames where the player remains standing on the surface.

💡 Pay Attention to Sprite Origins and Collision Masks: Ensure your sprite's collision mask dimensions do not change unpredictably between frames or animations. If you switch from a 32-pixel-tall sprite to a 30-pixel-tall sprite without setting the origin to the bottom center (Bottom Centre), the character will momentarily detach from the floor, causing a false negative in is_grounded.

Detecting the Exact Landing Moment

Beyond knowing whether the character is continuously grounded, we often need to identify the exact frame the player hits the ground after falling. This transition is the ideal trigger for visual effects (such as dust clouds), impact sound effects (such as a solid thud or crunching gravel), or subtle cosmetic squash-and-stretch animations.

To detect this state change, we can compare the current frame's `is_grounded` value with the previous frame's value. All it takes is declaring an extra variable named `was_grounded` in the **Create** event and updating it properly at the end of the **Step** event.

Let's break down the logic behind this pattern: the condition `!was_grounded && is_grounded` can only evaluate to true for a single frame—the frame where we transition from airborne (`was_grounded == false`) to ground contact (`is_grounded == true`). On the next frame, because `was_grounded` is updated to match `is_grounded`, the condition automatically evaluates to false again. This is a simple yet foundational design pattern in game programming.

GML Visual vs. GML Code for Contact Detection

While this course prioritizes GML Code for its flexibility, precision, and industry relevance in indie development, GameMaker also lets you implement this logic using its visual block system (GML Visual). For students curious about how this logic translates to visual blocks, the conceptual approach is identical:

In GML Visual, you use the 'If Collision at Object' action block. In its parameters, you pass the X coordinate as `x`, the Y coordinate as `y + 1`, and select `obj_solid`. Beneath that conditional block, you attach an 'Assign Variable' block setting `is_grounded` to `true`, followed by an 'Else' block setting it to `false`. In GML Code, that entire four-block visual structure is expressed cleanly and concisely in a single self-explanatory line: `is_grounded = place_meeting(x, y + 1, obj_solid);`.

Hands-On Exercises

To solidify the concepts covered in this lesson, complete the following three exercises directly within your GameMaker project.

Summary

  • The 'grounded' state accurately tracks whether the character rests on a physical surface, preventing bugs such as infinite mid-air jumping.
  • A boolean flag in GML stores true or false (1 or 0), providing a clean, accessible way to check the character's physical state.
  • The place_meeting(x, y + 1, obj_solid) technique checks for an obstacle exactly one pixel below the collision mask without shifting the player's actual coordinates.
  • Execution order in the Step event is critical: process inputs and forces first, resolve physical collisions second, and update the is_grounded flag last.
  • Comparing the previous state (was_grounded) with the current state (is_grounded) lets you pinpoint the exact landing frame to trigger sound effects, animations, and particle bursts.

Practise what you learned

Exercise 1 · 1 point

Does the function `place_meeting(x, y + 1, obj_solid)` permanently modify the player instance's actual `y` coordinate in the room?

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

Exercise 2 · 1 point

Why does the standard ground-check technique use `y + 1` in `place_meeting` instead of the instance's current `y` coordinate?

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

Exercise 3 · 2 points

Complete the assignment for the boolean variable `is_grounded`. Use the `place_meeting` function to check for a collision with `obj_solid` exactly 1 pixel below the player's current position.

Your code is not executed: specific requirements are checked on the text.

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

Sign in to save your progress — Sign in