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

Building a Visual State Machine

Lesson 27 of 60 · 60 min

Also available in: ES

Introduction to the Visual State Machine

In previous modules, you implemented horizontal movement, acceleration, gravity, ground detection (`grounded`), and a solid collision system along both axes. However, a character that glides statically across the screen or whose animations flicker uncontrollably lacks the tactile and aesthetic polish essential to a retro 2D platformer. To make gameplay feel responsive and lively, your character's visual presentation must accurately reflect underlying physics and mechanics on every single frame.

A common beginner pitfall is hardcoding sprite assignments directly into keyboard checks (such as `if (keyboard_check(vk_right)) sprite_index = spr_player_run;`). This approach quickly devolves into spaghetti code: sprites flicker, jump animations cancel prematurely, characters run in midair, or frames freeze whenever opposing keys are pressed at the same time. To resolve this cleanly and scalably in GameMaker, we will build a dedicated Visual Finite State Machine (visual FSM).

Concept: Finite State Machines (FSM) Applied to Animation

A Finite State Machine is a computational model stipulating that an entity can occupy exactly one state from a finite set at any given time. In our context, the character cannot be running and idling simultaneously, nor can it be in a rising fall state. Driven by input conditions—such as horizontal speed `hsp`, vertical speed `vsp`, and floor contact `grounded`—the machine transitions between states and updates relevant visual properties (`sprite_index`, `image_speed`, and `image_xscale`).

In modern GameMaker, the cleanest, most performant way to represent states is using enumerators (`enum`). An enum maps descriptive names to sequential integers under the hood. This eliminates slow, typo-prone string comparisons (like `state = "running"`) and allows the compiler to evaluate state checks instantly.

Schematic diagram of a finite state machine illustrating logical transitions between Idle, Run, Jump, and Fall states based on ground contact and horizontal/vertical velocities.
State transition flow diagram driven by velocities and ground contact. · AI-generated image

Standardizing Graphic Assets and Collisions

Before writing any visual logic, you must follow a golden rule of 2D platformer development: maintain spatial and dimensional consistency across all sprites assigned to a single object. When you change an instance's `sprite_index`, GameMaker automatically swaps the default collision mask and origin to match the new sprite unless configured otherwise. If your running sprite is 32 pixels tall with a bottom-centered origin, but your jump sprite is 40 pixels tall with a centered origin, the character will sink into the floor or float mid-jump, completely breaking collision consistency.

Sprite AssetContent & SubimagesOrigin PointCollision Mask Setup
spr_player_idle4 to 6 frames (breathing or bobbing)Bottom Centre (x: centered, y: bottom)Manual or Same as Mask (identical Bounding Box)
spr_player_run6 to 8 frames (running cycle)Bottom Centre (x: centered, y: bottom)Identical size and offset as idle
spr_player_jump1 to 2 frames (upward impulse)Bottom Centre (x: centered, y: bottom)Identical size and offset as idle
spr_player_fall1 to 2 frames (free fall)Bottom Centre (x: centered, y: bottom)Identical size and offset as idle

💡 Critical Rule: To prevent your character from getting stuck inside platforms or walls when swapping sprites, assign a dedicated collision sprite in the 'Collision Mask' property of 'obj_player' (for instance, a simple rectangle named 'spr_player_mask'), or ensure every animation sprite shares the exact same origin and bounding box dimensions down to the pixel.

Variable Setup in the Create Event

Inside the Create event of `obj_player`, you already have kinematic variables from earlier modules (`hsp`, `vsp`, `walk_spd`, `grav`, `jump_spd`, `grounded`). Now, define your state enumerator, the variable tracking the active state, and a variable tracking the previous frame's state. The previous-state variable allows you to detect the exact frame a transition occurs so you can reset `image_index` to zero, preventing animations from starting midway through their loops.

// ----------------------------------------------------------------------------
// OBJECT: obj_player
// EVENT: Create
// ----------------------------------------------------------------------------

// Movement and physics variables (established in modules 4 and 5)
hsp = 0;
vsp = 0;
walk_spd = 4;
grav = 0.4;
jump_spd = -8.5;
grounded = false;

// Player visual state definitions
enum PLAYER_STATE {
    IDLE,
    RUN,
    JUMP,
    FALL
}

