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

Sprite State Management and Sub-Images in GameMaker

Lesson 26 of 60 · 50 min

Also available in: ES

Introduction: Connecting Physics with Visual Feedback

Up to this point in the course, our main character features a solid movement system: it responds to keyboard inputs, calculates horizontal acceleration, applies gravity, and detects precise collisions against floors and walls. However, on screen, the character can look stiff or incomplete if its visuals do not reflect its mechanical state. A character sprinting at top speed with a static idle drawing, or legs churning mid-air while falling, immediately breaks player immersion.

In GameMaker, an instance's graphical presentation is governed by Sprite assets and a set of built-in variables specifically designed to determine which frame renders and how fast it plays. In this lesson, you will take full control of the animation engine using GML Code (GameMaker Language). You will learn how to break down animations into sub-images, avoid visual desyncs during state transitions, and tie playback speed directly to the physics variables calculated in previous modules.

Anatomy of a GameMaker Sprite: Sub-Images and Playback Speed

A Sprite in GameMaker is rarely just a single still picture. In most 2D games, it consists of a sequential strip of frames known technically as 'sub-images'. Each sub-image represents an individual pose in an animation sequence, indexed using a zero-based numbering system. If a run sprite contains 6 frames, its numerical indices run strictly from 0 to 5.

Inside GameMaker's Sprite Editor, each asset has two primary configuration settings that define its default behavior: the sprite origin (which we set to 'Bottom Centre' for our platformer to ensure reliable ground alignment) and the base animation speed (typically defined as 'Frames per Second' or FPS, such as 12 or 15 FPS). While these settings are initially configured visually in the editor, their real power comes from manipulating them dynamically through code at runtime.

Diagram illustrating a sequence of sub-images indexed from 0 to 5, featuring progression markers for image_index and image_speed multipliers.
Figure 1: Conceptual overview of cyclic sub-image advancement driven by image_index and image_speed. · AI-generated image

Essential Built-In Animation Variables in GML

Every instance placed in a room automatically inherits a collection of built-in variables that communicate with the graphics engine on every step of the game loop. The four indispensable variables controlling our animations are detailed below:

GML VariableData TypeAccessPrimary Function
sprite_indexAsset Reference (Sprite)Read / WriteStores the sprite asset currently assigned to the instance (e.g., spr_player_run).
image_indexReal (Floating Point)Read / WriteIndicates the current frame being displayed. Internally increments as a float before being truncated for rendering.
image_speedRealRead / WriteMultiplier applied to the sprite's base animation speed. 1 is nominal speed, 0 pauses playback, and 0.5 plays at half speed.
image_numberPositive IntegerRead-OnlyReturns the total number of sub-images in the sprite currently assigned to sprite_index.

It is critical to realize that 'image_index' is not a simple integer jumping abruptly from 0 to 1 and then 2 each game step. Because a game typically runs at 60 steps per second (Room Speed / Game Speed) while a sprite animation might be set to 10 or 12 FPS, GameMaker adds a fractional decimal to 'image_index' every step (for instance, adding 0.2 per step). The renderer draws only the floored integer value, but this internal floating-point counter ensures smooth playback at the specified rate regardless of the engine's global frame rate.

💡 Golden rule: Never attempt to set 'image_number' in code. It is a strictly read-only variable reflecting the total frames created in the asset editor. Attempting to assign a value to it will trigger a runtime error.

The Classic Sprite Switching Pitfall and How to Fix It

A very common bug among beginner developers happens when switching an instance's sprite between idle ('spr_player_idle') and running ('spr_player_run'). Suppose 'spr_player_idle' has 4 frames (indices 0 to 3) and 'spr_player_run' has 8 frames (indices 0 to 7). If your step code executes 'sprite_index = spr_player_run' every frame the player holds right, GameMaker retains the accumulated value of 'image_index'.

If the transition occurs while 'image_index' is at 3.8, the new run cycle abruptly starts midway through its motion. Even worse: transitioning from a 10-frame attack at index 8 down to a 2-frame idle sprite causes 'image_index' to briefly exceed the target sprite's valid frame range, leading to visual flickering until the engine wraps the counter back to zero.

