Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 12: Depuración, optimización, exportación y publicación.

Optimización del rendimiento y uso de memoria

Clase 57 de 60 · 50 min

También disponible en: EN

Introducción a la optimización en videojuegos 2D

A lo largo de los módulos anteriores has construido un videojuego de plataformas 2D completamente funcional: tu personaje corre, salta, combate enemigos, recoge monedas, escucha música y navega por distintos niveles mediante transiciones pulidas. Sin embargo, un juego que funciona de forma impecable en un ordenador de desarrollo potente puede sufrir tirones, caídas de fotogramas o cierres inesperados al ejecutarse en equipos más modestos, consolas portátiles o dispositivos móviles. Aquí es donde entra en juego la optimización: el conjunto de técnicas orientadas a hacer que nuestro software consuma la menor cantidad posible de recursos (procesador, tarjeta gráfica y memoria RAM) manteniendo una experiencia visual fluida y estable.

Optimizar no consiste en transformar todo el código en fórmulas matemáticas crípticas desde el primer día ni en obsesionarse antes de tiempo. En el desarrollo de software existe un aforismo clásico atribuido a Donald Knuth: «la optimización prematura es la raíz de todos los males». Si intentas optimizar cada línea de código antes de comprobar si el juego realmente sufre ralentizaciones, perderás tiempo valioso y harás que tu proyecto sea más difícil de leer y mantener. El enfoque profesional consiste en escribir código limpio, ordenado y estructurado, identificar con precisión dónde están los cuellos de botella mediante herramientas de diagnóstico, y aplicar correcciones específicas donde verdaderamente existan pérdidas de rendimiento o consumo desmedido de memoria.

Métricas clave de rendimiento: FPS del juego versus FPS reales

Para medir el rendimiento de un juego en GameMaker, el primer indicador en el que nos fijamos suele ser la tasa de fotogramas por segundo (Frames Per Second o FPS). No obstante, dentro del motor existe una distinción crucial entre dos variables integradas que todo creador debe comprender con exactitud: la velocidad objetivo del juego y la velocidad real de procesamiento.

La velocidad del juego (representada históricamente por room_speed y configurada actualmente en GameMaker como la velocidad de actualización de la room, típicamente a 60 FPS) define cuántas veces por segundo se ejecutan los eventos de lógica (Create, Step, etc.) y dibujo. Por otro lado, la variable interna fps_real representa cuántos ciclos por segundo podría ejecutar tu ordenador si GameMaker no estuviese limitado artificialmente a la tasa de refresco del monitor o a los 60 cuadros programados. Si tu juego corre a 60 cuadros estables, la variable fps devolverá 60, pero fps_real podría marcar 450, 1200 o 3000, indicando que el procesador dispone de un margen holgado de tiempo libre entre fotograma y fotograma.

MétricaFunción o Variable GMLSignificado y Rango Óptimo
FPS Objetivogame_get_speed(gamespeed_fps)Fotogramas lógicos por segundo fijados por el proyecto (usualmente 60).
FPS PercibidosfpsTasa real de visualización en pantalla. Debe mantenerse igual al objetivo (ej. 60).
FPS Reales de CPUfps_realCapacidad teórica máxima del procesador. Cuanto más supere a los 60 FPS, más holgado va el juego.
Delta Timedelta_timeMicrosegundos transcurridos entre el fotograma actual y el anterior.

Cuando el valor de fps_real desciende por debajo de 60, el motor ya no dispone de tiempo suficiente dentro de la fracción de segundo (16.66 milisegundos a 60 Hz) para completar toda la lógica y el renderizado. Como consecuencia inmediata, la variable fps caerá, el juego se ralentizará visualmente y los controles responderán con retraso (input lag).

Gestión de memoria y fugas de recursos (Memory Leaks)

Uno de los problemas más graves en el desarrollo de videojuegos no es la lentitud visual instantánea, sino la degradación progresiva: un juego que arranca fluido, pero tras jugar 10 minutos se congela o colapsa el sistema operativo. Este fenómeno suele deberse a una fuga de memoria (Memory Leak). Una fuga de memoria ocurre cuando reservamos un espacio de memoria dinámica para almacenar datos o recursos, pero olvidamos destruirlo o liberarlo cuando ya no lo necesitamos. El sistema continúa acumulando esa basura en la memoria RAM hasta agotar la capacidad disponible.

GameMaker cuenta con un recolector de basura (Garbage Collector) automático para variables estándar, arrays convencionales y structs de GML. Sin embargo, existen recursos que operan de forma manual a bajo nivel y que el recolector de basura NO liberará automáticamente. Eres tú, como programador, quien tiene la responsabilidad absoluta de destruirlos cuando su ciclo de vida termine.

Recurso Dinámico ManualFunción de CreaciónFunción de Destrucción Obligatoria
Estructura de datos Listads_list_create()ds_list_destroy(id)
Estructura de datos Mapads_map_create()ds_map_destroy(id)
Estructura de datos Rejillads_grid_create(w, h)ds_grid_destroy(id)
Superficies gráficassurface_create(w, h)surface_free(id)
Búferes de memoriabuffer_create(size, type, align)buffer_delete(id)
Sistemas de partículaspart_system_create()part_system_destroy(id)

