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

Vertical Collisions and Floors

Lesson 23 of 60 · 60 min

Also available in: ES

Fundamentals of Vertical Collision in Platformers

In 2D platformer development, vertical physics is the cornerstone upon which the entire interactive experience rests. While horizontal movement governs progression and stage exploration, vertical movement dictates gravity, jumping, falling, and the character's sense of weight. A flawed vertical collision system instantly breaks player immersion: falling through floors (a phenomenon known technically as 'tunneling'), characters visibly sinking several pixels into the ground before stopping, or jumps clipping unexpectedly through solid ceilings.

In GameMaker, the 2D coordinate space follows a convention that every developer must master from day one: the vertical axis (Y) increases downwards. The coordinate origin (0, 0) is located at the top-left corner of the Room. Consequently, any positive value added to vertical velocity moves the instance downward (toward the floor), while any negative value moves it upward (toward the ceiling or sky). This inverted orientation relative to the traditional Cartesian plane directly shapes the logic and mathematical calculations behind collision checks.

Schematic technical diagram showing a character collision box approaching a floor block and a ceiling block, highlighting velocity vectors and step-by-step checks.
Figure 1: Predictive detection and step-by-step pixel approach on the vertical (Y) axis toward floors and ceilings. · AI-generated image

The Discrete Movement Problem and the Predictive Solution

Game engines execute their main loop frame by frame. If a character accumulates a vertical velocity of 12 pixels per frame due to gravity and you simply write 'y += vspd;', the engine does not smoothly sweep the character across those 12 pixels over time; instead, it 'teleports' the instance 12 pixels down on the very next frame. If a solid floor sits just 4 pixels below the character's feet, the instance will end up embedded 8 pixels deep inside the block.

To prevent this issue, 2D game programmers use an industry-standard technique known as 'predictive collision resolution with contact approach'. Instead of moving the instance first and checking for collisions afterward, the algorithm evaluates the intended future position ('y + vspd') beforehand. If a solid obstacle is detected at that destination, direct movement is halted, and a fine-stepping loop moves the character pixel by pixel until it rests flush against the surface, after which residual velocity is canceled.

Mathematical Breakdown of the Vertical Approach Algorithm

Vertical collision logic relies on two core GameMaker functions: 'place_meeting()' and 'sign()'. The 'place_meeting(x, y, obj)' function takes the calling instance's collision mask, hypothetically positions it at the provided (x, y) coordinates, and checks whether that placement overlaps with any instance of the specified object. It returns a boolean ('true' or 'false') without altering the instance's actual position.

Meanwhile, the mathematical function 'sign(val)' inspects the sign of a numeric value: returning 1 if positive, -1 if negative, and 0 if zero. In this context, 'sign(vspd)' is an indispensable tool because it collapses the direction of vertical movement into a unit vector of exactly one pixel: 1 downward when falling, and -1 upward when ascending during a jump.

// Algorithmic structure of predictive vertical collision
if (place_meeting(x, y + vspd, obj_bloque))
{
    // While there is no collision one pixel ahead in the direction of velocity
    while (!place_meeting(x, y + sign(vspd), obj_bloque))
    {
        y = y + sign(vspd); // Step exactly one pixel toward the surface
    }
    
    vspd = 0; // Fully cancel accumulated vertical velocity
}

// If there is no collision, or once the exact position is resolved, apply movement
y = y + vspd;

The behavior of the 'while' loop deserves close attention. A 'while' loop repeats its internal statements as many times as necessary within the exact same frame before GameMaker renders anything to the screen. If a character falls at 8 pixels per frame and the floor is 3 pixels away, the initial condition 'place_meeting(x, y + 8, obj_bloque)' evaluates to true. The 'while' loop checks 'y + 1' (empty) and steps 1 pixel; checks 'y + 2' (empty) and steps another pixel; checks 'y + 3' (contacts the floor). Detecting contact one pixel ahead, the loop stops immediately. The character's position is snapped flush to the edge of the floor, and 'vspd' is set to 0, ensuring gravity will not push the character into solid geometry on the next frame.

Floors and Ceilings: Unified Handling with sign()

One of the greatest strengths of this algorithm is its mathematical symmetry. Beginners often make the mistake of writing one code block for floors ('if vspd > 0') and a completely separate one for ceilings ('if vspd < 0'). This duplication introduces bugs and makes maintenance tedious. By leveraging 'sign(vspd)', the exact same snippet seamlessly handles both scenarios with millimeter precision:

1. When falling ('vspd > 0'): 'sign(vspd)' equals +1. The loop nudges the character downward until touching the top surface of the floor block, setting 'vspd = 0'. The player lands cleanly, and gravity stops pulling them into the void.