To prevent this issue, always check whether the current sprite already matches the target asset before assigning it. If it does not, swap the sprite and reset 'image_index = 0' immediately, guaranteeing every animation begins cleanly on its first frame.

// Safe check before switching sprites
if (sprite_index != spr_player_run)
{
    sprite_index = spr_player_run;
    image_index = 0;
}

Syncing Animation Speed with Movement Physics

In many classic retro titles, run animations loop at a single fixed speed. However, if your character uses gradual acceleration and deceleration (as implemented in Module 4), running the animation at full cadence while the player is barely starting to move creates 'foot skating': the character's feet move noticeably faster than the ground passes beneath them.

We can eliminate this by dynamically modulating 'image_speed' based on the player's actual horizontal speed ('hsp'). Calculating the ratio of current horizontal speed to maximum movement speed ('move_speed_max') yields a scaling factor that adjusts step cadence naturally.

Manual Frame Control: Jumping and Falling

Airborne states require a different approach. In standard platformers, jumping is rarely a long, looping sequence; instead, it is split into a distinct upward rise pose and an open falling pose. Developers commonly package both poses into a single asset ('spr_player_jump') containing two sub-images: sub-image 0 for ascending and sub-image 1 for falling.

To manage this directly, set 'image_speed = 0'. This halts automatic frame advancement, allowing you to explicitly dictate which sub-image displays based on the sign of the vertical velocity ('vsp').

// Manual airborne frame control
if (!grounded)
{
    sprite_index = spr_player_jump;
    image_speed = 0; // Freeze automatic frame progression
    
    if (vsp < 0)
    {
        // Character is ascending
        image_index = 0;
    }
    else
    {
        // Character reached peak apex and is falling
        image_index = 1;
    }
}

Detecting Keyframes and Animation Events

In professional game development, certain events must trigger in tight sync with animations: playing a footstep audio cue when a boot lands, spawning dust particles, or enabling an attack hitbox during the strike frame. In GML, you monitor 'image_index' within the Step event to achieve this.

Because 'image_index' accumulates floating-point values that increment based on delta time and animation settings, a condition such as 'if (image_index == 2)' almost never evaluates to true; the counter will jump from something like 1.88 to 2.12 without landing exactly on 2.0000. To reliably detect entering a keyframe without third-party libraries, inspect 'floor(image_index)' and guard it with a boolean flag so the action runs only once per cycle.

// Conceptual example: Detecting footstep on frame 2 of spr_player_run
// Required setup variable in Create event: footstep_played = false;

var _current_frame = floor(image_index);

if (_current_frame == 2)
{
    if (!footstep_played)
    {
        // Execute one-time action upon entering frame 2
        // For example: play a sound effect or spawn dust particles
        footstep_played = true;
    }
}
else
{
    // Reset trigger once we leave frame 2 for the next animation loop
    footstep_played = false;
}

Step-by-Step Implementation in obj_player

Now let us integrate these techniques into our platformer's primary player object, 'obj_player'. We keep our project architecture clean by organizing setup variables in the Create event and placing animation logic at the very end of the Step event, right after velocity and collision routines from Module 5 finish resolving.

Step 1: Asset setup. In the Asset Browser, ensure you have the following sprites configured with origins set to 'Bottom Centre': - 'spr_player_idle': Breathing or resting pose (2 to 4 frames, speed: 6 FPS). - 'spr_player_run': Complete run cycle (4 to 8 frames, speed: 12 FPS). - 'spr_player_jump': Airborne sprite with 2 frames (frame 0: rising; frame 1: falling).

Step 2: Initialize variables in the 'obj_player' Create event. Add tracker variables to monitor animation states and speed modifiers.

/// obj_player Create Event

// Movement physics variables (established in earlier modules)
hsp = 0;
vsp = 0;
grav = 0.3;
move_speed_max = 3.5;
jump_force = -7.0;
grounded = false;

// Variables added for animation control
footstep_trigger = false;
animation_base_speed = 1.0;

Step 3: Integrate into the 'obj_player' Step event. Append a dedicated block ('Animation System') at the end of the Step script, leveraging the final values of 'grounded', 'hsp', and 'vsp'.

