Introducción al Desarrollo de Videojuegos con GameMaker · Module 6: Animations, Sprites, Backgrounds, and Tilemaps

Precise Collisions with Tilemaps

Lesson 30 of 60 · 60 min

Also available in: ES

From Instance Collisions to Tilemap Collisions

Throughout Module 5, we built the physics foundation for our character: applying gravity, calculating horizontal acceleration, handling jumping, and implementing solid collisions using objects like obj_bloque or obj_suelo via functions like place_meeting(). That approach is ideal for understanding the fundamental logic of velocity vectors and contact between geometric bodies. However, in professional 2D platformer development, populating an extensive level with hundreds or thousands of individual solid object instances introduces critical drawbacks.

Each instance in GameMaker consumes system memory and CPU cycles on every frame to manage its internal events, position variables, and transformations. If a level is 4,000 pixels wide and you place 16x16 pixel blocks for floors and walls, you could easily end up with over 2,000 active instances just for static geometry. In the previous lesson, you saw how Tilemaps render entire environments by combining graphics onto a single, ultra-lightweight surface. In this lesson, we take the definitive step: using that exact cell structure to compute high-precision, high-performance collisions without creating a single solid object in the room.

💡 A Note on GML Visual: While GameMaker provides visual action blocks to query layer data, tile-based collision math in medium to large projects is almost universally implemented in GML Code. This is due to the need to verify multiple exact points along the bounding box in a fraction of a second with total mathematical control.

How GameMaker Understands a Tilemap at the Data Level

A Tilemap is not a flat, indivisible image; it is a two-dimensional grid of numeric data. Each grid cell stores an integer known as a tile index. An empty cell always has an index of 0. Any cell containing a graphic pulled from your Tileset will have a positive integer index (1, 2, 3, etc.), corresponding to its position in the source tileset asset.

To detect whether our character is touching an obstacle, we do not need to check for collisions against complex physics geometry. We only need to answer a straightforward mathematical question: does the pixel coordinate the character is moving into fall within a cell whose index is not 0? GameMaker provides an internal, high-performance function designed specifically for this purpose: tilemap_get_at_pixel().

Function / VariableReturn TypeDescription and Purpose
layer_get_id(layer_name)Layer IDRetrieves the unique ID of a room layer using its name string.
layer_tilemap_get_id(layer_id)Tilemap IDFinds and returns the Tilemap element residing on the specified layer.
tilemap_get_at_pixel(tilemap_id, x, y)Integer (Real)Returns the tile index located at room coordinates (x, y). Returns 0 if empty or -1 if out of bounds.
bbox_left, bbox_right, bbox_top, bbox_bottomInteger (Real)Built-in read-only variables indicating the exact room coordinates of the four edges of the instance's collision mask.
Schematic diagram of a character bounding box on a grid with sampling check points at corners and edges.
Sampling points along the bounding box (AABB) evaluated against the Tilemap grid. · AI-generated image

Setting Up the Room and Grabbing the Tilemap in Code

Before programming movement, we must ensure the Tilemap layer is properly identified in the Room. There are two common industry approaches: using an invisible collision tile layer (where you place solid colored blocks strictly to define walls), or checking directly against the visual terrain layer created in lesson 6.4. To keep this course clear and organized, we will use the layer where you drew your level terrain, named "Tiles_Terreno" in your Room Editor's layer inspector.

To interact with the Tilemap from the player object (obj_jugador), we need to store the Tilemap's ID when the character is created. Querying the layer by name every single frame in the Step event would waste CPU cycles unnecessarily. Best practice is to retrieve and store the reference once in the Create event.

// ====================================================================
// RESOURCE: Objects -> obj_jugador
// EVENT: Create
// ====================================================================

// Movement and physics variables (continued from Module 5)
vel_x = 0;              // Current horizontal velocity
vel_y = 0;              // Current vertical velocity
vel_caminar = 2.5;      // Maximum movement speed
gravedad = 0.25;        // Constant downward force
fuerza_salto = -6.5;    // Initial upward impulse
en_suelo = false;       // Flag to check if touching solid ground

// Retrieve the tilemap reference for collisions
// 1. Locate the layer ID using its exact name in the Room Editor
var _id_capa = layer_get_id("Tiles_Terreno");

// 2. Extract the Tilemap element ID contained within that layer
mapa_colision = layer_tilemap_get_id(_id_capa);

