Quick answer: Global variables in GameMaker are not declared automatically — reading global.score before any code assigns it returns undefined. Initialize all globals in a persistent controller object or a script that runs before any game object, and use variable_global_exists() to guard against race conditions.

You set global.score = 0 in the Create event of your game controller object, then try to read it in the Create event of your HUD object and get undefined. Or you set it fine, but one specific enemy type always crashes with “Variable global.score not set before reading it.” Global variable bugs in GameMaker are common, and they all share the same root cause: GameMaker’s global scope is a flat dictionary that is only populated when code explicitly assigns to it — there is no forward declaration, no type system, and no guaranteed initialization order unless you design one.

Global Variables vs Instance Variables

Before debugging, it’s worth being precise about what “global” means in GML. An instance variable is a value that belongs to a specific object instance:

// In obj_player Create event
score = 0;      // instance variable — belongs to this obj_player instance only
lives = 3;

If you have two instances of obj_player, each has its own score and lives. To read another instance’s variable you need an instance ID: other_player.score. A global variable, by contrast, lives in a single shared scope accessible from everywhere:

// In obj_controller Create event
global.score     = 0;    // global — one copy shared by everything
global.high_score = 0;
global.current_level = 1;

Any object, any instance, any script can read and write global.score without needing a reference. The trade-off is that there is only ever one value — and if nobody has assigned it yet, reading it returns undefined rather than throwing an immediately obvious error.

The Execution Order Problem

GameMaker executes Create events for room objects in layer order, then within a layer in the order objects were placed in the room editor. This order is deterministic but easy to get wrong. If obj_hud is on a layer above obj_controller, or was placed in the room before it, its Create event runs first — before obj_controller has assigned any global variables.

“Layer order in the Room Editor determines Create event execution order. Objects on higher layers (lower index) run their Create events first.”

This is the exact scenario that produces intermittent undefined bugs: the game works fine when you test it in isolation but fails after you reorder layers in the Room Editor, or when you add a new object to a layer that happens to run before your controller.

// obj_hud Create event — runs BEFORE obj_controller if hud is on a higher layer
var display_score = global.score;    // undefined! obj_controller hasn’t run yet
draw_text(32, 32, "Score: " + string(display_score));

Using variable_global_exists() as a Guard

The safest way to read a global variable when you’re not 100% certain it has been initialized is to check with variable_global_exists() first. This function returns true if the global has been assigned at least once, false otherwise:

// Defensive read in obj_hud Create event
if (variable_global_exists("score"))
{
  hud_score = global.score;
}
else
{
  hud_score = 0;    // fallback default
  show_debug_message("WARNING: global.score not initialized when obj_hud created");
}

The show_debug_message call will appear in the Output console during development, flagging the race condition without crashing the game. Remove or convert it to a silent log before release.

The globalvar Keyword vs global. Prefix

Older GML tutorials and imported projects from GameMaker: Studio 1 often use the globalvar syntax:

// Legacy syntax (GMS1 era) — still valid but discouraged
globalvar score, high_score, current_level;
score = 0;
high_score = 0;

The globalvar keyword declares a variable name as global so it can be referenced without the global. prefix. This looks convenient but creates confusion: reading score inside a script looks identical to reading a local variable named score, making it easy to shadow globals accidentally. The modern global. prefix is always explicit. Use it for all new code and migrate away from globalvar when touching existing files.

The Right Pattern: Persistent Controller Object

The cleanest solution to global variable initialization is a persistent controller object that runs before everything else and is never destroyed between rooms. Create an object called obj_game_controller, set it as Persistent in the object properties, and place it on the very first layer in your first room. Its Create event becomes the single source of truth for all global initialization:

// obj_game_controller Create event
// This runs once, before all other objects in the room.
// Because it’s persistent, it survives room transitions.

global.score          = 0;
global.high_score     = 0;
global.current_level  = 1;
global.player_lives   = 3;
global.difficulty     = "normal";
global.sfx_volume     = 1.0;
global.music_volume   = 0.8;
global.save_slot      = 0;

Place obj_game_controller on the topmost layer (Layer 0 or the layer with the lowest depth value) in the Room Editor. GameMaker processes lower depth values first, so this object’s Create event will fire before every other object in the room. Because it’s persistent, it carries all global state intact when the player transitions to the next room — no re-initialization required.

If you prefer not to use a runtime object for initialization, you can use a Script asset as a pseudo-module that other Create events call explicitly:

// scr_init_globals script asset
function scr_init_globals()
{
  if (!variable_global_exists("score"))
  {
    global.score         = 0;
    global.high_score    = 0;
    global.current_level = 1;
    global.player_lives  = 3;
  }
}

// Call at the top of every object’s Create event that needs globals
scr_init_globals();

The variable_global_exists guard inside the script makes it idempotent — calling it multiple times is safe and will not reset globals mid-game. This pattern scales well to large projects where multiple rooms have different starting objects.

Debugging undefined in a Live Build

If players are reporting crashes with “Variable global.X not set before reading it” in a shipped build, you need to know which object is reading the variable and when. GameMaker’s YYC runtime will crash with that message rather than returning undefined, making the issue more visible in production than in the VM runner. Add breadcrumb logging around global reads in any object that accesses globals during its Create event, and capture those logs in your bug reporting pipeline. Bugnet’s in-game SDK lets you attach custom metadata — including GML variable dumps — to crash reports so you can see exactly what state the game was in when the variable was accessed.

Global variables aren’t magic — they’re just a dictionary that someone has to fill before anyone reads from it.