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

Movement and Collision Code Optimization

Lesson 25 of 60 · 55 min

Also available in: ES

Introduction to Refactoring and Optimization in GameMaker

Throughout the previous four lessons of this module, you progressively built your character's physics engine: first you added constant gravitational acceleration, then you implemented horizontal collision checks against solid obstacles, followed by vertical resolution for floors and ceilings, and finally a boolean flag to track whether the character is securely grounded. While the character now jumps, runs, and collides properly in your test environment, it is very common for the Step event code to become disorganized, dense, and error-prone as soon as you attempt to introduce new mechanics.

This phenomenon is known in software development as technical debt. It happens when features are implemented sequentially and cumulatively—stacking lines on top of each other without stepping back to rethink the overall architecture. Refactoring is the systematic process of restructuring existing code without altering its externally observable behavior for the player. In this chapter, we will not create new mechanics from the user's perspective; instead, we will take the entire movement and collision system built inside the player object (obj_player) and transform it into a modular, readable, optimized codebase ready to scale into the complex animations, enemy behaviors, and moving platforms we will explore in upcoming modules.

Diagnosing the Existing Code: Identifying Technical Debt

To optimize successfully, the first step is to perform a constructive autopsy of the code we have assembled so far. A beginner developer typically places all physics logic directly into the Step event of obj_player. Opening that event reveals an unbroken stream of input polling, math calculations, gravity adjustments, two separate while loops for pixel-by-pixel collision resolution, and auxiliary ground-detection checks.

Let's examine what this monolithic block looks like before applying proper software engineering practices:

// --- PREVIOUS STATE (UNREFACTORED) IN obj_player: Step Event ---

// 1. Key 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;
hsp = _move * walk_speed;

// 3. Gravity application
vsp = vsp + grv;

// 4. Jumping
if (place_meeting(x, y + 1, obj_solid))
{
    if (_key_jump)
    {
        vsp = jump_speed;
    }
}

// 5. Horizontal Collision
if (place_meeting(x + hsp, y, obj_solid))
{
    while (!place_meeting(x + sign(hsp), y, obj_solid))
    {
        x = x + sign(hsp);
    }
    hsp = 0;
}
x = x + hsp;

// 6. Vertical Collision
if (place_meeting(x, y + vsp, obj_solid))
{
    while (!place_meeting(x, y + sign(vsp), obj_solid))
    {
        y = y + sign(vsp);
    }
    vsp = 0;
}
y = y + vsp;

// 7. Grounded state
is_grounded = place_meeting(x, y + 1, obj_solid);

While functional, this monolithic script suffers from serious maintainability problems. First, it bundles multiple responsibilities into a single flat file: player input, physical rules, geometric spatial resolution, and actor state are all entangled. Second, there is clear redundancy: we perform the check place_meeting(x, y + 1, obj_solid) during the jump step, and then evaluate an identical call at the end of the script to update the is_grounded variable. Every collision function call requires geometric calculations from the engine; duplicating them needlessly wastes CPU cycles.

Third, if we later decide to introduce a walking enemy that obeys gravity, we might be tempted to copy and paste the entire collision block into the enemy object. Code duplication is one of the biggest pitfalls in game development: if you discover a bug in collision handling or want to adjust how bodies interact with complex corners, you would have to update every single object individually rather than fixing a single, centralized component.

Separation of Concerns: Functions and Modularity

The Single Responsibility Principle states that each module or function should handle one specific, well-defined job. In a 2D platformer, the character's lifecycle in each frame naturally breaks down into four sequential stages: first, gather player intent (input polling); second, calculate desired forces and impulses (physics and acceleration); third, resolve spatial constraints imposed by the environment (solid collisions); and fourth, update final coordinates and visual representation (sprites and animations).

Four-stage modular flowchart for 2D movement and collision handling: input processing, velocity calculation, collision resolution, and position update.
Figure 1: Decoupled pipeline of responsibilities for the player update loop in each simulation step. · AI-generated image

In modern GameMaker, there are two canonical ways to modularize logic using GML Code: through global functions declared inside a Script asset in the Asset Browser, or through local methods assigned as variables in an object's Create event. For player-specific logic such as inputs and internal states, instance methods provide a clean, encapsulated solution. However, for solid collision resolution, a modular function hosted in a Script asset is superior, allowing you to reuse the exact same collision mathematics for the protagonist, allies, enemies, and physical projectiles alike.

Creating a Reusable Collision Script

Let's create our first general physics utility script. In the GameMaker Asset Browser, right-click the Scripts folder (or the root directory if you haven't organized into folders yet) and choose Create -> Script. Name this asset scr_physics_utilities.

Inside this script, we will write a global function using modern GML syntax called player_collision_and_move. This function encapsulates impact detection algorithms across both cardinal axes and applies the resulting displacement to the calling instance's coordinates, producing clean and consistent code.