2. When jumping ('vspd < 0'): 'sign(vspd)' equals -1. The loop pushes the character upward until their head touches the underside of the ceiling block. At that moment, setting 'vspd = 0' instantly cancels upward jump momentum. On the next frame, gravity adds positive velocity again, causing the character to begin falling naturally right after bonking their head—just like in classic retro platformers.

Complete Integration into the Player Object

To ensure technical consistency across our platformer project, we will integrate vertical collisions alongside the horizontal collisions and basic gravity system covered previously. All variables must first be declared in the Create event of the player object ('obj_jugador'), guaranteeing they exist in memory before any script reads or modifies them.

// ========================================================
// OBJECT: obj_jugador
// EVENT: Create
// ========================================================

// Horizontal movement variables
hspd = 0;             // Current horizontal speed in pixels per frame
move_speed = 3.5;     // Maximum lateral movement speed

// Vertical movement and physics variables
vspd = 0;             // Current vertical speed in pixels per frame
grv = 0.25;           // Gravity acceleration applied per frame
jump_speed = -6.5;    // Initial jump impulse (negative moves upward)
vspd_max = 9.0;       // Terminal falling velocity

With variables properly initialized, the Step event will centralize input handling, physics acceleration, and sequential collision resolution. The execution order within the Step event is critical: first read player input, then calculate target velocities (including gravitational acceleration), resolve horizontal collisions, resolve vertical collisions, and finally apply movement.

// ========================================================
// OBJECT: obj_jugador
// EVENT: Step
// ========================================================

// 1. KEYBOARD INPUT CAPTURE
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 = _key_right - _key_left;
hspd = _move * move_speed;

// 3. APPLY GRAVITY
// Gravity increases downward speed until terminal velocity is reached
vspd = vspd + grv;
if (vspd > vspd_max)
{
    vspd = vspd_max;
}

// 4. HORIZONTAL COLLISION RESOLUTION
if (place_meeting(x + hspd, y, obj_bloque))
{
    while (!place_meeting(x + sign(hspd), y, obj_bloque))
    {
        x = x + sign(hspd);
    }
    hspd = 0;
}
x = x + hspd;

// 5. VERTICAL COLLISION RESOLUTION (FLOORS AND CEILINGS)
if (place_meeting(x, y + vspd, obj_bloque))
{
    while (!place_meeting(x, y + sign(vspd), obj_bloque))
    {
        y = y + sign(vspd);
    }
    vspd = 0;
}
y = y + vspd;

Notice that movement is strictly divided into two distinct phases: the X axis is resolved and applied first ('x = x + hspd'), followed by the Y axis ('y = y + vspd'). This separation of axes (Orthogonal Axis Separation) is essential. If you attempted to evaluate diagonals simultaneously with 'place_meeting(x + hspd, y + vspd, obj_bloque)', the character would snag on corners or freeze when walking flush against a vertical wall while standing on the floor.

Functional Differences Between Collision Methods in GameMaker

GameMaker provides several collision detection functions, and confusing them often leads to severe gameplay glitches or performance drops. The table below outlines the most relevant functions to clarify when and why 'place_meeting()' is the ideal choice for platformer collision algorithms.

GML FunctionUnderlying GeometryReturn ValueRecommended Use Case
place_meeting(x, y, obj)The full collision mask (hitbox) of the calling instance shifted to (x, y)Boolean (true / false)Solid character collisions with environment (walls, floors, and ceilings).
instance_place(x, y, obj)The full collision mask of the calling instance shifted to (x, y)Unique instance ID of the collided object (or noone)Interactions where the hit object must be modified (damaging an enemy, breaking a block).
position_meeting(x, y, obj)A single infinitesimal point (x, y)Boolean (true / false)Cursor/mouse hovering or isolated point checks.
collision_rectangle(x1, y1, x2, y2, obj, prec, notme)An arbitrary bounding box defined by four coordinatesUnique instance ID of the collided object (or noone)Area triggers, enemy lines of sight, or area-of-effect damage.

💡 Golden rule for collision masks: Ensure your character sprite uses a fixed-size rectangular collision mask, or explicitly set a separate mask in the object's 'Mask' property. If the collision mask changes dimensions during an animation (e.g., the character's head rises two pixels in a running frame), the instance may become trapped inside a floor or ceiling due to the sudden geometric shift.

Practical Case Studies and Troubleshooting

Step-by-Step GameMaker Setup

To test this behavior hands-on in your project, follow these step-by-step instructions in the GameMaker IDE:

Step 1: Create the solid block sprite. In the Asset Browser, right-click the 'Sprites' folder and select 'Create > Sprite'. Name it 'spr_bloque'. In the Image Editor, draw a solid 32x32-pixel square using a dark, flat color that contrasts clearly with the room background. Leave the sprite origin at Top Left (x=0, y=0).