With mapa_colision initialized, obj_jugador now has direct access to the Tilemap data in memory. Next, we need to examine how to evaluate the character's edges.

Sampling Points and the Bounding Box (AABB)

In GameMaker, an instance is positioned using its (x, y) coordinates, which depend on where you set the sprite origin (for instance, bottom center). If you only checked tilemap_get_at_pixel(mapa_colision, x, y), you would only be testing a single microscopic point at the base of the character. As a result, the player's head, sides, or shoulders would pass right through walls and ceilings without registering any collision.

To keep the character from penetrating level geometry, we must sample the built-in collision mask variables: bbox_left, bbox_right, bbox_top, and bbox_bottom. These variables return the exact world position in pixels for the edges of the Axis-Aligned Bounding Box (AABB).

However, there is a critical detail for characters taller or wider than 16 pixels: if a character is 32 pixels tall and you only check their top and bottom corners, a thin 16-pixel-thick platform positioned at waist level would pass right between those two sample points undetected. For this reason, whenever a sprite is larger than a tile cell, it is essential to check an intermediate midpoint along the edges or check across the entire span.

Building a Helper Collision Function by Coordinates

To avoid writing repetitive, complex function calls on every movement line, we can write a Script that takes a coordinate (px, py) and returns a simple boolean (true or false) indicating whether that position contains a solid terrain tile. This modular approach keeps the codebase clean and easy to read for any team member.

// ====================================================================
// RESOURCE: Scripts -> scr_colisiones_tilemap
// ====================================================================

/// @function es_celda_solida(_mapa, _px, _py)
/// @description Returns true if the given coordinates touch a solid tile (> 0).
/// @param {Id.TileMapElement} _mapa Tilemap identifier.
/// @param {Real} _px X coordinate in pixels.
/// @param {Real} _py Y coordinate in pixels.
function es_celda_solida(_mapa, _px, _py) {
    // Query the tile index at the given coordinate
    var _indice = tilemap_get_at_pixel(_mapa, _px, _py);
    
    // Index 0 represents an empty cell (air or non-collidable).
    // Any value greater than 0 corresponds to solid level geometry.
    return (_indice > 0);
}

With this helper function in place, testing whether multiple points touch a tile becomes trivial. For example, to check the character's right side at an upcoming horizontal position, we can sample the top-right corner, the right midpoint, and the bottom-right corner.

Step-by-Step Horizontal Collision Resolution

Just as we did in Module 5's classic collision system, we should never directly update x by adding vel_x without resolving collisions first. The horizontal sequence consists of four distinct steps:

1. Determine the leading edge: If vel_x > 0, we are moving right, so the leading edge is bbox_right. If vel_x < 0, we are moving left, so the leading edge is bbox_left.

2. Predict contact: Check whether the future position of that edge (bbox_right + vel_x or bbox_left + vel_x) intersects solid cells at the top, middle, or bottom of the character's body.

3. Pixel-perfect snapping: If an impact is imminent, we avoid leaving the player floating away from the wall or clipping inside it. Using a simple loop or grid alignment math based on tile size (16 pixels), we advance the player pixel by pixel until they make exact contact with the wall.

4. Halt velocity: Set vel_x = 0 to cancel momentum before updating the final x coordinate.

Vertical Collision Resolution and Ground Detection

The vertical axis follows an analogous process, while incorporating gravity and the en_suelo (grounded) state. If vel_y > 0, the character is falling, meaning the evaluated edge is bbox_bottom. If the bottom edge touches a solid tile while descending:

• Snap the character downward until their feet rest flush on the tile surface. • Set vel_y = 0 to cancel accumulated downward gravitational momentum. • Set en_suelo = true, allowing the player to jump again.

Conversely, if vel_y < 0, the character is ascending during a jump and their top edge (bbox_top) hits a ceiling. In that moment, we zero out vel_y so downward gravity takes over immediately, preventing the character's head from penetrating the ceiling.

Complete Implementation in obj_jugador's Step Event

Below is the complete integration of the system. Notice how keyboard input, gravitational acceleration, tilemap edge collisions, and final coordinate updates are computed in sequence.

// ====================================================================
// RESOURCE: Objects -> obj_jugador
// EVENT: Step
// ====================================================================

// --------------------------------------------------------------------
// 1. KEYBOARD INPUT CAPTURE
// --------------------------------------------------------------------
var _tecla_izq   = keyboard_check(vk_left)  || keyboard_check(ord("A"));
var _tecla_der   = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _tecla_salto = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));