/// @function player_collision_and_move(_collision_object)
/// @description Resolves horizontal and vertical collisions against a solid object and updates position.
/// @param {Asset.GMObject} _collision_object The object to check collisions against.
function player_collision_and_move(_collision_object)
{
    // --- HORIZONTAL RESOLUTION ---
    if (place_meeting(x + hsp, y, _collision_object))
    {
        // Move pixel by pixel toward the exact collision boundary
        while (!place_meeting(x + sign(hsp), y, _collision_object))
        {
            x += sign(hsp);
        }
        // On direct contact, zero out accumulated horizontal speed
        hsp = 0;
    }
    
    // Apply resulting horizontal movement to the X axis
    x += hsp;

    // --- VERTICAL RESOLUTION ---
    if (place_meeting(x, y + vsp, _collision_object))
    {
        // Move pixel by pixel toward the contact surface (floor or ceiling)
        while (!place_meeting(x, y + sign(vsp), _collision_object))
        {
            y += sign(vsp);
        }
        // On direct contact, zero out vertical speed (falling or rising)
        vsp = 0;
    }
    
    // Apply resulting vertical movement to the Y axis
    y += vsp;
}

Notice why this architecture is technically robust. The function accepts the _collision_object parameter. This means we are not hardcoded to obj_solid: later in production, we can pass a parent object representing stone blocks, locked doors, or breakable platforms without having to modify a single line of collision logic.

Structuring the Create Event: Clean Initialization

A frequent mistake in GameMaker is declaring variables on the fly inside the Step event. To guarantee optimal memory performance and prevent compiler warnings, every variable that shapes obj_player's behavior should be declared explicitly in the Create event. Furthermore, grouping these variables into semantic categories using clear comments allows any team member to immediately understand the purpose of each parameter.

// --- obj_player: Create Event ---

// 1. Movement configuration parameters and constants
walk_speed = 3.5;       // Base horizontal speed in pixels per frame
grv = 0.35;             // Gravitational constant applied each frame
jump_speed = -7.5;      // Initial upward impulse (negative is up)
max_fall_speed = 9.0;   // Terminal fall velocity to prevent clipping through floors

// 2. Dynamic runtime velocity variables
hsp = 0.0;              // Current accumulated horizontal speed
vsp = 0.0;              // Current accumulated vertical speed

// 3. Physical state variables
is_grounded = false;    // Tracks whether the character is on solid ground

// 4. Input reading variables (initialized for consistency)
key_left = false;
key_right = false;
key_jump = false;

// 5. Local methods for player behavior
get_input = function()
{
    key_left  = keyboard_check(vk_left) || keyboard_check(ord("A"));
    key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
    key_jump  = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));
};

calculate_movement = function()
{
    // Resulting horizontal movement direction (-1, 0, or 1)
    var _move_direction = key_right - key_left;
    hsp = _move_direction * walk_speed;
    
    // Gravity application capped at terminal velocity
    if (vsp < max_fall_speed)
    {
        vsp += grv;
    }
    else
    {
        vsp = max_fall_speed;
    }
    
    // Jump execution strictly conditioned on being grounded
    if (is_grounded && key_jump)
    {
        vsp = jump_speed;
        is_grounded = false;
    }
};

Notice how we introduced the max_fall_speed variable. In a production game, if a character falls from a massive height without a terminal velocity cap, vsp will increase indefinitely due to constant gravity. If vsp reaches an excessive value in a single frame (such as 40 pixels per frame), collision checks can tunnel through thin geometry or demand dozens of iterations inside the while loop to resolve contact, causing noticeable frame drops. Capping terminal velocity is not only physically natural, but it also guarantees that collision loops never consume more execution cycles than budgeted.

The New Step Event: A Clean, Concise Orchestrator

With helper methods defined in Create and the physics routine encapsulated in a Script, the obj_player Step event ceases to be a tangled sequence of math operations. Instead, it becomes an executive-level orchestrator that reads cleanly from top to bottom. Any developer reviewing this file can understand the character's full update flow in less than ten seconds.

// --- obj_player: Step Event (Refactored) ---

// Phase 1: Player input gathering
get_input();

// Phase 2: Physics calculations and acceleration
calculate_movement();

// Phase 3: Collision resolution and world translation against solids
player_collision_and_move(obj_solid);

// Phase 4: Grounded state update for the next frame
is_grounded = place_meeting(x, y + 1, obj_solid);

Compare this version to the original script. If an issue appears with keyboard inputs, you go directly to the get_input method in the Create event. If the character clips through a wall, you investigate scr_physics_utilities. If you want to tune gravity or jump height, you adjust the values in Create without risking any regressions in collision math. Every piece of software now lives in its proper place.

Mathematical Optimization and Precision Considerations in GameMaker

Working with floating-point numbers (decimal values like 3.5 or 0.35) in GameMaker introduces subtle technical considerations that every developer must understand to avoid visual artifacts and collision bugs. Let's examine the best practices for maintaining precise physics and crisp rendering:

First, consider the distinction between place_meeting and instance_place. The function place_meeting(x, y, obj) only checks whether geometric overlap exists between collision masks, returning a simple boolean (true or false). By contrast, instance_place(x, y, obj) must search active room instances and return the unique instance ID (id) of the colliding entity. Calling instance_place inside a while loop is significantly heavier on performance. For simple obstacle blocking against standard walls and floors, place_meeting is always the mathematically optimal choice.

