Introducción al Desarrollo de Videojuegos con GameMaker · Module 10: Level Design, Cameras, and Game Progression

Managing Persistent Data Across Levels

Lesson 49 of 60 · 55 min

Also available in: ES

The Persistence Challenge: Why Does Data Disappear Between Levels?

In previous lessons, you learned how to build interactive rooms, place obstacles, and configure portals or doors that transport the player character from one room to another using the `room_goto()` function. However, when testing that transition for the first time, a classic game development problem emerges: the player collects coins in the first level, earns 350 points, walks through the exit door into level two, and suddenly their score drops back to zero while their lives reset to default. This behavior often baffles beginners, but it stems from a fundamental GameMaker operating principle: room memory cleanup.

By default, whenever GameMaker executes a room transition, it automatically destroys all active instances in the departing room to free system RAM. That means your player object (`obj_jugador`), enemies, collectibles, and UI panels vanish entirely. When the new room loads, the engine parses the layout you configured in the Room Editor and creates brand-new instances from scratch. As they instantiate, each object runs its Create event again, resetting any internal variables to factory defaults. If you wrote `puntos = 0;` and `vidas = 3;` in your player's Create event, every new level inevitably resets the user's hard-earned progress.

To build a cohesive gameplay experience where players feel continuous progression across multiple worlds and challenges, you must decouple vital data from the transient physical instances in each level. There are two primary approaches in GameMaker to solve this problem: making specific objects persistent, or storing critical data in global-scoped variables. Throughout this lesson, we will examine both techniques, focusing especially on global variables—the cleanest, most predictable, and easiest-to-debug industry standard for both beginners and experienced developers.

Variable Scopes in GameMaker: Local, Instance, and Global

To master persistence, you must clearly understand how GameMaker categorizes and isolates memory using variable scopes. A variable is simply a named container that holds a value in memory; its scope dictates which parts of your code have permission to read or modify that container, as well as how long it survives during the game's lifecycle.

Variable TypeSyntax DeclarationScope / VisibilityLifespan
Local (Temporary)var mi_variable = 10;Strictly within the event, function, or script where it was declared.Destroyed the instant the event or script finishes executing for that step.
Instancemi_variable = 10;Accessible by the owning instance and externally via 'id.variable'.Remains active as long as the instance exists; destroyed on room change or death.
Globalglobal.mi_variable = 10;Accessible from any object, script, function, or event across the entire game.Persists indefinitely throughout the entire game session until closed.

Local variables (declared with the `var` keyword) are ephemeral and used for short calculations within a single step. Instance variables belong to a specific object instance: if you have three coins on screen, each possesses its own `x` and `y` instance variables. If a coin is destroyed, its variables disappear with it. Global variables, on the other hand, do not belong to any physical entity in the room; they attach directly to the engine's core memory space. This independence makes them the ideal mechanism for tracking remaining health, player inventory, accumulated currency, and high scores.

Conceptual diagram illustrating how instance variables are destroyed upon changing rooms, while global variables remain accessible between Room 1 and Room 2.
Figure 1: Comparison of lifespans between volatile room memory and uninterrupted global storage. · AI-generated image

Strategies for Initializing Global Variables

One of the most common pitfalls when working with global variables is initializing them inside the Create event of an object that respawns in every room, such as the player or a HUD controller. If you place `global.puntuacion = 0;` inside `obj_jugador`'s Create event, every time a new level loads and the player spawns, that line executes and overwrites accumulated score back to zero. To prevent this, global variables must be initialized exactly once during game startup.

In modern GameMaker versions, there are two secure, professional ways to guarantee a single, bulletproof initialization: using an auto-executing configuration script, or routing startup through a dedicated initialization room (`rm_init`). Let's examine both so you can pick the right fit for your project structure.

Method 1: Declaration in an Initialization Script (Recommended in Modern GameMaker)

Any code written at the root of a GML script file (outside of any explicit `function` declaration) executes automatically the very moment the game boots, before the first room even renders on screen. This behavior makes scripts ideal for declaring all initial global states in a clean, centralized configuration file.

Create a new Script asset in the Asset Browser and name it `scr_game_init`. Inside, add the following foundational configuration code:

// Script: scr_game_init
// This code executes once when the game boots up.

// Player progression and state
global.vidas = 3;
global.vidas_maximas = 3;
global.puntuacion = 0;
global.monedas = 0;

// Level control tracking
global.nivel_actual = 1;
global.juego_pausado = false;

// Debug log in the output console
show_debug_message("Global system initialized successfully: Lives = " + string(global.vidas));

By prefixing these variables with `global.`, GameMaker allocates dedicated global memory immediately. From that point on, any room or object can read or write `global.puntuacion` without any risk of accidental reinitialization during level transitions.

Method 2: Dedicated Startup Room (rm_init)

The second method, frequently seen in larger games with complex title menus, uses a lightweight, empty room placed at the absolute top of the room list in the Asset Browser, conventionally named `rm_init` or `rm_preload`. This room contains only one instance: a manager object (`obj_game_manager`). In its Create event, this manager initializes your global data and immediately transitions to the title screen or Level 1 using `room_goto(rm_nivel_1)`.

