Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 3: Fundamentos de programación con GML Code.

Creación y llamada de scripts personalizados

Clase 15 de 60 · 60 min

También disponible en: EN

Modularización y reutilización de código en GameMaker

A medida que un videojuego crece en complejidad, escribir toda la lógica directamente en los eventos de los objetos (como Create, Step o Draw) se vuelve insostenible. Si necesitas calcular la distancia entre dos entidades, limitar la vida máxima del personaje o aplicar una fórmula de daño en diez objetos distintos, duplicar el mismo bloque de código genera errores difíciles de rastrear, dificulta el mantenimiento e incrementa drásticamente el tiempo de desarrollo. En programación, este problema se resuelve aplicando el principio fundamental DRY (Don't Repeat Yourself, o No te repitas).

En GameMaker, la herramienta principal para estructurar y reutilizar código son los scripts. Un script es un recurso del Asset Browser que actúa como contenedor global donde definimos una o varias funciones personalizadas mediante GML Code. Una vez declarada una función en un script, cualquier objeto, alarma o evento de tu juego puede invocarla de la misma forma en que invoca las funciones nativas del motor (como instance_create_layer, place_meeting o point_distance), pasando argumentos específicos y recibiendo valores procesados.

Diagrama esquemático que muestra cómo diferentes objetos del juego envían argumentos a una función definida en un script central y reciben un valor de retorno procesado.
Flujo de ejecución: los objetos envían parámetros a una función centralizada en un script y reciben un resultado computado. · Imagen generada con IA

Creación de un recurso Script en el Asset Browser

Para crear un script en GameMaker, localiza el panel del Asset Browser (generalmente ubicado en el lateral derecho de la interfaz de trabajo). Haz clic derecho sobre la carpeta Scripts o en cualquier espacio vacío del panel, selecciona Create y haz clic en Script. Inmediatamente se creará un nuevo archivo de texto de código y GameMaker te solicitará que le asignes un nombre identificador.

Es una buena práctica de nomenclatura utilizar el prefijo scr_ seguido del área temática que agrupará las funciones contenidas en ese archivo. Por ejemplo, scr_matematicas, scr_combate, scr_movimiento o scr_utilidades. En las versiones modernas de GameMaker (a partir de la versión 2.3), un único recurso de script puede contener múltiples declaraciones de funciones independientes, lo que te permite organizar tu proyecto por módulos temáticos en lugar de tener cientos de archivos individuales con una sola línea cada uno.

💡 Nota histórica importante: En versiones antiguas de GameMaker Studio (1.4 y anteriores a la 2.3), el archivo de script equivalía a una sola función que llevaba el mismo nombre del archivo y recibía parámetros mediante el arreglo interno argument[0], argument[1], etc. En el GameMaker moderno, los scripts son archivos contenedores que declaran funciones formales mediante la palabra clave function con parámetros nombrados.

Anatomía y sintaxis de una función personalizada

Una función es un bloque de instrucciones que realiza una tarea específica, recibe datos de entrada (llamados parámetros o argumentos), ejecuta un procesamiento interno y, opcionalmente, devuelve un resultado al punto del programa donde fue invocada mediante la sentencia return. La estructura básica de una función en GML Code es la siguiente:

// Declaración de una función básica dentro de un Script
function calcular_area_rectangulo(_ancho, _alto)
{
    var _area = _ancho * _alto;
    return _area;
}

Analicemos cada componente de esta estructura: primero, la palabra reservada 'function' indica al compilador de GameMaker que estamos declarando una nueva rutina ejecutable. A continuación, definimos el nombre de la función (calcular_area_rectangulo), el cual debe ser descriptivo, estar en minúsculas y usar guiones bajos según las convenciones estándar de GML. Entre paréntesis colocamos los parámetros separados por comas; en este caso, _ancho y _alto. Dentro del cuerpo de la función, delimitado por llaves {}, realizamos las operaciones necesarias y utilizamos la palabra reservada 'return' para enviar el resultado de vuelta.

Es fundamental utilizar variables locales declaradas con 'var' (como _area) para los cálculos intermedios dentro de una función. Las variables locales solo existen mientras la función se está ejecutando y se liberan de la memoria de inmediato al terminar, evitando contaminar la memoria del objeto que invocó la función o sobrescribir variables de instancia con el mismo nombre accidentalmente.

Documentación con comentarios JSDoc y autocompletado

Uno de los aspectos más potentes de GameMaker es su sistema de autocompletado de código (IntelliSense). Cuando escribes una función nativa como instance_destroy, el editor te muestra en la barra inferior qué argumentos espera y qué hace. Puedes lograr exactamente el mismo comportamiento profesional en tus funciones personalizadas añadiendo comentarios con formato JSDoc inmediatamente antes de la declaración de la función, utilizando la sintaxis de triple barra (///).

/// @function                   calcular_dano_mitigado(_dano_base, _defensa)
/// @description                Calcula el daño final recibido restando el porcentaje de defensa.
/// @param {Real} _dano_base    Cantidad de daño inicial antes de mitigación.
/// @param {Real} _defensa      Puntos de defensa del receptor (rango de 0 a 100).
function calcular_dano_mitigado(_dano_base, _defensa)
{
    // Aseguramos que la defensa se mantenga entre 0 y 90 para no bloquear el 100% del daño
    var _defensa_segura = clamp(_defensa, 0, 90);
    var _factor_reduccion = 1 - (_defensa_segura / 100);
    var _dano_final = _dano_base * _factor_reduccion;
    
    return _dano_final;
}

Al incluir estas cuatro líneas de comentarios JSDoc, cada vez que comiences a escribir 'calcular_dano_mitigado' en cualquier evento de cualquier objeto, GameMaker sugerirá automáticamente la función, resaltará los nombres de los parámetros y te indicará el tipo de dato requerido en la parte inferior de la ventana de código.

Llamada de funciones y ámbito de ejecución (Scope)

Llamar o invocar a una función personalizada es tan simple como escribir su nombre seguido de los argumentos requeridos entre paréntesis dentro de un evento de un objeto. Sin embargo, para evitar errores de lógica, es crucial entender qué ámbito o 'scope' tiene la función en el instante en que se ejecuta.

Cuando una función declarada en un script es invocada desde un objeto (por ejemplo, desde el evento Step de obj_jugador), el contexto de la palabra clave 'self' corresponde a esa instancia específica del jugador. Esto significa que la función puede leer y modificar directamente las variables de instancia de dicho objeto, a menos que esté diseñada como una función puramente matemática.

Tipo de FunciónPropósito PrincipalRetorno (return)Interacción con la Instancia
Función Pura / MatemáticaRealizar cálculos basados únicamente en los argumentos recibidos.Siempre devuelve un valor (número, string, booleano, etc.).No lee ni altera variables de instancia del objeto invocador.
Función de Acción / EstadoModificar el estado o propiedades del objeto que la llama.Opcional (puede no retornar nada o devolver true/false si tuvo éxito).Modifica directamente variables de instancia como x, y, hp, estado.
Función de ComprobaciónEvaluar una condición lógica compleja y validar reglas de juego.Devuelve siempre un valor booleano (true o false).Lee condiciones del entorno, colisiones o variables de la instancia.

Distinguir cuándo diseñar una función pura y cuándo una función de acción es una habilidad fundamental. Las funciones puras son más fáciles de depurar y pueden reutilizarse sin riesgo en cualquier parte del proyecto. Las funciones de acción simplifican drásticamente los eventos extensos, convirtiendo un evento Step sobrecargado en una secuencia limpia y legible de llamadas a funciones.

Valores por defecto en parámetros

En el desarrollo de videojuegos, con frecuencia deseamos que ciertos argumentos de nuestras funciones tengan valores predeterminados en caso de que quien las invoque decida no especificarlos. GameMaker permite asignar valores por defecto en la propia lista de parámetros utilizando el operador de asignación (=).

/// @function               aplicar_impulso(_fuerza_x, _fuerza_y = 0)
/// @param {Real} _fuerza_x Fuerza horizontal a aplicar
/// @param {Real} _fuerza_y Fuerza vertical opcional (por defecto 0)
function aplicar_impulso(_fuerza_x, _fuerza_y = 0)
{
    // Modifica las variables de instancia del objeto que llama a la función
    velocidad_x += _fuerza_x;
    velocidad_y += _fuerza_y;
}

Con esta declaración, si llamamos a 'aplicar_impulso(5)' desde el jugador, _fuerza_x tomará el valor 5 y _fuerza_y asumirá automáticamente el valor 0 sin lanzar un error por falta de argumentos. Si llamamos a 'aplicar_impulso(5, -12)', ambos valores serán tomados de los parámetros suministrados explícitamente.

Integración en el proyecto del juego de plataformas 2D

En las lecciones anteriores de este módulo hemos definido variables para nuestro personaje principal obj_jugador, tales como puntos de salud (hp, hp_maxima), monedas recolectadas y multiplicadores de daño. Ahora vamos a estructurar estas mecánicas mediante scripts dedicados, preparando el terreno para el movimiento y las colisiones que abordaremos en los próximos módulos.

Paso a paso, implementaremos un script central para la gestión de vitalidad y combate llamado scr_combate, y otro script para cálculos de movimiento llamado scr_matematicas_plataformas. Creemos el primer archivo en el Asset Browser.

/// Script: scr_combate

/// @function                  recibir_dano(_hp_actual, _dano)
/// @description               Aplica daño a una cantidad de salud garantizando que no caiga por debajo de cero.
/// @param {Real} _hp_actual   Salud actual de la entidad.
/// @param {Real} _dano        Puntos de daño recibidos.
function recibir_dano(_hp_actual, _dano)
{
    var _nueva_hp = _hp_actual - _dano;
    
    if (_nueva_hp < 0)
    {
        _nueva_hp = 0;
    }
    
    return _nueva_hp;
}

/// @function                  curar_entidad(_hp_actual, _curacion, _hp_maxima)
/// @description               Restaura salud sin superar el límite de salud máxima establecido.
/// @param {Real} _hp_actual   Salud actual de la entidad.
/// @param {Real} _curacion    Cantidad de puntos a restaurar.
/// @param {Real} _hp_maxima   Puntos máximos de salud permitidos.
function curar_entidad(_hp_actual, _curacion, _hp_maxima)
{
    var _resultado = _hp_actual + _curacion;
    
    if (_resultado > _hp_maxima)
    {
        _resultado = _hp_maxima;
    }
    
    return _resultado;
}

Ahora observemos cómo se consumen estas funciones desde el objeto jugador. En clases anteriores creamos el objeto obj_jugador. Vamos a su evento Create y a su evento Step para poner a prueba las funciones que acabamos de declarar.

// Objeto: obj_jugador
// Evento: Create

hp_maxima = 100;
hp = hp_maxima;
esta_vivo = true;

// Variable de depuración para probar impactos
contador_invulnerabilidad = 0;
// Objeto: obj_jugador
// Evento: Step

// Reducir temporizador de invulnerabilidad si está activo
if (contador_invulnerabilidad > 0)
{
    contador_invulnerabilidad -= 1;
}

// Simulación de prueba: Si presionamos la tecla 'H' (Hurt), recibimos 25 puntos de daño
if (keyboard_check_pressed(ord("H")) && contador_invulnerabilidad == 0)
{
    hp = recibir_dano(hp, 25);
    contador_invulnerabilidad = 30; // Medio segundo de gracia a 60 FPS
    show_debug_message("Daño recibido. Salud restante: " + string(hp));
}

// Simulación de prueba: Si presionamos la tecla 'C' (Curar), restauramos 20 puntos de salud
if (keyboard_check_pressed(ord("C")))
{
    hp = curar_entidad(hp, 20, hp_maxima);
    show_debug_message("Curación aplicada. Salud actual: " + string(hp));
}

// Comprobación de estado vital
if (hp <= 0 && esta_vivo)
{
    esta_vivo = false;
    show_debug_message("El jugador ha sido derrotado.");
}

Observa con atención la simplicidad del evento Step. En lugar de tener complejas condiciones matemáticas anidadas en el evento del jugador, la línea 'hp = recibir_dano(hp, 25);' expresa con absoluta claridad la intención de la mecánica. Y lo más ventajoso: si en el Módulo 7 creamos un enemigo patrullero llamado obj_enemigo_patrulla, podremos utilizar exactamente las mismas funciones 'recibir_dano' y 'curar_entidad' sin reescribir una sola línea de lógica matemática.

Funciones matemáticas para física de plataformas: La aproximación lineal

Un problema muy común en los juegos de plataformas retro es hacer que una velocidad desacelere suavemente hasta llegar a cero (fricción) sin sobrepasar el valor objetivo ni oscilar entre valores positivos y negativos. En GML, la función nativa lerp() interpola porcentualmente, pero para una aceleración y fricción constantes con paso fijo se utiliza una función de aproximación. Crearemos un segundo script llamado scr_matematicas_plataformas para alojar esta herramienta fundamental.

/// Script: scr_matematicas_plataformas

/// @function                  aproximar_valor(_actual, _objetivo, _paso)
/// @description               Acerca un valor inicial hacia un objetivo sumando o restando un paso fijo sin pasarse.
/// @param {Real} _actual      Valor numérico de partida.
/// @param {Real} _objetivo    Valor al que se desea llegar.
/// @param {Real} _paso        Magnitud de cambio positiva por ciclo.
function aproximar_valor(_actual, _objetivo, _paso)
{
    if (_actual < _objetivo)
    {
        return min(_actual + _paso, _objetivo);
    }
    else if (_actual > _objetivo)
    {
        return max(_actual - _paso, _objetivo);
    }
    
    return _objetivo;
}
// Objeto: obj_jugador
// Evento: Step (Fragmento de fricción)

// Supongamos que velocidad_x tiene un valor residual y no hay teclas presionadas
var _friccion = 0.5;
velocidad_x = aproximar_valor(velocidad_x, 0, _friccion);

Si velocidad_x valía 1.2, al restar 0.5 pasará a 0.7, luego a 0.2, y en el siguiente ciclo, en lugar de pasar a -0.3 (lo que causaría que el personaje se desplace temblorosamente hacia atrás), la función max(0.2 - 0.5, 0) limitará el valor exactamente a 0. Esto proporciona una parada perfecta y limpia, ideal para un juego de plataformas con estética retro de alta precisión.

Buenas prácticas en la estructuración de scripts

Para mantener tu proyecto escalable y fácil de leer a medida que avancemos hacia módulos más complejos, considera siempre las siguientes pautas de diseño:

1. Un propósito por función: Cada función debe hacer una sola cosa bien. Si tienes una función llamada 'manejar_jugador()' que calcula vidas, lee el teclado, reproduce sonidos y mueve la cámara al mismo tiempo, el código volverá a ser confuso. Separa esa lógica en 'actualizar_combate()', 'leer_controles()' y 'actualizar_posicion()'.

2. Prefijos consistentes: Usa prefijos claros para los argumentos de las funciones (por ejemplo, comenzar los nombres de parámetros con un guion bajo como _velocidad o _limite) para diferenciarlos instantáneamente de las variables de instancia globales o del objeto (como velocidad o limite).

3. Evita dependencias invisibles: Siempre que sea viable, haz que las funciones reciban todo lo que necesitan mediante sus parámetros en lugar de asumir que una variable global existe. Esto hace que tus funciones sean portables; podrás copiarlas y pegarlas en futuros proyectos de GameMaker y funcionarán de inmediato sin requerir modificaciones.

Ejercicios prácticos

Realiza los siguientes tres ejercicios prácticos en tu proyecto para consolidar la creación, documentación e invocación de scripts y funciones personalizadas en GML Code.

Resumen

  • Los scripts en GameMaker actúan como librerías globales que contienen funciones personalizadas reutilizables por cualquier objeto del juego.
  • A partir de GameMaker 2.3+, se utiliza la palabra clave 'function' seguida del nombre y de la lista de parámetros entre llaves para declarar funciones estructuradas.
  • La instrucción 'return' detiene la ejecución de la función y devuelve un resultado procesado al punto de llamada, permitiendo cálculos limpios y modulares.
  • Los comentarios JSDoc (iniciados con ///) permiten que el editor de GameMaker ofrezca autocompletado inteligente y descripciones contextuales para tus propias funciones.
  • El uso de variables locales ('var') dentro de las funciones previene fugas de memoria y evita sobrescribir accidentalmente variables de instancia de los objetos.
  • Dividir el código en scripts modulares (como scr_combate y scr_matematicas_plataformas) deja el proyecto preparado y ordenado para las mecánicas de movimiento y colisión del siguiente módulo.

Practica lo aprendido

Ejercicio 1 · 1 punto

¿Por qué es fundamental declarar las variables intermedias con la palabra reservada 'var' dentro de una función personalizada en GameMaker?

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

Ejercicio 2 · 1 punto

Relaciona cada elemento o convención de la creación de scripts en GameMaker con su descripción correspondiente según el texto:

  • scr_
  • ///
  • return
  • =
Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 3 · 2 puntos

Siguiendo el ejemplo de la lección, escribe la función personalizada llamada 'calcular_area_rectangulo'. La función debe recibir los parámetros '_ancho' y '_alto', calcular su producto en una variable local '_area' mediante 'var', y devolver dicho valor con 'return'.

Tu código no se ejecuta: se comprueban requisitos concretos sobre el texto.

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

Ingresa para guardar tu progreso — Ingresar