Second, be aware of sub-pixel jitter. In games with low-resolution retro pixel art (such as 320x180 or 640x360 scaled to modern displays), shifting x and y coordinates by fractional values can cause GameMaker to render sprites across pixel boundaries, resulting in shimmering or blurriness. The right solution is not to round physical x and y variables in the Step event—doing so ruins smooth accelerations and falls—but to round visual positions strictly during the Draw event.

// --- obj_player: Draw Event (Optional for Pixel-Perfect Art) ---

// Instead of letting GameMaker draw at fractional coordinates by default,
// we force rendering at the nearest rounded position:
draw_sprite_ext(
    sprite_index,
    image_index,
    floor(x),
    floor(y),
    image_xscale,
    image_yscale,
    image_angle,
    image_blend,
    image_alpha
);

By applying floor(x) and floor(y) exclusively inside the draw call, the physics simulation preserves full floating-point precision under the hood, while the player's screen displays a clean, razor-sharp sprite perfectly aligned with the game's pixel grid.

Comparative Table of Code Design Patterns

The following table contrasts common anti-patterns used by beginners with the clean, professional patterns recommended for sustainable 2D game development:

AspectAnti-Pattern (Cluttered Code)Refactored Solution (Clean Code)Direct Benefit
Step Event StructureDozens of consecutive lines mixing input, math, and collision loops in one script.Distinct phases called through concise methods (get_input, calculate_movement).Absolute clarity; immediate readability of the frame's core update loop.
Collision DetectionDuplicated collision code inside every interactive object (player, enemies, projectiles).Modularized function in a standalone Script, parameterized by obstacle type.Centralized maintenance: fixing a bug once fixes it everywhere.
Velocity ControlUncapped gravitational acceleration leading to runaway falling speeds.Terminal velocity variable (max_fall_speed) that bounds downward vsp growth.Prevents while-loop freezes and accidental clipping through thin terrain.
Variable DeclarationVariables initialized on the fly inside if statements or update steps.Formal, typed declaration in the Create event with clean initial defaults.Eliminates compiler warnings and ensures predictable memory allocation.
Pixel Art RenderingManually rounding physics coordinates x and y within the Step event.Floating-point physics left intact; coordinate rounding handled strictly in Draw.Smooth, natural physics motion without sprite shimmering or visual distortion.

Tooling Perspective: GML Code vs. GML Visual

In GameMaker, you can build games using either GML Code (written programming) or GML Visual (node-based visual blocks). In GML Visual, modularization is achieved using the 'Declare Function' and 'Execute Script' blocks. However, when collision routines involve iterative pixel-by-pixel checks with nested while loops and sign() vector math, visual block diagrams consume massive screen space and become difficult to maintain and audit. GML Code offers superior informational density, syntactic precision, and refactoring speed for platformer physics, making it the industry standard for modern indie development.

💡 Golden rule of optimization: Never optimize blindly without an identified need. Using while loops for pixel-by-pixel collision resolution is perfectly safe and efficient, provided your maximum velocities are bounded by a reasonable terminal velocity (e.g., 8 to 12 pixels per frame).

Practical Consolidation Exercises

Put your refactoring and code-hardening skills into practice by completing the following three structured exercises in your current project.

Summary

  • Refactoring transforms functional but messy code into a clean, readable, and extensible architecture without altering the game's observable behavior.
  • Separating responsibilities into sequential phases (input polling, force calculations, collision resolution, and state updates) prevents cross-system bugs.
  • Instance methods declared in the Create event neatly encapsulate object-specific behaviors.
  • Functions located in Script assets decouple physics from individual objects, allowing players, enemies, and props to share the exact same collision routine.
  • Implementing limits such as terminal fall velocity (max_fall_speed) saves CPU cycles and prevents collision routines from tunneling through thin surfaces.
  • For pixel art graphics, keeping floating-point precision in physics while rounding coordinates with floor() exclusively in the Draw event prevents sprite jitter.
  • With clean and optimized movement and collision systems in place, your project has the solid technical foundation required to implement state-driven animations in the next module.

Practise what you learned

Exercise 1 · 1 point

Place the four sequential phases of the character's per-frame lifecycle in chronological order, based on the single-responsibility principle:

  1. 1Update final position and visual presentation (sprites and animations)
  2. 2Calculate desired forces and impulses (physics and accelerations)
  3. 3Resolve spatial constraints imposed by the level (collisions with solids)
  4. 4Gather player intent (input detection)
You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 2 · 1 point

In the modular function player_collision_and_move(_collision_object), what is the primary advantage of taking _collision_object as a parameter instead of hardcoding obj_solid?

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

Exercise 3 · 1 point

In game development, the disorganized buildup of code caused by adding features without rethinking the overall architecture is known as . In contrast, the systematic process of restructuring existing code without altering its external behavior visible to the player is called .

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

Sign in to save your progress — Sign in