Analicemos un error típico muy común entre desarrolladores principiantes: crear una estructura de datos dentro de un evento repetitivo como el evento Step o el evento Draw sin destruirla en ese mismo bloque, o crearla en el evento Create sin destruirla en el evento Clean Up o Destroy.

// ========================================================
// OBJETO: obj_inventario
// EVENTO: Create
// ========================================================
// Creamos una lista dinámica para registrar los objetos recogidos
lista_items = ds_list_create();
ds_list_add(lista_items, "Espada de madera");
ds_list_add(lista_items, "Pocion de salud");


// ========================================================
// OBJETO: obj_inventario
// EVENTO: Clean Up
// ========================================================
// Verificamos si la lista existe antes de destruirla para evitar errores
if (ds_exists(lista_items, ds_type_list)) {
    ds_list_destroy(lista_items);
}

💡 Regla de oro de la memoria: Por cada función con la palabra create que uses para estructuras de datos, superficies o emisores de partículas, debe existir una función con la palabra destroy o free correspondiente en el evento Clean Up de ese objeto.

Diagrama esquemático que representa el equilibrio entre CPU (lógica y colisiones), Memoria RAM (estructuras de datos) y GPU (páginas de texturas y llamadas de dibujo) en la optimización de un videojuego 2D.
Figura 1: Los tres pilares del rendimiento en videojuegos 2D: cálculo en CPU, gestión de RAM y renderizado en GPU. · Imagen generada con IA

Optimización de la GPU: Páginas de texturas y llamadas de dibujo (Draw Calls)

Al renderizar un juego 2D, el procesador (CPU) envía instrucciones a la tarjeta gráfica (GPU) sobre qué dibujar en la pantalla. Cada instrucción o paquete de instrucciones que la CPU envía a la GPU se conoce como una llamada de dibujo o «Draw Call». Cuantas más llamadas de dibujo independientes deba procesar el hardware por fotograma, más se saturará el canal de comunicación entre la CPU y la tarjeta gráfica.

GameMaker optimiza esto internamente mediante una técnica llamada «Batching» (procesamiento por lotes). Si tienes 200 monedas en pantalla que comparten la misma imagen o provienen de la misma página de texturas, GameMaker no envía 200 órdenes individuales a la tarjeta gráfica; las agrupa en un único paquete y le dice a la GPU: «Dibuja estas 200 monedas de golpe». Esto consume apenas una sola llamada de dibujo.

Sin embargo, el procesamiento por lotes se rompe (se interrumpe) cada vez que ocurre un cambio de estado en la GPU. Los motivos más frecuentes por los que se rompe un lote son: 1. Cambiar de textura: Cuando el siguiente sprite que se debe dibujar pertenece a una página de texturas diferente (Texture Swap). 2. Cambiar de modo de fusión: Usar gpu_set_blendmode(bm_add) para efectos de iluminación o brillo y luego volver a bm_normal. 3. Activar o desactivar shaders o cambiar de superficie (surface).

Optimización de instancias: Desactivación fuera de la vista de la cámara

En nuestro juego de plataformas, un nivel grande puede tener miles de elementos colocados en la room: 50 enemigos patrullando, 300 monedas flotando, trampas mecánicas con pinchos y cientos de bloques de suelo. Si cada una de esas 500 instancias ejecuta su evento Step en cada fotograma comprobando colisiones con el suelo y calculando gravedad, la CPU estará procesando lógica de enemigos y monedas que el jugador ni siquiera puede ver porque están al otro extremo del mapa.

La solución estándar y más potente en GameMaker es la desactivación de instancias por regiones. Cuando una instancia está desactivada, GameMaker detiene completamente la ejecución de todos sus eventos (Step, Draw, Alarm, etc.), sin borrarla de la memoria de la room ni reiniciar sus variables. Permanece en un estado de congelación perfecta hasta que la volvemos a activar.

Implementemos un controlador de optimización que desactive los objetos lejanos y solo active aquellos que se encuentran dentro del campo visual de la cámara (con un pequeño margen exterior para que el jugador nunca vea aparecer o desaparecer cosas de golpe).

// ========================================================
// OBJETO: obj_gestor_optimizacion
// EVENTO: Step
// ========================================================
// 1. Obtenemos las coordenadas y dimensiones de la camara actual
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. Definimos un margen de seguridad alrededor de la pantalla
// Esto asegura que los enemigos se activen un poco antes de entrar en pantalla
var _margen = 96;

// 3. Activamos todas las instancias para recalcular correctamente
instance_activate_all();

// 4. Desactivamos todas las instancias de objetos activos que queden fuera
// de la region ampliada por el margen. (Nota: obj_jugador y el propio
// gestor NUNCA deben desactivarse).
instance_deactivate_region(
    _cam_x - _margen,
    _cam_y - _margen,
    _cam_w + (_margen * 2),
    _cam_h + (_margen * 2),
    false, // false = desactiva las que estan AFUERA de la region
    true   // true = no desactivarse a si mismo
);

