Introducción al Desarrollo de Videojuegos con GameMaker · Module 5: Collisions, Gravity, and Platforms
Horizontal Collisions with Solids
Lesson 22 of 60 · 60 min
Also available in: ES
Introduction: The Problem of Movement and Solids
In Module 4, you learned how to capture keyboard or gamepad input and convert it into horizontal velocity using a variable like hsp (horizontal speed). At the end of each game step, you applied that velocity directly to the instance's position with simple math: x += hsp. While this produces smooth, intuitive movement in empty space, it causes an immediate problem the moment you build a playable environment: instances have no mass or physical presence by default; they are simply Cartesian coordinates and graphics rendered to a screen.
If an instance sits at x = 100 with hsp = 4, the next frame will jump it to x = 104, then x = 108, and so on. If a 16-pixel-wide brick wall sits at x = 105, GameMaker simply computes the addition and places the instance at x = 108. As far as the engine is concerned, nothing went wrong—it just added two values together. But to the player, the character clips into the wall or passes straight through it (a technical issue known in game development as "tunneling" or discrete step passing).
The goal of this lesson is to turn unconstrained movement into environment-aware movement. You will learn to query GameMaker's collision engine before committing any change along the horizontal axis. If the projected path is clear, the character moves at full speed. If an obstacle lies ahead, the character stops flush against the surface—leaving no visible gap and never getting embedded inside solid geometry.
Collision Masks and Bounding Boxes
Before writing a single line of collision code, you need to understand what GameMaker evaluates when checking whether two elements touch. Every sprite asset in GameMaker has more than just raw pixels and an origin point; it also has a Collision Mask. This mask defines the geometric boundary that interacts with the game's physical world.
By default, GameMaker assigns sprites an automatic rectangular mask encompassing all non-transparent pixels. This rectangle's bounds are tracked internally via four room-relative cardinal values: bbox_left, bbox_right, bbox_top, and bbox_bottom. When two instances collide, the engine checks for overlap between their bounding boxes.
| Internal Property | Axis | In-Game Description |
|---|---|---|
| bbox_left | Horizontal (X) | X coordinate of the left edge of the instance's collision mask. |
| bbox_right | Horizontal (X) | X coordinate of the right edge of the instance's collision mask. |
| bbox_top | Vertical (Y) | Y coordinate of the top edge of the instance's collision mask. |
| bbox_bottom | Vertical (Y) | Y coordinate of the bottom edge of the instance's collision mask. |
For our retro platformer project, we will work with two primary objects: obj_player (the player-controlled character) and obj_wall (a generic solid block used for floors, ceilings, and walls). To ensure stable, glitch-free collisions, the wall sprite (spr_wall) should be an exact square (such as 32x32 or 16x16 pixels) with its collision mask set to Full Image and its origin typically anchored at Top Left (x: 0, y: 0).
💡 Important: For obj_player, keep the collision mask set to fixed manual values or define a consistent mask_index. If the mask changes dimensions between idle and running animations, the character could change width mid-stride and get stuck inside an adjacent wall.
Look-Ahead Detection: The place_meeting Function
GameMaker provides several collision-checking functions in GML Code, but their precision and intended use cases differ significantly. Beginners often mix up position_meeting, instance_place, and place_meeting. For handling a character's physical movement, place_meeting is the right tool.
What does place_meeting(x, y, obj) actually do? It takes the collision mask of the calling instance, shifts it virtually to the specified (x, y) coordinates, and checks whether that hypothetical footprint overlaps any instance of obj. It returns true if there is an overlap, or false if the space is completely clear. Crucially, the instance does not actually move in the room or in memory; this is purely a predictive check against the immediate future.

