Introducción al Desarrollo de Videojuegos con GameMaker · Module 12: Debugging, Optimization, Exporting, and Publishing

Performance Optimization and Memory Management

Lesson 57 of 60 · 50 min

Also available in: ES

Introduction to 2D Game Optimization

Throughout the previous modules, you built a fully functional 2D platformer: your character runs, jumps, fights enemies, collects coins, triggers audio cues, and transitions smoothly between levels. However, a game that runs flawlessly on a high-end development PC can suffer from stuttering, frame drops, or crashes when running on entry-level hardware, handheld consoles, or mobile devices. This is where optimization comes in: the set of practices aimed at ensuring software uses as few hardware resources as possible (CPU, GPU, and RAM) while delivering a smooth, responsive player experience.

Optimizing does not mean writing cryptic, convoluted code from day one or obsessing over micro-optimizations prematurely. In software engineering, Donald Knuth's famous maxim rings true: "premature optimization is the root of all evil." If you attempt to optimize every line of code before checking whether the game actually suffers from bottlenecks, you will waste valuable time and make your codebase hard to read and maintain. The professional approach is to write clean, modular, well-structured code, profile your game using diagnostic tools to identify actual bottlenecks, and apply targeted fixes where real slowdowns or memory leaks occur.

Key Performance Metrics: Target FPS vs. Real FPS

When measuring game performance in GameMaker, the first metric developers check is the frame rate (Frames Per Second, or FPS). However, within the engine, there is a fundamental distinction between two built-in variables that every creator should understand: target game speed and actual processing capability.

Game speed (historically represented by room_speed and now configured in GameMaker via game speed settings, typically 60 FPS) dictates how many times per second logic events (Create, Step, etc.) and drawing operations execute. Conversely, the built-in variable fps_real represents how many cycles per second your hardware could process if GameMaker were not artificially capped to your monitor's refresh rate or the 60 FPS target. If your game runs at a stable 60 frames, the fps variable returns 60, while fps_real might show 450, 1200, or 3000, indicating that the CPU has ample headroom between frames.

MetricGML Function / VariableMeaning & Expected Range
Target FPSgame_get_speed(gamespeed_fps)Configured logic update rate per second (typically 60).
Perceived FPSfpsActual display refresh rate on screen. Should stay locked to target (e.g., 60).
CPU Real FPSfps_realTheoretical maximum processing capacity. The higher above 60 FPS, the more CPU headroom available.
Delta Timedelta_timeMicroseconds elapsed between the current and previous frame.

When fps_real drops below 60, the engine no longer has enough time within the frame budget (16.66 milliseconds at 60 Hz) to finish running game logic and render calls. Consequently, perceived fps drops below 60, gameplay visually stutters, and controls exhibit input latency.

Memory Management and Memory Leaks

One of the most insidious bugs in game development is not an immediate framerate drop, but progressive degradation: a game that boots up smoothly, but after 10 minutes begins stuttering, freezes, or crashes outright. This behavior is typically caused by a memory leak. A memory leak occurs when dynamic memory is allocated to hold data or visual assets, but is never released back to the system once no longer needed. Over time, unreferenced data piles up in system RAM until the operating system runs out of allocatable memory.

GameMaker features an automatic Garbage Collector for standard variables, conventional arrays, and GML structs. However, several low-level dynamic data resources are NOT tracked or cleared by the garbage collector. As the programmer, you are solely responsible for manually freeing these resources once their lifecycle ends.

Manual Dynamic ResourceCreation FunctionRequired Cleanup Function
DS Listds_list_create()ds_list_destroy(id)
DS Mapds_map_create()ds_map_destroy(id)
DS Gridds_grid_create(w, h)ds_grid_destroy(id)
Render Surfacessurface_create(w, h)surface_free(id)
Data Buffersbuffer_create(size, type, align)buffer_delete(id)
Particle Systemspart_system_create()part_system_destroy(id)

A common beginner mistake is creating dynamic data structures inside recurring events like Step or Draw without immediately freeing them, or creating them in the Create event without writing corresponding destruction logic in the Clean Up or Destroy events.