// 5. Nos aseguramos de mantener siempre despiertos al jugador y al controlador
instance_activate_object(obj_jugador);
instance_activate_object(obj_controlador_hud);
instance_activate_object(id);

Con estas pocas líneas de código en un objeto gestor persistente, un nivel masivo que antes consumía el 90 % del tiempo de CPU procesará únicamente las 15 o 20 instancias visibles en ese instante, multiplicando el rendimiento por un factor significativo.

Buenas prácticas de programación y colisiones en GML

Además del manejo de instancias y memoria, la forma en que redactamos nuestros algoritmos cotidianos en GML repercute directamente en la fluidez del juego. A continuación, revisamos las mejores prácticas fundamentales que diferencian un código novato de una arquitectura profesional.

1. Simplificación de máscaras de colisión

Las colisiones precisas píxel a píxel (Precise Collision Checking) son el enemigo número uno del rendimiento en las físicas 2D. Comprobar si dos máscaras con curvas orgánicas y orificios transparentes se tocan exige examinar matrices de píxeles individuales, lo que requiere miles de operaciones por segundo. Para un juego de plataformas fluido, utiliza siempre máscaras rectangulares simples (Bounding Box: Rectangle). En el editor de sprites de GameMaker, selecciona la opción 'Rectangle' en la sección Collision Mask para el jugador, los enemigos y los bloques sólidos.

2. Evitar búsquedas costosas en el evento Step

Funciones como instance_nearest() o con_nombre_de_objeto con bucles que recorren todas las instancias de la room consumen mucho procesamiento si se ejecutan 60 veces por segundo en cada enemigo. Si un enemigo necesita saber la posición del jugador, es mucho más eficiente guardar la referencia directa en una variable o acceder a obj_jugador.x si solo existe un jugador, en lugar de calcular distancias con instance_nearest() en cada ciclo.

// ========================================================
// OBJETO: obj_enemigo_patrulla
// EVENTO: Step (Practica INEFICIENTE a evitar)
// ========================================================
// Evitar: buscar la instancia mas cercana en cada fotograma
var _objetivo = instance_nearest(x, y, obj_jugador);
if (_objetivo != noone) {
    if (point_distance(x, y, _objetivo.x, _objetivo.y) < 120) {
        // Perseguir al jugador...
    }
}

// ========================================================
// OBJETO: obj_enemigo_patrulla
// EVENTO: Step (Practica EFICIENTE y limpia)
// ========================================================
// Correcto: si solo hay un jugador, acceder directamente a sus coordenadas
// tras comprobar que la instancia existe en el juego
if (instance_exists(obj_jugador)) {
    if (point_distance(x, y, obj_jugador.x, obj_jugador.y) < 120) {
        // Perseguir al jugador...
    }
}

3. Reutilización y 'Object Pooling' para proyectiles y efectos

Crear y destruir cientos de instancias en periodos muy cortos (por ejemplo, balas de una torreta o chispas tras una explosión) estresa al gestor de memoria interna del motor. La técnica de Object Pooling consiste en crear una pequeña reserva fija de objetos (por ejemplo, 20 balas) al inicio del nivel en el evento Create. Cuando una bala se dispara, simplemente se vuelve visible y activa; cuando impacta contra una pared, en lugar de destruirla con instance_destroy(), se desactiva y se regresa a la reserva para volver a utilizarse cuando sea necesario.

Ejercicios prácticos

Pon en práctica los principios de optimización aprendidos completando los tres ejercicios siguientes en el proyecto de plataformas que venimos desarrollando:

Resumen

  • La optimización consiste en garantizar un rendimiento fluido y constante mediante código limpio y uso eficiente de CPU, GPU y RAM, evitando la optimización prematura.
  • La variable fps representa la tasa percibida en pantalla, mientras que fps_real indica la capacidad de cálculo libre que tiene la CPU en cada instante.
  • Las fugas de memoria ocurren al crear estructuras dinámicas (ds_list, ds_grid, superficies, partículas) sin destruirlas; el evento Clean Up es el lugar óptimo para liberarlas de manera segura.
  • Para optimizar la GPU, agrupa sprites en Texture Groups y minimiza cambios innecesarios de texturas, superficies y modos de fusión para reducir las llamadas de dibujo (Draw Calls).
  • La desactivación de instancias fuera de la vista de la cámara (instance_deactivate_region) ahorra grandes cantidades de procesamiento en niveles extensos al pausar objetos lejanos.

Practica lo aprendido

Ejercicio 1 · 1 punto

¿Cuál es la ventaja fundamental de liberar estructuras de datos dinámicas en el evento Clean Up en lugar de hacerlo en el evento Destroy?

Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 2 · 1 punto

Al desactivar instancias que quedan fuera de la vista de la cámara, GameMaker las borra de la memoria de la room y reinicia todas sus variables para ahorrar recursos.

Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 3 · 1 punto

Relaciona cada concepto de optimización y rendimiento con su definición correspondiente según el texto:

  • fps_real
  • Draw Call
  • Batching
  • Fuga de memoria
Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ingresa para guardar tu progreso — Ingresar