// Calculated direction (-1, 0, 1)
var _dir = _tecla_der - _tecla_izq;
vel_x = _dir * vel_caminar;

// --------------------------------------------------------------------
// 2. GRAVITY APPLICATION AND JUMPING
// --------------------------------------------------------------------
vel_y += gravedad;

// Jump conditioned on standing on solid ground
if (en_suelo && _tecla_salto) {
    vel_y = fuerza_salto;
    en_suelo = false;
}

// Assume airborne until checked below
en_suelo = false;

// --------------------------------------------------------------------
// 3. HORIZONTAL TILEMAP COLLISION
// --------------------------------------------------------------------
if (vel_x != 0) {
    // Determine which edge to evaluate based on movement direction
    var _borde_x = (vel_x > 0) ? bbox_right : bbox_left;
    
    // Vertical points to evaluate along that edge (top, middle, and bottom)
    // Subtract 1 from the bottom (bbox_bottom - 1) to avoid false positives with the floor
    var _y_superior = bbox_top;
    var _y_medio    = (bbox_top + bbox_bottom) * 0.5;
    var _y_inferior = bbox_bottom - 1;
    
    // Check if the next position contains a solid tile
    var _colision_h = es_celda_solida(mapa_colision, _borde_x + vel_x, _y_superior) ||
                      es_celda_solida(mapa_colision, _borde_x + vel_x, _y_medio)    ||
                      es_celda_solida(mapa_colision, _borde_x + vel_x, _y_inferior);
                      
    if (_colision_h) {
        // Pixel-by-pixel snapping up to the wall
        var _paso_h = sign(vel_x);
        while (!es_celda_solida(mapa_colision, _borde_x + _paso_h, _y_superior) &&
               !es_celda_solida(mapa_colision, _borde_x + _paso_h, _y_medio)    &&
               !es_celda_solida(mapa_colision, _borde_x + _paso_h, _y_inferior)) {
            x += _paso_h;
            _borde_x += _paso_h;
        }
        vel_x = 0;
    }
}

// Apply resolved horizontal movement
x += vel_x;

// --------------------------------------------------------------------
// 4. VERTICAL TILEMAP COLLISION
// --------------------------------------------------------------------
if (vel_y != 0) {
    // Determine which vertical edge to evaluate (head or feet)
    var _borde_y = (vel_y > 0) ? bbox_bottom : bbox_top;
    
    // Horizontal points to evaluate across the character's width
    var _x_izq    = bbox_left;
    var _x_centro = (bbox_left + bbox_right) * 0.5;
    var _x_der    = bbox_right;
    
    // Check collision at the future vertical position
    var _colision_v = es_celda_solida(mapa_colision, _x_izq,    _borde_y + vel_y) ||
                      es_celda_solida(mapa_colision, _x_centro, _borde_y + vel_y) ||
                      es_celda_solida(mapa_colision, _x_der,    _borde_y + vel_y);
                      
    if (_colision_v) {
        // Pixel-by-pixel snapping toward the vertical obstacle
        var _paso_v = sign(vel_y);
        while (!es_celda_solida(mapa_colision, _x_izq,    _borde_y + _paso_v) &&
               !es_celda_solida(mapa_colision, _x_centro, _borde_y + _paso_v) &&
               !es_celda_solida(mapa_colision, _x_der,    _borde_y + _paso_v)) {
            y += _paso_v;
            _borde_y += _paso_v;
        }
        
        // If falling and a collision occurs, solid ground has been reached
        if (vel_y > 0) {
            en_suelo = true;
        }
        
        vel_y = 0;
    }
}

// Apply resolved vertical movement
y += vel_y;

Pay close attention to a crucial technical detail in the horizontal check: when calculating _y_inferior, we use `bbox_bottom - 1` instead of `bbox_bottom`. If you evaluate the exact pixel of bbox_bottom during a horizontal check while standing on the floor, that coordinate overlaps with the top surface of the floor tile below, falsely registering an oncoming wall. Subtracting 1 pixel lifts the sensor just above the walking surface, ensuring smooth movement without snagging.

Direct Mathematical Grid Alignment (Loop Alternative)