/// End of obj_player Step Event - Animation and Sprite Control

// 1. GROUNDED CHARACTER MANAGEMENT
if (grounded)
{
    // If there is no significant horizontal speed, display idle
    if (abs(hsp) < 0.1)
    {
        if (sprite_index != spr_player_idle)
        {
            sprite_index = spr_player_idle;
            image_index = 0;
        }
        image_speed = 1.0; // Standard idle animation speed
    }
    else
    {
        // If moving horizontally, transition to run
        if (sprite_index != spr_player_run)
        {
            sprite_index = spr_player_run;
            image_index = 0;
        }
        
        // Scale animation speed proportionally to movement speed
        // Clamp to a minimum of 0.3 so the animation doesn't freeze when starting out
        var _speed_ratio = abs(hsp) / move_speed_max;
        image_speed = max(0.3, _speed_ratio);
    }
}
// 2. AIRBORNE CHARACTER MANAGEMENT
else
{
    if (sprite_index != spr_player_jump)
    {
        sprite_index = spr_player_jump;
    }
    
    // Freeze automatic playback to set frames manually
    image_speed = 0;
    
    // Sub-image 0 during rise, sub-image 1 during fall
    if (vsp < 0)
    {
        image_index = 0;
    }
    else
    {
        image_index = 1;
    }
}

💡 Note on GML Visual: Developers using visual action blocks can replicate this system using 'Set Instance Variable' blocks for 'Sprite' and 'Image Index', paired with conditional 'If Variable' checks to evaluate ground status and speeds.

Hands-On Exercises

Complete these practice exercises to reinforce and test your mastery of the concepts covered in this lesson.

Exercise 1: Reverse speed modulation. Modify the run animation logic so that if the player is moving in one direction but inputs the opposite direction (skidding before stopping), the run animation speed is halved (0.5 multiplier) to visually convey sudden braking.

Exercise 1 Verification: Move at full speed and tap the opposite direction; the character's leg animation should noticeably slow down before settling into the idle pose.

Exercise 2: Precise landing impact. Add a third sub-image (index 2) to 'spr_player_jump' depicting the character crouching upon impact. In the Step event, detect the exact frame the character transitions from airborne to grounded, and hold frame 2 for 4 game steps before allowing the transition to 'spr_player_idle'.

Exercise 2 Verification: Dropping from a high ledge should trigger a momentary crouch/squash pose on impact rather than snapping instantly to the upright idle stance.

Exercise 3: Console keyframe logging with show_debug_message. Update 'spr_player_run' to cleanly detect when the right foot touches down (for example, frame 1). Using 'show_debug_message("Right step")', verify in GameMaker's Output window that the message logs exactly once per run cycle without spamming during the frame's duration.

Exercise 3 Verification: In GameMaker's Output console, the message must appear at a steady, rhythmic cadence matching the run animation, without flooding the console with duplicate lines within a single second.

Summary

  • GameMaker sprites consist of sequential sub-images indexed from zero (0, 1, 2, ... N-1).
  • Key animation variables include 'sprite_index' (asset), 'image_index' (current frame), 'image_speed' (playback multiplier), and 'image_number' (total frame count, read-only).
  • When switching sprites via code, always verify 'if (sprite_index != new_sprite)' and reset 'image_index = 0' to prevent stutter and frame skipping.
  • Setting 'image_speed = 0' stops automatic animation cycles, allowing precise manual frame selection (ideal for jump/fall poses driven by 'vsp').
  • Modulating 'image_speed' dynamically with horizontal velocity removes foot skating and gives 2D platformer movement a polished, physical feel.

Practise what you learned

Exercise 1 · 1 point

What happens if you attempt to modify the built-in variable `image_number` directly in GML Code?

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

Exercise 2 · 2 points

Complete the body of the conditional statement to safely switch to `spr_player_run` and reset the animation so it starts cleanly from its first frame.

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.

Exercise 3 · 1 point

Match each built-in GameMaker variable with its corresponding role in animation playback:

  • sprite_index
  • image_index
  • image_speed
  • image_number
You are practising without an account: nothing is saved. Sign up to keep your progress.

Sign in to save your progress — Sign in