// ========================================================
// OBJECT: obj_inventory
// EVENT: Create
// ========================================================
// Create a dynamic list to store collected item names
item_list = ds_list_create();
ds_list_add(item_list, "Wooden Sword");
ds_list_add(item_list, "Health Potion");


// ========================================================
// OBJECT: obj_inventory
// EVENT: Clean Up
// ========================================================
// Verify the list exists before destroying it to prevent runtime crashes
if (ds_exists(item_list, ds_type_list)) {
    ds_list_destroy(item_list);
}

💡 Golden Rule of Memory: For every built-in function containing the word 'create' that allocates data structures, surfaces, or particle systems, there must be a matching 'destroy' or 'free' call placed in that object's Clean Up event.

Schematic diagram depicting the balance between CPU (logic and collisions), RAM (data structures), and GPU (texture pages and draw calls) in 2D game optimization.
Figure 1: The three pillars of 2D game performance: CPU computation, RAM management, and GPU rendering. · AI-generated image

GPU Optimization: Texture Pages and Draw Calls

When rendering a 2D scene, the CPU dispatches rendering instructions to the graphics card (GPU). Each instruction or packaged set of instructions sent across this bus is known as a "Draw Call." The higher the number of individual draw calls dispatched per frame, the more CPU-to-GPU overhead you introduce, leading to communication bottlenecks.

GameMaker optimizes this pipeline under the hood through "Batching." If 200 coins appear on screen sharing the same sprite or residing on the same underlying texture page, GameMaker does not send 200 separate draw calls. Instead, it aggregates them into a single geometry packet and issues a single instruction: "Render these 200 coin quads together." This requires only one draw call.

However, batching breaks whenever a GPU render state change occurs. The most frequent causes of batch breaks are: 1. Texture Page Swaps: When the next sprite to be rendered resides on a different texture page. 2. Blend Mode Changes: Switching blend modes using gpu_set_blendmode(bm_add) for lighting or glows and restoring bm_normal. 3. Shader or Surface Switches: Activating/deactivating custom shaders or targeting a different drawing surface.

Instance Optimization: Off-Screen Deactivation

In a platformer, a large level might contain thousands of placed room instances: 50 patrolling enemies, 300 collectible coins, spike traps, moving hazards, and hundreds of collision tiles. If all 500+ active instances execute their Step events every single frame—calculating gravity, pathing, and bounding-box collisions—the CPU wastes cycles computing logic for objects the player cannot even see.

The standard solution in GameMaker is region-based instance deactivation. When an instance is deactivated, GameMaker completely stops running all of its events (Step, Draw, Alarm, etc.) without removing it from room memory or wiping its variable states. It remains frozen until explicitly reactivated.

Let us implement a dedicated optimization controller that deactivates distant instances and only activates those inside the camera's view (plus a safe outer margin so objects do not visibly pop into existence at screen edges).

// ========================================================
// OBJECT: obj_optimization_manager
// EVENT: Step
// ========================================================
// 1. Retrieve the position and dimensions of the active view camera
var _cam = view_camera[0];
var _cam_x = camera_get_view_x(_cam);
var _cam_y = camera_get_view_y(_cam);
var _cam_w = camera_get_view_width(_cam);
var _cam_h = camera_get_view_height(_cam);

// 2. Define a safety margin around the screen perimeter
// This ensures enemies activate slightly before entering the visible view
var _margin = 96;

// 3. Temporarily activate all instances to perform accurate spatial testing
instance_activate_all();

// 4. Deactivate all instances located outside the expanded camera region
// (Note: obj_player and this controller must NEVER remain deactivated)
instance_deactivate_region(
    _cam_x - _margin,
    _cam_y - _margin,
    _cam_w + (_margin * 2),
    _cam_h + (_margin * 2),
    false, // false = deactivate instances OUTSIDE the region
    true   // true = do not deactivate the calling instance (self)
);