💡 Golden Rule: Whichever method you choose, never initialize a persistent global variable inside the Create event of your player object or any entity placed directly within regular gameplay rooms.

The Central Controller Object: Creating obj_game_manager

Although global variables float independently in engine memory, software architecture best practices recommend having a dedicated controller object to handle overarching game logic: checking if lives have reached zero to trigger Game Over, firing celebration sound effects on high scores, or orchestrating scene transitions. We will name this object `obj_game_manager`.

Create a new object in the Asset Browser and call it `obj_game_manager`. You do not need to assign it a sprite, as it acts as an invisible coordinator. In its Object Properties panel, make sure to check the **Persistent** box.

When an object's **Persistent** flag is checked, GameMaker alters its default lifecycle: calling `room_goto()` will no longer destroy this instance. Instead, it travels smoothly into the incoming room, maintaining all its internal variables and running its events without interruption.

// Object: obj_game_manager
// Event: Create

// Check if another manager already exists to prevent duplicates
if (instance_number(obj_game_manager) > 1)
{
    instance_destroy();
    exit;
}

// Object will survive room transitions thanks to the Persistent property
persistent = true;

// Controller local tracking variable
tiempo_en_nivel = 0;

The `if (instance_number(obj_game_manager) > 1)` check is a simple Singleton safeguard. If you accidentally place an `obj_game_manager` in Level 1 and another in Level 2 in the Room Editor, the duplicate detects an existing manager arriving from the previous room and promptly destroys itself, preventing logic collisions.

Rewriting Collectibles: Adding to Global Memory

In earlier exercises, collecting a coin might have modified an instance variable directly on the player (`puntos += 10;`). Now that our global persistence system is ready, we need to update our collectible items (`obj_moneda`) so they write directly to global memory. This ensures collected points outlive the room in which they were picked up.

Open `obj_moneda`. If handling pickup collisions on the coin itself against `obj_jugador`, navigate to its Collision Event with `obj_jugador` and insert the following code:

// Object: obj_moneda
// Event: Collision with obj_jugador

// Increment global variables directly
global.monedas += 1;
global.puntuacion += 100;

// Bonus check: 100 coins award an extra life
if (global.monedas >= 100)
{
    global.monedas -= 100;
    global.vidas += 1;
    
    // Play extra life audio cue (if available)
    if (audio_exists(snd_vida_extra))
    {
        audio_play_sound(snd_vida_extra, 10, false);
    }
}

// Play standard coin collection sound
if (audio_exists(snd_moneda))
{
    audio_play_sound(snd_moneda, 5, false);
}

// Destroy this coin instance so it cannot be collected twice
instance_destroy();

Notice the architecture: when collecting a coin, it no longer matters whether the player reaches an exit door two seconds later or triggers a room shift immediately. The 100 points and the collected coin now live safely in global memory, ready to be read by any future room.

Handling Damage, Lives, and Respawning

The next critical integration is damage and lives handling. Previously, if the player fell into a hazard or touched an enemy, calling `room_restart()` often reset lives back to 3 because the player's Create event executed again. By tying health to `global.vidas`, restarting a room restores visual elements and obstacle positions while keeping the life deduction intact.

Let's update the event where the player takes fatal damage (for instance, inside `obj_jugador`'s Collision Event with `obj_trampa_pinchos` or `obj_enemigo`):

// Object: obj_jugador
// Event: Collision with obj_peligro (or hazard boundary)

// Deduct one life from global storage
global.vidas -= 1;

// Check if the player still has remaining lives
if (global.vidas > 0)
{
    // Play hit or fall sound
    if (audio_exists(snd_golpe))
    {
        audio_play_sound(snd_golpe, 8, false);
    }
    
    // Restart current room to retry the level
    room_restart();
}
else
{
    // Player has run out of lives: Game Over
    if (audio_exists(snd_game_over))
    {
        audio_play_sound(snd_game_over, 10, false);
    }
    
    // Navigate to Game Over screen
    room_goto(rm_gameover);
}

Notice what happens: when `global.vidas > 0` and GameMaker runs `room_restart()`, the level reloads cleanly, recreating crates and repositioning enemies. However, because `global.vidas` sits outside room scope, the player respawns at their starting marker with their remaining life count intact (e.g., 2 lives instead of 3). Persistence works both ways: it protects earned rewards and enforces penalties.

Updating the HUD: Synchronized Display

When designing a user interface object like `obj_hud`, beginners often draw text and heart icons by reading directly from the player instance, using statements like `obj_jugador.puntos`. This introduces two severe vulnerabilities: if the player instance is destroyed during a death animation or level fade, the HUD throws a runtime crash (`Unable to find instance...`). Furthermore, it tightly couples UI rendering to the player's physical presence.

By migrating all game stats to global variables, your HUD becomes completely autonomous. It can render cleanly even in cutscene rooms or victory screens where the player object does not exist. Open `obj_hud` and configure its **Draw GUI** event to pull values directly from global scope:

// Object: obj_hud
// Event: Draw GUI

// Text appearance settings
draw_set_font(fnt_interfaz);
draw_set_color(c_white);
draw_set_halign(fa_left);
draw_set_valign(fa_top);

// Pixel offset from top-left screen corner
var margen_x = 32;
var margen_y = 24;

// Render global metrics
draw_text(margen_x, margen_y, "SCORE: " + string(global.puntuacion));
draw_text(margen_x, margen_y + 28, "COINS: " + string(global.monedas));

// Render lives label and icons
draw_text(margen_x, margen_y + 56, "LIVES: ");

for (var i = 0; i < global.vidas; i++)
{
    // Draw heart icon for each active life
    draw_sprite(spr_icono_corazon, 0, margen_x + 90 + (i * 32), margen_y + 60);
}

// Reset alignment to engine defaults
draw_set_halign(fa_left);
draw_set_valign(fa_top);

With this decoupling, no matter where the camera moves or whether the screen is fading between levels, the HUD always has instant, safe access to live game stats without risking null pointer crashes or out-of-sync values.

Persistent Objects vs. Global Variables: Architectural Trade-Offs

Earlier we noted that objects can also be made persistent by checking their **Persistent** box. Novice developers often attempt to solve room transitions by simply marking `obj_jugador` as persistent. While this appears convenient at first glance, in 2D platformers it frequently creates severe architectural headaches if not managed with extreme care:

ApproachKey AdvantagesCommon Risks & Drawbacks
Global Variables + ControllerData is 100% isolated and protected. Levels can be freely composed in the Room Editor. Easy to debug and save to disk.Requires prefixing with 'global.' or organizing data inside structured structs.
Persistent Player (obj_jugador)Automatically retains all internal variables without converting them into global scope.Player spawns in the next room at the exact (x, y) coordinates from the previous room, often embedding them inside walls or midair.

If your player object is persistent, when leaving `rm_nivel_1` at position (x: 1800, y: 300) and entering `rm_nivel_2`, the player lands at `x = 1800, y = 300` in the new room, regardless of where you intended the level entrance to be. Fixing this requires writing manual teleportation and repositioning scripts for every room entrance. For this reason, industry standard practice for 2D platformers keeps rooms and player instances non-persistent—allowing precise placement via GameMaker's Room Editor—while delegating persistent state to global variables or an invisible persistent controller.

Step-by-Step: Complete Level-Transition Workflow with Persistence

Let's connect all these components into an end-to-end operational workflow. Here is how an exit door, the global controller, and room loading interact to ensure zero data loss along the way:

Full Game Reset on Game Over or Victory

Because global variables survive throughout the game's entire session, there comes a point where you do want to clear them: when the player exhausts all lives and returns to the title menu, or clicks 'Retry'. If you do not reset global values, starting a new game will inherit zero lives or retain scores from the previous defeat.

To handle this cleanly, define a reset function inside a script accessible across all UI buttons and menu screens. Add this function to the bottom of `scr_game_init`:

// Script: scr_game_init (Add to the end of the file)

/// @function reiniciar_partida()
/// @description Resets all global variables back to their new-game defaults.
function reiniciar_partida()
{
    global.vidas = 3;
    global.puntuacion = 0;
    global.monedas = 0;
    global.nivel_actual = 1;
    
    // After clearing global memory, navigate to the first playable room
    room_goto(rm_nivel_1);
}

Now, on the 'Retry' or 'Play Again' button in your end-of-game screen, simply execute `reiniciar_partida();` inside the appropriate button press or mouse click event. Your game's data lifecycle is now fully closed and under complete control.

Practical Exercises

Put your persistence management skills into practice by completing the following three exercises in your retro platformer project.

Summary

  • By default, GameMaker destroys active instances and their instance variables during room transitions with room_goto() to free memory.
  • Instance variables only exist while their parent instance is alive, whereas global variables (prefixed with 'global.') persist in memory throughout the entire game session.
  • Global variables should be initialized exactly once, ideally inside an auto-executing script (scr_game_init) or a dedicated startup room (rm_init).
  • Marking objects as 'Persistent' allows them to survive room changes, but applying this to player objects often introduces coordinate misalignment bugs.
  • Decoupling HUD displays and scoring logic from physical instances prevents runtime crashes and ensures smooth cross-level progression.

Practise what you learned

Exercise 1 · 1 point

Match each variable scope in GameMaker with its behavior and lifecycle according to the lesson:

  • Local variables
  • Instance variables
  • Global variables
You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 2 · 1 point

Why is it a mistake to initialize a variable such as `global.score = 0;` inside the player object's Create event (`obj_player`)?

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

Exercise 3 · 1 point

Complete the code in the Create event of `obj_game_manager` to prevent duplicate instances and ensure it persists across room transitions: ```gml if (instance_number(obj_game_manager) > ) { instance_destroy(); exit; } persistent = ; ```

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

Sign in to save your progress — Sign in