Step 2: Create the block object. In the Asset Browser, right-click 'Objects' and select 'Create > Object'. Name it 'obj_bloque' and assign it the 'spr_bloque' sprite. You do not need to check GameMaker's built-in 'Solid' box; our custom code resolves collisions mathematically without relying on legacy collision physics.

Step 3: Verify the player object. Ensure your 'obj_jugador' has a representative sprite assigned (such as 'spr_jugador' at 32x32 or 32x48 pixels). Open the sprite in the editor, expand the 'Collision Mask' panel on the right, set the 'Type' to 'Rectangle', and ensure the 'Origin' is set to a consistent anchor point, such as 'Bottom Center' or 'Top Left'. For this course, 'Bottom Center' is recommended for bipedal characters so that the anchor aligns directly with the feet.

Step 4: Design the test room. Open your test room ('Room1'). In the Layers inspector, select the Instances layer ('Instances'). Drag multiple instances of 'obj_bloque' into the room to form a continuous horizontal floor along the bottom. Then, place several blocks to build elevated platforms at varying heights, and place an overhead row of blocks suspended in the air to serve as a ceiling test. Finally, place an instance of 'obj_jugador' suspended in mid-air above the floor.

Step 5: Run and test. Press F5 or click the 'Run' button on the top toolbar. When the game starts, the character should fall under gravity and come to a clean stop directly on the floor surface without jittering or visible gaps. When moving with arrow keys or A/D, the character should glide across the continuous floor without sinking. Jumping beneath the suspended platform should cause the character's head to bump the ceiling and begin falling immediately.

Practical Exercises

Complete the following three exercises to reinforce and expand upon the concepts learned in this lesson:

Exercise 1 (Terminal Velocity Adjustment and Stress Testing): In the Create event of 'obj_jugador', modify 'vspd_max' to a low value of 2.0, then test it with an extreme value of 20.0. Run the room in both scenarios and have the player jump from the highest platform. Visually confirm that, despite the extreme fall speed in the second test, the character stops with exact pixel precision on top of 'obj_bloque' without clipping through. Verification criteria: The character rests flush atop the block without sinking even a single pixel in either test.

Exercise 2 (Ceiling Bump Visual Feedback): Implement immediate visual feedback when the player hits a ceiling. In the Step event of 'obj_jugador', locate the vertical collision block. Before 'vspd = 0;', add a conditional check to see if the collision occurred while moving upward ('if (vspd < 0)'). If true, temporarily tint the player sprite by setting 'image_blend = c_red;'. When the player is not colliding or touches the ground, reset 'image_blend = c_white;'. Verification criteria: The player flashes red exclusively on the frame their head hits an overhead block, returning to normal color during the fall.

Exercise 3 (Fast Fall Mechanic): Add a responsive fast-fall mechanic using keyboard input. In the Step event, detect whether the down key is held ('keyboard_check(vk_down) || keyboard_check(ord("S"))'). If held while airborne and moving downward ('vspd > 0'), double the gravitational acceleration applied that frame ('vspd += grv * 2;'). Verification criteria: Holding down while falling causes the character to accelerate downward much faster, while still landing cleanly and securely on the floor without clipping.

Summary

  • The vertical (Y) axis in GameMaker increases downward: positive velocities represent falling (toward the floor) and negative velocities represent rising (toward the ceiling).
  • Discrete frame-by-frame movement can cause tunneling through obstacles if speed exceeds obstacle thickness.
  • Predictive resolution checks the future position (y + vspd) using place_meeting() before altering the instance's actual coordinates.
  • The while (!place_meeting(x, y + sign(vspd), obj_bloque)) loop steps the instance pixel by pixel until exact surface contact is achieved.
  • The sign(vspd) function unifies the algorithm: it yields +1 when falling toward floors and -1 when rising toward ceilings.
  • Once a vertical collision is resolved, vspd must be explicitly reset to 0 to cancel momentum and allow gravity to accumulate cleanly.
  • Separating horizontal (X) and vertical (Y) collision resolution and application prevents snagging on corners and flush walls.

Practise what you learned

Exercise 1 · 1 point

Order the steps of the Step event execution cycle sequentially to ensure proper handling of movement and collisions, as covered in the lesson:

  1. 1Resolve horizontal collisions
  2. 2Calculate target velocities and apply gravity acceleration
  3. 3Capture player keyboard inputs
  4. 4Resolve vertical collisions
You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 2 · 1 point

What value does the function sign(vspd) return when the character rises into the air after jumping, and what is the technical reason for this in GameMaker?

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

Exercise 3 · 2 points

Complete the predictive vertical collision block. Implement the while loop to move the character pixel by pixel toward the surface using sign(vspd) while no collision occurs, then negate the vertical speed by assigning 0 to vspd.

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