Let's compare the three main collision functions side by side to see why place_meeting is specifically suited for character movement:
| GML Function | Evaluated Geometry | Return Value | Recommended Primary Use |
|---|---|---|---|
| position_meeting(x, y, obj) | A single point (x, y) | Boolean (true / false) | Mouse clicks, cursor checks, or point sensors. |
| place_meeting(x, y, obj) | The instance's full collision mask | Boolean (true / false) | Movement checks, solid walls, and floor detection. |
| instance_place(x, y, obj) | The instance's full collision mask | Instance ID or noone | Interactions where you need to reference the other object directly (damaging an enemy, flipping a specific switch). |
The Naive Stop and the Gap Problem
An intuitive first attempt at horizontal collisions is a straightforward condition: if there is an obstacle at the target position, zero out horizontal velocity; otherwise, move forward. Consider this naive snippet:
// Naive attempt (NOT recommended):
if (place_meeting(x + hsp, y, obj_wall)) {
hsp = 0;
}
x += hsp;While this snippet does prevent the character from passing through the wall, it introduces a visual bug that feels unpolished: a noticeable gap. Suppose your character moves at hsp = 4 and is currently 2 pixels away from a wall to the right. When the code checks place_meeting(x + 4, y, obj_wall), it sees that x + 4 hits the wall. As a result, it sets hsp = 0 and the character halts immediately.
The character freezes 2 pixels away from the wall, floating in midair without making contact. If running speed were 6, the player could get stuck anywhere from 1 to 5 pixels away depending on where their stride landed. To fix this gap and deliver responsive, professional platformer controls, we need a pixel-by-pixel approach algorithm.
The Classic Solution: Pixel-by-Pixel Approach with sign() and while
The industry standard for 2D platformers built with custom physics is a two-phase check inside the Step event: early detection followed by contact resolution. If an impact is detected at x + hsp, we do not simply kill all velocity on the spot; we close the remaining distance between the instance and the obstacle first.
To close that distance without overshooting, we advance one pixel at a time in the direction of travel. To determine which way that single step should go, GameMaker provides an essential math function: sign(val).
The sign(n) function evaluates the sign of a real number and returns one of three values: 1 if positive, -1 if negative, and 0 if exactly zero. For example:
| Input (hsp Value) | Result of sign(hsp) | Physical Direction Represented |
|---|---|---|
| hsp = 4.5 | 1 | One-unit step to the right (+1 on X) |
| hsp = -3 | -1 | One-unit step to the left (-1 on X) |
| hsp = 0 | 0 | Stationary (no horizontal movement) |
With this tool in hand, if place_meeting(x + hsp, y, obj_wall) returns true, we enter a while loop. The condition reads: "while there is NO collision a single pixel ahead in my movement direction (!place_meeting(x + sign(hsp), y, obj_wall)), move forward one pixel (x += sign(hsp))". The moment the character sits flush against the wall, the condition evaluates to false, the loop exits immediately, and we set hsp = 0.
Step-by-Step Project Implementation
Now let's integrate this logic into our retro platformer project in a clean, modular way. Make sure your Asset Browser contains the necessary resources before adding the code.
1. Create spr_wall: Make a 32x32 pixel sprite styled as a stone or brick tile. Keep the origin at Top Left (x: 0, y: 0) and verify under Collision Mask that the Mode is set to "Automatic" and the Type is set to "Rectangle".
2. Create obj_wall: Create a new object, name it obj_wall, and assign it spr_wall. Leave the "Solid" checkbox unchecked in the object properties; that checkbox is tied to GameMaker's legacy drag-and-drop collision system and can cause unexpected behavior with custom scripts.
3. Review obj_player's Create Event: In our player object, confirm that the horizontal movement, acceleration, and base speed variables from earlier lessons are properly initialized.
// ====================================================================
// OBJECT: obj_player
// EVENT: Create
// ====================================================================
// Horizontal movement variables
hsp = 0; // Current horizontal speed in pixels per frame
move_speed = 4; // Player's maximum horizontal speed
// Gravity and vertical variables (from previous lesson)
vsp = 0; // Current vertical speed
grav = 0.3; // Gravitational acceleration
grav_max = 8; // Terminal fall velocity4. Structure obj_player's Step Event: The Step event should follow three strict, sequential stages: Input gathering and velocity calculation, Collision detection and resolution, and Final coordinate updates. Below is the complete block with horizontal collision handling integrated.
// ====================================================================
// OBJECT: obj_player
// EVENT: Step
// ====================================================================
// --------------------------------------------------------------------
// STAGE 1: INPUT AND INTENDED VELOCITY
// --------------------------------------------------------------------
// Read keyboard input (arrow keys or A/D)
var _key_left = keyboard_check(vk_left) || keyboard_check(ord("A"));
var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
// Calculate net direction (-1, 0, 1)
var _move = _key_right - _key_left;
// Determine desired horizontal speed
hsp = _move * move_speed;
// Apply preliminary gravity on the vertical axis (covered in Lesson 1)
vsp += grav;
if (vsp > grav_max) {
vsp = grav_max;
}
// --------------------------------------------------------------------
// STAGE 2: HORIZONTAL COLLISION DETECTION AND RESOLUTION
// --------------------------------------------------------------------
// Check if projected position (x + hsp) collides with obj_wall
if (place_meeting(x + hsp, y, obj_wall)) {
// If a collision lies along that span, step pixel-by-pixel
// using sign(hsp) to advance exactly 1 pixel toward the obstacle
while (!place_meeting(x + sign(hsp), y, obj_wall)) {
x += sign(hsp);
}
// Once the player is flush against the surface (1 pixel away from overlap),
// cancel horizontal speed completely for this frame
hsp = 0;
}
// --------------------------------------------------------------------
// STAGE 3: APPLY MOVEMENT
// --------------------------------------------------------------------
// Apply resolved horizontal movement to actual position
x += hsp;
// (Note: Vertical resolution and vsp handling are detailed in the next lesson)
y += vsp;Edge Cases, Common Pitfalls, and Optimization
As your project grows in scope, this basic collision logic can run into edge cases that cause unwanted behavior or severe performance drops if left unaddressed.
1. Infinite Loop Risk with sign(0)
If hsp ever equals 0 when the collision branch is reached (for instance, due to external position nudges or physics impulses from other objects), sign(0) returns 0. Inside a while loop advancing by sign(hsp), x += 0 would never change the instance's position, the collision condition would never change, and the game would hang in an infinite loop, freezing GameMaker's main thread.
To guard against this, wrap the check in a guard clause ensuring horizontal speed is non-zero (hsp != 0), or verify that its absolute value exceeds a minimum threshold:
if (hsp != 0) {
if (place_meeting(x + hsp, y, obj_wall)) {
while (!place_meeting(x + sign(hsp), y, obj_wall)) {
x += sign(hsp);
}
hsp = 0;
}
}2. Grouping Collision Objects with Parents
In a full platformer, obj_player collides with far more than static walls—locked doors, breakable crates, metal platforms, and stone pillars all share physical solidity. Writing a separate if (place_meeting(...)) block for every distinct object in the Step event is poor architecture: it wastes CPU cycles and makes the code difficult to maintain.
The solution is object inheritance using Parents in GameMaker. Create an empty object called obj_solid or par_solid (with no sprite and no code). Then, on every solid obstacle (obj_wall, obj_block_wood, obj_iron_gate), open its properties and set its Parent to obj_solid.
Thanks to GameMaker's inheritance hierarchy, place_meeting(x + hsp, y, obj_solid) checks for collisions against obj_solid and any of its children in one go, consolidating your entire horizontal collision pass into a single, clean check.
3. Fractional Speeds and Sub-Pixel Precision
When you introduce acceleration curves or friction (such as move_speed = 3.75), position and speed variables accumulate fractional values. In modern versions of GameMaker, place_meeting handles sub-pixel positions using floating-point checks aligned with the rendering pipeline. However, stepping x += sign(hsp) when x has a fractional component (like x = 12.4) yields 13.4, 14.4, and so on.
For standard pixel-art platformers on an integer grid, this rarely causes visual issues. But if you notice sub-pixel jitter or microscopic seams when stopping, common remedies include snapping the coordinate to an integer upon collision or maintaining a dedicated fractional accumulator (a pattern we explore in our advanced physics optimization module).
Practical Exercises
Complete the following three hands-on exercises in your GameMaker project to solidify what you have learned.
Exercise 1: Observe the Gap Problem Directly In obj_player, temporarily comment out the while loop with double slashes (//), leaving only if (place_meeting(x + hsp, y, obj_wall)) { hsp = 0; }. Set your movement speed to an odd, high value like move_speed = 7. Run the game, dash straight into a wall, and observe how often the character stops with empty space between its sprite and the wall. Then uncomment the while loop and verify that contact is pixel-perfect every time, regardless of running speed. Success criteria: With the while loop restored, the character touches the wall texture with zero pixel separation whether approaching from the left or right.
Exercise 2: Create a Breakable Block Using Parents Create a new 32x32 sprite named spr_crate with a wooden box texture. Create an object called obj_crate and assign it the sprite. Next, create an empty parent object named obj_solid, and set obj_solid as the Parent for both obj_wall and obj_crate. Update obj_player's Step event to check against obj_solid instead of obj_wall. Success criteria: Place crates and walls in your room. The player must stop flush against both objects identically without any crate-specific collision code added to obj_player.
Exercise 3: Visual Debugging with a Collision Vector Open obj_player's Draw event. Make sure to call draw_self() first so the sprite continues rendering. Right below it, draw a colored line showing where the collision check is projecting each frame using draw_line_color(x, y, x + hsp, y, c_red, c_red). Success criteria: Moving left or right in the room should project a visible red line from the player's origin forward, showing the exact distance checked ahead before committing movement.
Summary
- Direct movement (x += hsp) causes clipping and tunneling through solid objects due to discrete frame-by-frame coordinate updates.
- The place_meeting(x, y, obj) function evaluates the instance's collision mask at a simulated future coordinate without modifying its actual position.
- Halting movement immediately without an approach step leaves visible gaps between the character and obstacles whenever speed exceeds the remaining distance.
- The sign(hsp) function converts any speed into a unit direction (-1, 0, or 1), making it possible to step forward exactly one pixel at a time.
- The loop while (!place_meeting(x + sign(hsp), y, obj_solid)) { x += sign(hsp); } closes the remaining distance pixel by pixel before setting hsp = 0.
- Parent objects (like obj_solid) let you group walls, crates, and barriers under a single clean, modular collision check.
Practise what you learned
Exercise 1 · 1 point
What flaw occurs if you use the naive approach `if (place_meeting(x + hsp, y, obj_wall)) { hsp = 0; }` when the character moves at `hsp = 4` and is only 2 pixels away from a wall?
Exercise 2 · 1 point
Complete the pixel-by-pixel approach algorithm used to resolve exact horizontal collisions against a wall: ```gml if (place_meeting(x + hsp, y, obj_wall)) { while (!place_meeting(x + (hsp), y, obj_wall)) { x += (hsp); } hsp = ; } x += hsp; ```
Exercise 3 · 1 point
The `place_meeting(x, y, obj)` function physically moves the instance to the specified coordinates, modifying its actual position on screen and in memory during the check.
Sign in to save your progress — Sign in