The while loop for pixel snapping is intuitive and highly reliable for standard gameplay speeds. In professional 2D engines, however, there is also a direct mathematical approach: calculating the exact grid multiple where the collided cell starts. Because tiles have a constant size (such as 16x16 pixels), the top edge of a solid block always sits at an exact multiple of 16.

For example, if a character falls toward the floor and bbox_bottom would penetrate a cell, the snapped Y position for the sprite origin can be calculated by subtracting the offset between the origin and the bottom edge: `y = (tile_y * 16) - 1 - (bbox_bottom - y)`. For those getting started with GameMaker, the single-pixel stepping while loop (`sign(vel)`) presented above is the most straightforward and resilient against floating-point rounding quirks.

Tilemap Collisions vs. Solid Object Collisions

Now that both systems are in your game development toolkit, it is essential to understand when to use each as you move toward enemy AI and level design:

FeatureObject-Based Collisions (Module 5)Tilemap-Based Collisions (Module 6)
Memory FootprintHigh: requires a full instance data structure for every block in the room.Near zero: data is read directly from a 2D byte array in graphics memory.
CPU PerformanceDrops noticeably in large rooms with thousands of active instances.Constant and ultra-fast (O(1)), whether the room contains 100 or 10,000 cells.
Ease of EditingLaborious: requires manually placing and aligning individual objects in the room.Instant: painted directly using tileset brushes and room layers.
Recommended UseMoving platforms, breakable doors, triggers, and dynamic hazards.Static solid level geometry: walls, floors, ceilings, and room perimeter bounds.

Hands-On Practice Exercises

Put what you have learned into practice with these three structured exercises for the retro platformer project:

Activity 1: Configure the Collision Layer and Retrieve Its ID. Open your project's main room. In the Layer Editor, make sure your terrain tile layer is named exactly "Tiles_Terreno" (case-sensitive). In the Create event of obj_jugador, add a check using show_debug_message() to print the value of mapa_colision to the console. Verification criterion: Pressing F5 to run the game should print an integer greater than or equal to 0 in the Output window. If it prints -1, the layer name is misspelled or does not exist.

Activity 2: Complete Migration and Removing Redundant Solid Instances. In the room editor, select and delete all instances of obj_suelo or obj_bloque placed during Module 5, leaving only the artwork painted onto the tile layer. Ensure that the Step event of obj_jugador contains the tilemap collision logic covered in this lesson. Verification criterion: The character should walk, jump, and collide with walls and floors just as solidly as before, but the debug overlay will show a dramatic drop in active instance count.

Activity 3: Ceiling Collision and Overhead Response. Place a floating platform using tiles at a height reachable by a full jump. Jump directly underneath it to test the behavior of the top sensor. Verification criterion: The character must immediately interrupt their upward climb the instant their head touches the bottom edge of the overhead tile and begin falling cleanly under gravity, without snagging or clipping through the platform.

Summary

  • Tilemaps manage static level geometry without individual instances, saving massive amounts of memory and CPU cycles.
  • The function layer_tilemap_get_id(layer_get_id('LayerName')) connects player code to the room's tile grid.
  • tilemap_get_at_pixel(map, x, y) queries world coordinates and returns the tile index; values greater than 0 confirm solid ground.
  • Accurate collisions require checking the bounding box edges (bbox_left, bbox_right, bbox_top, bbox_bottom), including midpoints for larger sprites.
  • Subtracting 1 pixel from the bottom edge (bbox_bottom - 1) in horizontal checks prevents the floor beneath the player from registering as a wall.
  • Collision resolution runs in two separate axes (horizontal, then vertical), ensuring pixel-by-pixel snapping before final positions update.

Practise what you learned

Exercise 1 · 1 point

What integer index does GameMaker always assign to an empty cell (no graphic or collision) within a tilemap grid?

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

Exercise 2 · 1 point

Why is it best practice to store the tilemap ID (`collision_map`) in the Create Event instead of looking it up inside the Step Event?

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

Exercise 3 · 1 point

Place the 4 steps for resolving horizontal collisions in chronological order before updating the character's x position:

  1. 1Halt momentum by setting vel_x = 0 before applying the final position update.
  2. 2Predict contact by checking whether the edge's projected position overlaps solid cells.
  3. 3Determine the leading edge based on movement direction (bbox_right or bbox_left).
  4. 4Perform pixel snapping by moving the player flush against the wall.
You are practising without an account: nothing is saved. Sign up to keep your progress.

Sign in to save your progress — Sign in