// 5. Explicitly ensure key controllers and the player remain active
instance_activate_object(obj_player);
instance_activate_object(obj_hud_controller);
instance_activate_object(id);

With these few lines in a persistent manager object, a sprawling level that previously consumed 90% of your CPU budget will only update the 15 to 20 instances visible on screen, dramatically increasing frame rates and reducing processing overhead.

GML Coding Best Practices and Collision Handling

Beyond instance counts and memory deallocation, how you structure daily algorithms in GML directly impacts performance. Below are foundational practices that separate unoptimized scripts from production-ready game architecture.

1. Simplify Collision Masks

Precise per-pixel collision checking is one of the heaviest operations in 2D physics. Checking whether two complex concave shapes with transparent cutouts intersect requires scanning individual pixel matrices, triggering thousands of CPU checks per frame. For responsive platformers, always stick to simple rectangular bounding boxes (Bounding Box: Rectangle). In GameMaker's Sprite Editor, set Collision Mask shape to 'Rectangle' for players, enemies, and solid terrain.

2. Avoid Expensive Queries Inside the Step Event

Functions like instance_nearest() or 'with' loops querying broad object types iterate over every active instance in the room. Running these checks 60 times per second across dozens of enemies creates massive CPU bottlenecks. If an enemy simply needs the player's position, it is far more efficient to check whether the player exists and read obj_player.x directly, rather than recalculating spatial proximity every frame.

// ========================================================
// OBJECT: obj_enemy_patrol
// EVENT: Step (INEFFICIENT - Avoid this pattern)
// ========================================================
// Avoid: Searching for the nearest instance every single frame
var _target = instance_nearest(x, y, obj_player);
if (_target != noone) {
    if (point_distance(x, y, _target.x, _target.y) < 120) {
        // Chase player...
    }
}

// ========================================================
// OBJECT: obj_enemy_patrol
// EVENT: Step (EFFICIENT - Recommended pattern)
// ========================================================
// Better: If only one player instance exists, query coordinates directly
// after ensuring the instance exists in the current room
if (instance_exists(obj_player)) {
    if (point_distance(x, y, obj_player.x, obj_player.y) < 120) {
        // Chase player...
    }
}

3. Object Pooling for Projectiles and Particles

Instantiating and destroying hundreds of objects in short bursts (such as rapid-fire turret bullets or debris particles) places heavy stress on GameMaker's dynamic memory allocator. The Object Pooling pattern addresses this by pre-spawning a fixed pool of objects (e.g., 20 bullets) in the Create event. When a bullet is fired, it is made visible and activated; when it impacts a wall, rather than destroying it via instance_destroy(), it is deactivated and returned to the pool for reuse.

Practical Exercises

Put these optimization principles into practice by completing the following three exercises in your platformer project:

Summary

  • Optimization ensures consistent, smooth gameplay through clean code and efficient CPU, GPU, and RAM usage while avoiding premature micro-optimizations.
  • The fps variable reflects perceived render speed, whereas fps_real indicates available CPU processing headroom for each logic frame.
  • Memory leaks happen when dynamically allocated resources (ds_lists, grids, surfaces, particle systems) are not destroyed; the Clean Up event is the designated location to release them.
  • GPU performance relies on batching; grouping assets into Texture Groups and minimizing blend mode, shader, and texture swaps drastically lowers draw calls.
  • Off-screen instance deactivation using instance_deactivate_region saves substantial CPU time in large rooms by pausing logic for non-visible objects.

Practise what you learned

Exercise 1 · 1 point

What is the primary advantage of freeing dynamic data structures in the Clean Up event rather than in the Destroy event?

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

Exercise 2 · 1 point

When deactivating instances that move outside the camera view, GameMaker removes them from room memory and resets all their variables to conserve resources.

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

Exercise 3 · 1 point

Match each performance and optimization concept with its correct definition:

  • fps_real
  • Draw Call
  • Batching
  • Fuga de memoria
You are practising without an account: nothing is saved. Sign up to keep your progress.

Sign in to save your progress — Sign in