// Current and previous state initialization
state = PLAYER_STATE.IDLE;
state_previous = PLAYER_STATE.IDLE;

// Horizontal sprite facing (1 = right, -1 = left)
facing = 1;

Notice that we declare `facing = 1`. While GameMaker provides the built-in variable `image_xscale` to flip instance rendering, storing your intended direction in an explicit helper variable (`facing`) before assigning it decouples raw player input from final rendering and prevents unintended scaling bugs.

Determining State in the Step Event

The Step event execution flow must follow a strict sequence of stages. Changing sprites while resolving collisions leads to inconsistent collision data. The recommended canonical pipeline is:

StagePrimary ActionVariables Involved
1. InputPoll player key presseskey_left, key_right, key_jump
2. Physics LogicCalculate horizontal acceleration and gravitymove_dir, hsp, vsp
3. Collisions & TranslationResolve wall collisions and update 'x', 'y'x, y, grounded
4. State EvaluationDetermine corresponding state based on physicsstate
5. Visual AssignmentAssign sprites, reset frames, and flip facingsprite_index, image_index, image_xscale

Here is the complete state evaluation and visual assignment block. Place it at the end of the Step event in `obj_player`, immediately after resolving horizontal and vertical collisions with floors and walls.

// ----------------------------------------------------------------------------
// OBJECT: obj_player
// EVENT: Step (Final section: Visual State Machine)
// ----------------------------------------------------------------------------

// --- STAGE 4: STATE EVALUATION BASED ON PHYSICS CONDITIONS ---
if (grounded) {
    // If the player is on a solid surface
    if (hsp == 0) {
        state = PLAYER_STATE.IDLE;
    } else {
        state = PLAYER_STATE.RUN;
    }
} else {
    // If the player is airborne (not grounded)
    if (vsp < 0) {
        state = PLAYER_STATE.JUMP;
    } else {
        state = PLAYER_STATE.FALL;
    }
}

// --- STAGE 5: VISUAL APPLICATION AND ANIMATION CONTROL ---
// If the state changed this frame, reset the animation index
if (state != state_previous) {
    image_index = 0;
}

// Sprite selection based on the evaluated state
switch (state) {
    case PLAYER_STATE.IDLE:
        sprite_index = spr_player_idle;
        image_speed = 1.0;
        break;
        
    case PLAYER_STATE.RUN:
        sprite_index = spr_player_run;
        // Optional tweak: sync animation speed with actual movement speed
        image_speed = 1.0;
        break;
        
    case PLAYER_STATE.JUMP:
        sprite_index = spr_player_jump;
        image_speed = 1.0;
        break;
        
    case PLAYER_STATE.FALL:
        sprite_index = spr_player_fall;
        image_speed = 1.0;
        break;
}

// --- HORIZONTAL ORIENTATION (FACING) ---
// Only update facing if the player is actively moving
if (hsp != 0) {
    facing = sign(hsp);
}

// Apply horizontal visual scale while preserving the base magnitude
image_xscale = facing;

// Update previous state record for the next frame
state_previous = state;

In-Depth Analysis of the State Assignment Logic

Let us break down the architectural choices in the code above to understand why each structure is used:

1. **Grounded vs. Airborne Hierarchy:** The primary branch evaluates `grounded`. When the player is in the air, pressing movement keys is irrelevant to base posture—vertical kinematics take precedence. Isolating airborne states from grounded states eliminates the classic glitch where a character loops a running animation while plummeting off a ledge.

2. **Distinguishing Jump and Fall by Velocity Sign:** In GameMaker, vertical coordinates increase downward, so upward momentum produces a negative vertical speed (`vsp < 0`), while gravity creates a positive speed (`vsp >= 0`). Splitting `vsp < 0` (rising arc) from `vsp >= 0` (falling arc) lets you display a dedicated downward pose that makes airborne motion look much more fluid without adding complex tracking logic.

3. **The `state != state_previous` Check:** This is the key mechanism for eliminating animation flicker. If a sprite has 6 frames and you set `image_index = 0` every step without checking for state changes, GameMaker forces frame 0 every tick, freezing the animation on a single pose. By evaluating `image_index = 0` only when `state` differs from `state_previous`, you ensure each animation starts cleanly at frame 0 and loops normally thereafter.

