Quick answer: Room End does not fire on persistent objects during room transitions because the instance is not actually leaving the world. It also fails to fire if the instance is destroyed before the transition or if room_goto is called from Game End. Move cleanup logic to the Cleanup Event, manually call event_perform(ev_other, ev_room_end) for persistent objects, or unset the persistent flag.

Here is how to fix GameMaker Room End event that never fires. You wrote a clean teardown routine in Room End: free your data structures, save player state, log the room exit. The first time you transition rooms, none of it runs. Player state is lost, ds_lists leak memory, and the log shows the transition happened but no exit message. The Room End event seems broken — but it is functioning exactly as documented. The issue is that “end of room” means something different from what most developers expect, especially when persistent objects are involved.

The Symptom

The Room End event on an object never fires when transitioning to another room via room_goto, room_goto_next, or room_restart. The instance survives the transition (or is destroyed and recreated) but the event code never runs.

Persistent controllers miss cleanup. A global game-state controller marked persistent transitions from the menu room to the gameplay room. Its Room End event was supposed to free a temporary ds_map used during menu navigation. The event never runs, the map leaks every time the player navigates away.

State save attempts fail silently. The Room End event on a level controller writes the current room’s player state to a save file. After transition the file is unchanged. The transition completed, the new room loaded, the player has spawned — but the save never happened.

Logging shows the gap. A show_debug_message at the top of Room End never appears in the output console, while show_debug_message in Room Start of the destination room does appear. The transition is happening; Room End is just being skipped for that particular instance.

Inconsistent behaviour. Some objects fire Room End, others do not. The pattern correlates with the persistent flag — non-persistent objects fire it, persistent ones do not.

What Causes This

Persistent objects do not raise Room End during transitions. The Room End event is defined as “the instance is leaving the room”. Persistent instances are not leaving — they are being kept alive across the transition. The engine optimises this case by skipping the event entirely. There is no flag to override this behaviour; it is intrinsic to how persistence works.

room_goto is deferred to end-of-step. Calling room_goto(rm_next) does not change rooms immediately. The current step finishes, End Step runs, the engine fires Room End on non-persistent instances, then the room changes. If you destroy the controller before End Step, no Room End fires for it. If you call game_end() after room_goto on the same step, the engine prioritises Game End and skips Room End entirely.

Game End cancels Room End. When game_end() is called, the engine triggers the Game End event on every instance and skips Room End even if a transition was queued. If your shutdown path goes through a brief room change followed by game exit, the Room End fires only for the original room, not for any intermediate one.

Instance destroyed first. If the instance has been removed via instance_destroy before the room transitions, its Room End never fires because the event hierarchy stops at Cleanup » Destroy » Room End. Once destroyed, the instance is gone before the room transition pass evaluates it.

The Fix

Step 1: Move cleanup logic to the Cleanup Event. The Cleanup Event runs unconditionally when the instance is removed for any reason. It catches room transitions on non-persistent objects, destroy calls, and game end. Use it for resource freeing that must always happen.

// obj_level_controller Create event — allocate resources
spawn_points = ds_list_create();
state_map = ds_map_create();
audio_emitters = ds_list_create();

// obj_level_controller Cleanup event — always runs once
if (ds_exists(spawn_points, ds_type_list)) {
    ds_list_destroy(spawn_points);
}

if (ds_exists(state_map, ds_type_map)) {
    ds_map_destroy(state_map);
}

if (ds_exists(audio_emitters, ds_type_list)) {
    var _n = ds_list_size(audio_emitters);
    for (var i = 0; i < _n; ++i) {
        var _e = audio_emitters[| i];
        if (audio_emitter_exists(_e)) {
            audio_emitter_free(_e);
        }
    }
    ds_list_destroy(audio_emitters);
}

show_debug_message("Level controller cleaned up");

The ds_exists guards matter because Cleanup can run on partially-initialised instances if the Create event errored out. Defensive checks make the cleanup safe in failure cases.

Step 2: Manually trigger Room End on persistent objects before transitioning. If your gameplay logic genuinely needs Room End semantics on a persistent controller (saving state, dispatching room-change events to systems), trigger it yourself before room_goto. Use a transition manager that handles the sequence cleanly.

// Global transition function — call this instead of room_goto
function scr_change_room(_target) {
    // Fire Room End on every instance that wants it
    with (all) {
        if (persistent) {
            // Force the event manually since it would be skipped
            event_perform(ev_other, ev_room_end);
        }
    }

    // Fire a global "about to change room" callback if defined
    if (variable_global_exists("on_room_change")
        && is_callable(global.on_room_change)) {
        global.on_room_change(_target);
    }

    // Now do the actual transition
    room_goto(_target);
}

The with (all) loop iterates every instance in the room. Filtering by persistent avoids double-firing Room End on non-persistent instances (which the engine will fire automatically a moment later). The manual event_perform with ev_other/ev_room_end matches the parameters the engine would use internally.

Choosing the Right Lifecycle Event

GameMaker exposes several lifecycle events that look similar but fire under different conditions. Choosing the right one removes most Room End surprises.

Room Start fires when an instance enters a room. For persistent instances, this fires once on first creation and again on every subsequent room transition. It is a reliable hook for “the room I am in just changed”.

Room End fires when a non-persistent instance is leaving the current room. It does not fire for persistent instances or for instances destroyed before End Step.

Cleanup fires whenever the instance is being removed for any reason. It is the safest place for resource freeing and the only event guaranteed to run exactly once before the instance ceases to exist.

Game End fires when game_end() is called. It runs in addition to Cleanup, not instead of it.

For state-saving on room change, use Room Start of the destination room rather than Room End of the source room. The destination is guaranteed to load, the source may have already been replaced — Room Start gives you a stable point to record “the previous room was X” via a global variable set just before room_goto.

“Persistence is a contract: this instance survives the room. Survivors do not get goodbye events. If you need a goodbye, schedule it yourself before you leave.”

Related Issues

If your persistent objects multiply across room transitions, see Persistent Instance Duplicates for the singleton pattern. If Cleanup fires twice on a single instance, check Cleanup Event Firing Twice for instance ID and event_perform recursion fixes.

Cleanup is the only lifecycle event you can trust to fire exactly once. Put resource frees there.