4. **Using `sign()` for Facing Direction:** The built-in `sign(val)` returns `1` for positive numbers, `-1` for negative numbers, and `0` for zero. By checking `if (hsp != 0) facing = sign(hsp);`, you ensure that when the player comes to a halt (`hsp == 0`), `facing` retains its last valid orientation (1 or -1). This avoids collapsing the sprite's width to zero and rendering it invisible.

Practical Optimization and Troubleshooting Cases

Hands-On Practice Exercises

Complete the following three exercises in your project to validate and refine your visual state machine architecture:

**Exercise 1: Implement a Dedicated Fall State.** In GameMaker's Sprite Editor, import or create a 32x32 sprite named `spr_player_fall` with a 'Bottom Centre' origin. Give the character a distinct falling silhouette (arms raised or cape billowing upward). Wire the state machine so that as the player reaches the apex of a jump (`vsp >= 0`), the sprite transitions smoothly into `spr_player_fall` before landing. **Validation Criteria:** When jumping, you should see a clear transition from `spr_player_jump` on the way up to `spr_player_fall` as soon as descent begins, landing cleanly into `spr_player_idle` or `spr_player_run` on the exact frame the character touches the floor.

**Exercise 2: Dynamically Sync Run Animation to Ground Speed.** In the `PLAYER_STATE.RUN` branch of your Step event, replace the fixed `image_speed = 1.0;` with a dynamic value proportional to the absolute horizontal velocity (`abs(hsp)`). When moving slowly (such as light gamepad tilt or gentle deceleration), the footwork should slow down; at top speed (`walk_spd`), it should play at standard rate. **Validation Criteria:** Use the formula `image_speed = abs(hsp) / walk_spd;`. Verify that slowing down smoothly lowers step frequency without snapping to a dead halt until the character completely stops.

**Exercise 3: On-Screen Debug State Overlay.** Open `obj_player`'s 'Draw GUI' event and use `draw_text()` to print the current state name in readable text in the top-left corner of the screen, alongside `hsp`, `vsp`, `grounded`, and `image_index`. **Validation Criteria:** Run your game and confirm the overlay text matches the character's visual pose on every frame. Ensure there are no erratic state flickers while running, jumping, walking off ledges without jumping, or pushing against solid walls.

Summary

  • A visual Finite State Machine (FSM) prevents animation glitches and flickering by decoupling physics checks from sprite rendering.
  • Using enums in GML Code provides clean, descriptive, and performant symbolic identifiers for each character pose (IDLE, RUN, JUMP, FALL).
  • All sprites assigned to a single instance must share consistent origin points ('Bottom Centre') and collision bounding boxes to prevent snagging on geometry.
  • Checking 'state != state_previous' is required to reset 'image_index = 0' strictly when entering a new state, preserving smooth playback.
  • Sprite facing is handled cleanly by passing 'sign(hsp)' to an explicit 'facing' variable applied to 'image_xscale' only when horizontal movement is non-zero.

Practise what you learned

Exercise 1 · 1 point

Why is it necessary to check the condition `state != state_previous` before resetting `image_index = 0` in the Step event?

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

Exercise 2 · 1 point

Complete the following code snippet from the evaluation phase in the Step event to determine the visual state when the character is airborne: ```gml if (grounded) { // Ground logic } else { // If the player is airborne (not grounded) if ( < 0) { state = PLAYER_STATE.JUMP; } else { state = PLAYER_STATE.; } } ```

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

Exercise 3 · 1 point

Arrange the execution blocks of the visual state machine in chronological order within the Step event, according to the design presented in the lesson:

  1. 1Assign the corresponding sprite_index and image_speed using the switch (state) structure.
  2. 2Save the current state into state_previous for the next frame.
  3. 3Update the facing variable based on movement and assign it to image_xscale.
  4. 4Evaluate physical conditions (grounded, hsp, vsp) to assign the value of state.
  5. 5Check if state != state_previous to reset image_index to zero on a state transition.
You are practising without an account: nothing is saved. Sign up to keep your progress.

Sign in to save your progress — Sign in