Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 11: Desarrollo completo de un videojuego de plataformas 2D.
Estructuración del proyecto final integrado
Clase 51 de 60 · 50 min
También disponible en: EN
Estructuración del proyecto final integrado
Bienvenidos al inicio del Módulo 11. A lo largo de los diez módulos previos has construido, pieza por pieza, los engranajes esenciales de un videojuego de plataformas 2D: la física de movimiento horizontal y vertical, la respuesta de colisiones por píxel o mediante tilemaps, la inteligencia artificial de patrulla de los enemigos, los sistemas de recolección y vida, el HUD dibujado en la capa gráfica, la emisión de audio, las partículas y la transición entre niveles. Sin embargo, al desarrollar proyectos progresivos es natural que los recursos, nombres de variables y scripts queden dispersos, redundantes o ligeramente desalineados entre módulos.
En la industria profesional del desarrollo de videojuegos, antes de iniciar la fase de ensamblaje general (donde se unen los niveles definitivos, la narrativa, los menús de inicio y las condiciones de victoria y derrota), se realiza una etapa obligatoria de consolidación técnica o «refactorización y estructuración». Esta sesión tiene como meta revisar minuciosamente el árbol de recursos (Asset Browser), unificar las nomenclaturas, alinear los orígenes de sprites y máscaras de colisión, y crear una arquitectura de scripts y objetos administradores que garantice que el videojuego funcione de manera predecible, escalable y libre de errores de memoria.
1. Auditoría del Asset Browser y convención de nomenclaturas
GameMaker permite organizar el Asset Browser mediante carpetas y etiquetas de color personalizadas. Un error habitual entre principiantes es dejar decenas de sprites, objetos y sonidos flotando en la raíz del explorador o dentro de carpetas predeterminadas genéricas. Cuando el proyecto supera los cincuenta recursos, encontrar el archivo correcto ralentiza el flujo de trabajo y facilita que se invoquen recursos duplicados u obsoletos.
La primera regla de la ingeniería limpia en GameMaker consiste en aplicar un sistema estricto de prefijos normalizados. Un prefijo identifica inequívocamente la naturaleza del recurso en el código GML, evitando colisiones de identificadores (por ejemplo, tener un sprite y un objeto llamados exactamente igual, lo cual corrompe la compilación).
| Prefijo | Tipo de Recurso | Ejemplo en el Proyecto | Propósito Específico |
|---|---|---|---|
| spr_ | Sprite | spr_player_run | Gráficos estáticos o tiras de animación para entidades y entornos. |
| obj_ | Object | obj_player | Entidades lógicas que contienen eventos, variables y código. |
| snd_ | Sound | snd_jump | Efectos de sonido (SFX) o temas musicales comprimidos/sin comprimir. |
| rm_ | Room | rm_level_01 | Salas o escenarios donde interactúan las instancias y capas. |
| fnt_ | Font | fnt_hud_retro | Tipografías rasterizadas para textos de menú e interfaz de usuario. |
| ts_ | Tile Set | ts_industrial_ruins | Conjunto de mosaicos optimizados para la construcción de escenarios. |
| scr_ | Script | scr_game_init | Archivos de funciones globales, definiciones de enums y macros. |
| ps_ | Particle System | ps_dust_run | Sistemas y emisores de partículas preconfigurados en el editor visual. |
| seq_ | Sequence | seq_game_intro | Animaciones cinemáticas compuestas de múltiples pistas. |
Más allá de los prefijos, es fundamental estructurar las carpetas por dominios funcionales en lugar de agrupar todo en una sola carpeta masiva de «Objetos» o «Sprites». La siguiente jerarquía modular es el estándar recomendado para nuestro juego de plataformas:
── [00_Core]
├── Scripts (scr_game_init, scr_utilities, scr_player_actions)
├── Fonts (fnt_hud, fnt_menu_title, fnt_menu_buttons)
└── Managers (obj_game_manager, obj_camera, obj_audio_manager)
── [01_Characters]
├── Player
│ ├── Sprites (spr_player_idle, spr_player_run, spr_player_jump, etc.)
│ └── Objects (obj_player)
└── Enemies
├── Sprites (spr_slime_walk, spr_slime_turn, spr_slime_dead)
└── Objects (obj_enemy_parent, obj_slime_patrol)
── [02_World_Environment]
├── Collisions (spr_solid_wall, obj_solid_wall, obj_platform_semisolid)
├── Hazards (spr_spikes, obj_spikes, obj_hazard_water)
├── Interactables (spr_checkpoint, obj_checkpoint, spr_door, obj_door)
├── Collectibles (spr_coin, obj_coin, spr_gem, obj_gem)
└── TileSets & Backgrounds (ts_dungeon, bg_sky, bg_mountains)
── [03_Interface]
├── HUD (spr_hud_heart, spr_hud_coin_icon, obj_hud)
└── Menus (spr_button_frame, obj_main_menu, obj_pause_screen)
── [04_Audio]
├── SFX (snd_player_jump, snd_player_hurt, snd_coin_pickup)
└── Music (snd_music_level_01, snd_music_title_screen)
── [05_Particles]
└── ps_dust_particles, ps_hit_sparkles
── [06_Rooms]
├── Screens (rm_menu_main, rm_victory, rm_gameover)
└── Levels (rm_level_01, rm_level_02)
2. Estandarización de orígenes de sprites y máscaras de colisión
Uno de los problemas más comunes al integrar recursos creados en momentos distintos es la inconsistencia en los puntos de origen (`xorigin`, `yorigin`) y en las dimensiones de las cajas de colisión (`bbox`). En un juego de plataformas, una ligera discrepancia de un píxel en el origen puede provocar que el personaje quede enterrado en el suelo al cambiar de animación de `spr_player_idle` a `spr_player_run`.
Para evitar estos comportamientos anómalos, adoptaremos las siguientes directrices obligatorias antes de programar la lógica integrada:
| Tipo de Entidad | Origen Recomendado | Alineación de Máscara de Colisión | Justificación Técnica |
|---|---|---|---|
| Personajes (Player, Enemigos) | Bottom-Center (Abajo-Centro) | Manual / Rectangular fija | Al cambiar de escala (`image_xscale = -1`), el personaje gira sobre su eje vertical sin desplazarse. El pie descansa con precisión en el punto Y del contacto con el suelo. |
| Bloques Sólidos y Suelos | Top-Left (Arriba-Izquierda) | Automática / Rectangular total (16x16 o 32x32) | Permite encajar perfectamente las instancias en la cuadrícula (grid) de la Room sin desfases numéricos. |
| Coleccionables (Monedas, Gemas) | Middle-Center (Centro exacto) | Manual / Círculo o rectángulo concéntrico | Facilita la animación por código (rotación con `image_angle` o pulsación con `image_xscale`) desde el núcleo del objeto. |
| Elementos de HUD / Menú | Top-Left o Middle-Center | No requiere colisión | Los textos y barras de interfaz se posicionan con exactitud calculando márgenes directos respecto a la vista de la GUI. |
💡 Regla de oro para el Player: Asigna una máscara de colisión única e invariable mediante la propiedad «Collision Mask» en el editor del objeto `obj_player`. Utiliza un sprite específico como `spr_player_mask` o mantén la máscara fijada permanentemente a `spr_player_idle`. Si dejas la máscara en «Same as sprite», cada frame de una animación compleja cambiará las dimensiones de colisión, produciendo atascos en techos y paredes.
3. Normalización de capas (Layers) en las salas
Cuando el motor ejecuta una sala (Room), el orden de dibujo y la evaluación de profundidad dependen estrictamente de las capas creadas. Si en `rm_level_01` la capa de colisiones se llama «Collisions» y en `rm_level_02` se llama «Capas_Colision», cualquier script que busque colisiones dinámicas por nombre de capa fallará de forma catastrófica.
Alinearemos todas las salas del proyecto a una estructura universal de capas, ordenadas de mayor a menor profundidad (de abajo hacia arriba en el editor de capas):
[Profundidad Menor - Frente visual]
- GUI_Layer (Capa no visible en Room editor, gobernada por evento Draw GUI)
- Instances_UI (Capa de instancias para menús interactivos en salas estáticas)
- Instances_Effects (Partículas, chispas, explosiones, polvo de salto)
- Instances_Entities (Jugador, enemigos, proyectiles, coleccionables interactivos)
- Instances_Environment (Puertas, palancas, cofres, plataformas móviles)
- Collisions (Instancias invisibles de obj_solid_wall o tilemaps de colisión sólida)
- Tiles_Foreground (Detalles visuales que tapan al jugador, ej: hojas de árboles, barandillas)
- Tiles_Main (Suelo y paredes visibles del nivel)
- Background_Front (Capas de paralaje cercano)
- Background_Back (Capa de fondo infinito o cielo lejano)
[Profundidad Mayor - Fondo absoluto]4. Centralización de constantes, estados y enums globales
Hasta este momento del curso, muchas variables globales o valores fijos (como la aceleración gravitacional o los estados del jugador) se inicializaban en el evento Create del jugador o directamente como números «mágicos» dispersos en el código. Esto dificulta el calibrado del juego. Si deseas cambiar la gravedad o comprobar en qué estado global se encuentra la partida, debes buscar en múltiples eventos.
En GameMaker moderno, la práctica profesional estándar es definir los enums, macros y funciones de inicialización dentro de un script de GML que se ejecuta automáticamente antes de que cargue la primera sala del juego. Crearemos un script llamado `scr_game_init`.
/// @file scr_game_init.gml
/// @description Define los enumeradores y macros fundamentales para todo el videojuego.
// 1. MACROS GLOBALES (Valores inmutables de configuración)
#macro GRAVITY_FORCE 0.45
#macro TERMINAL_VELOCITY 9.0
#macro TILE_SIZE 16
#macro GAME_VERSION "1.0.0"
#macro DEV_MODE false
// 2. ENUMERADORES DE ESTADO (Máquinas de estado finitas)
// Estados del jugador
enum PLAYER_STATE {
IDLE,
RUNNING,
JUMPING,
FALLING,
HURT,
DEAD
}
// Estados del bucle principal del juego
enum GAME_STATE {
BOOT,
MAIN_MENU,
PLAYING,
PAUSED,
LEVEL_TRANSITION,
GAME_OVER,
VICTORY
}
// Estados de comportamiento para enemigos terrestres
enum ENEMY_STATE {
PATROL,
ALERT,
ATTACK,
HURT,
DEAD
}
// Tipos de coleccionables
enum COLLECTIBLE_TYPE {
COIN,
GEM,
EXTRA_LIFE,
HEALTH_HEART
}Al declarar estos elementos en el ámbito de un script fuera de cualquier función envolvente, GameMaker los compila en el inicio del programa. De este modo, en cualquier objeto o evento puedes escribir `if (state == PLAYER_STATE.RUNNING)` sin peligro de que la palabra clave no esté inicializada.
5. Creación del controlador central: obj_game_manager
En lugar de obligar al objeto jugador a recordar cuántas monedas ha recogido el usuario en tres salas consecutivas o controlar si el juego está en pausa, crearemos un objeto persistente denominado `obj_game_manager`. Este objeto actúa como la «autoridad central» del sistema: sobrevive al cambio de salas, administra el ciclo de vida, almacena los datos de progresión y supervisa las condiciones globales de victoria o derrota.
Veamos la implementación detallada de sus eventos fundamentales.
// =====================================================================
// OBJETO: obj_game_manager
// EVENTO: Create
// =====================================================================
// Comprobación de patrón Singleton: asegurar que solo existe una instancia
if (instance_number(obj_game_manager) > 1) {
instance_destroy();
exit;
}
// Mantener vivo al cambiar de room
persistent = true;
// Estado inicial del sistema
current_game_state = GAME_STATE.BOOT;
// Variables de progresión global del jugador
global.player_max_health = 3;
global.player_health = global.player_max_health;
global.player_lives = 3;
global.player_score = 0;
global.player_coins = 0;
// Registro de progreso por niveles
global.current_level_index = 1;
global.total_levels = 2;
global.checkpoint_x = -1;
global.checkpoint_y = -1;
global.checkpoint_active = false;
// Configuración de audio y visuales
global.volume_sfx = 0.8;
global.volume_music = 0.6;
// Inicializar superficie de pantalla o pantalla completa si es necesario
window_set_caption("Aventuras Retro 2D - GM Platformer");
// Iniciar la transición desde la sala de arranque hacia el Menú Principal
room_goto(rm_menu_main);// =====================================================================
// OBJETO: obj_game_manager
// EVENTO: Room Start
// =====================================================================
// Identificar el contexto según la sala a la que se acaba de ingresar
switch (room) {
case rm_menu_main:
current_game_state = GAME_STATE.MAIN_MENU;
// Reiniciar datos de juego nuevo si volvemos a la portada
global.player_health = global.player_max_health;
global.player_score = 0;
global.player_coins = 0;
global.checkpoint_active = false;
break;
case rm_victory:
current_game_state = GAME_STATE.VICTORY;
break;
case rm_gameover:
current_game_state = GAME_STATE.GAME_OVER;
break;
default:
// Cualquier sala que no sea menú se asume como nivel de juego activo
current_game_state = GAME_STATE.PLAYING;
// Si existe un checkpoint registrado en esta sala, reubicar al jugador
if (global.checkpoint_active && instance_exists(obj_player)) {
obj_player.x = global.checkpoint_x;
obj_player.y = global.checkpoint_y;
}
break;
}// =====================================================================
// OBJETO: obj_game_manager
// EVENTO: Step
// =====================================================================
// Tecla rápida para activar o desactivar pantalla completa (F11 o Alt+Enter)
if (keyboard_check_pressed(vk_f11)) {
var _is_full = window_get_fullscreen();
window_set_fullscreen(!_is_full);
}
// Lógica global durante la partida activa
if (current_game_state == GAME_STATE.PLAYING) {
// Detección de pérdida total de vidas
if (global.player_lives <= 0) {
current_game_state = GAME_STATE.GAME_OVER;
room_goto(rm_gameover);
}
}6. Reutilización y refactorización de funciones auxiliares
Un síntoma clásico de código desorganizado es la duplicación de rutinas matemáticas o mecánicas. Por ejemplo, calcular la aproximación suave de una variable hacia cero (para fricción o desaceleración) o reproducir un sonido con tono aleatorio para evitar monotonía sonora son algoritmos que deben habitar en un script auxiliar general, no repetirse dentro de cada enemigo u objeto de entorno.
Crearemos el archivo `scr_utilities` para centralizar funciones matemáticas y de soporte que utilizaremos en las próximas clases de pulido e integración:
/// @file scr_utilities.gml
/// @description Colección de funciones auxiliares reutilizables en múltiples objetos.
/**
* Aproxima linealmente un valor actual hacia un valor objetivo sin sobrepasarlo.
* @param {Real} _current Valor actual que se desea transformar.
* @param {Real} _target Valor objetivo final.
* @param {Real} _step Monto de cambio absoluto por paso.
* @returns {Real} Nuevo valor ajustado.
*/
function approach(_current, _target, _step) {
if (_current < _target) {
return min(_current + _step, _target);
} else {
return max(_current - _step, _target);
}
}
/**
* Reproduce un sonido asignando una variación aleatoria de tono (pitch)
* para aportar dinamismo acústico sin saturar al jugador.
* @param {Asset.GMSound} _sound_asset Identificador del recurso de audio.
* @param {Real} _pitch_min Multiplicador mínimo de frecuencia (ej: 0.9).
* @param {Real} _pitch_max Multiplicador máximo de frecuencia (ej: 1.1).
*/
function play_sfx_pitched(_sound_asset, _pitch_min = 0.92, _pitch_max = 1.08) {
var _sound_instance = audio_play_sound(_sound_asset, 10, false);
var _random_pitch = random_range(_pitch_min, _pitch_max);
audio_sound_pitch(_sound_instance, _random_pitch);
audio_sound_gain(_sound_instance, global.volume_sfx, 0);
return _sound_instance;
}
/**
* Aplica daño al jugador de manera segura y centralizada.
* @param {Real} _amount Cantidad de puntos de daño a restar.
*/
function player_take_damage(_amount) {
if (!instance_exists(obj_player)) return;
with (obj_player) {
// Verificar si el jugador ya se encuentra en periodo de invulnerabilidad
if (!is_invincible && current_state != PLAYER_STATE.DEAD) {
global.player_health -= _amount;
if (global.player_health <= 0) {
global.player_health = 0;
current_state = PLAYER_STATE.DEAD;
v_speed = -5; // Impulso vertical dramático al morir
h_speed = 0;
} else {
current_state = PLAYER_STATE.HURT;
is_invincible = true;
invincibility_timer = room_speed * 1.5; // 1.5 segundos de gracia
v_speed = -3;
h_speed = -facing_direction * 2.5; // Retroceso (knockback)
}
// Sonido de impacto
play_sfx_pitched(snd_player_hurt);
}
}
}7. Lista de verificación (Checklist) previa al ensamblaje final
Antes de dar por concluida la estructuración del proyecto y pasar a la siguiente lección (donde ensamblaremos las pantallas de juego activas), es imperativo someter el proyecto a una prueba de diagnóstico. Revisa cada uno de los siguientes puntos en tu entorno de trabajo:
| Componente | Punto de Control | Criterio de Aprobación |
|---|---|---|
| Recursos Huérfanos | Assets no utilizados | No existen sprites de prueba vacíos ni scripts con nombres genéricos tipo 'Script1' o 'Sprite4'. |
| Puntos de Origen | Consistencia en entidades | Todos los sprites de `obj_player` y enemigos usan exactamente Bottom-Center con la misma caja de colisión. |
| Capa de Inicio | Sala de arranque (Boot Room) | `rm_boot` o `rm_init` es la primera sala en la lista de salas; solo contiene a `obj_game_manager` y salta de inmediato a `rm_menu_main`. |
| Objetos Persistentes | Duplicación de instancias | `obj_game_manager` tiene implementado el patrón de seguridad singleton (`instance_number > 1`) para evitar multiplicarse al reiniciar salas. |
| Profundidad y Capas | Nombres idénticos de layers | Todas las salas de nivel contienen capas con los mismos nombres exactos en minúsculas/mayúsculas: 'Collisions', 'Instances_Entities', 'Tiles_Main'. |
Ejercicios prácticos
Aplica lo aprendido en esta lección realizando las siguientes tres actividades estructuradas sobre el proyecto de plataformas que has venido construyendo.
Resumen
- La estructuración previa al ensamblado final garantiza que el proyecto sea mantenible, escalable y esté libre de bugs derivados de inconsistencias de nombres.
- Un estándar estricto de prefijos (spr_, obj_, snd_, rm_, etc.) y una jerarquía modular de carpetas aceleran la producción y evitan conflictos de compilación.
- La uniformidad en los puntos de origen (Bottom-Center para entidades, Top-Left para tiles y sólidos) y una máscara de colisión fija evitan atascos en la geometría del nivel.
- La creación de una sala de arranque (rm_boot) junto con un controlador central persistente (obj_game_manager) desacopla la lógica de partida de las instancias del jugador.
- El uso de enums y macros en scripts globales centraliza la configuración de la física y las máquinas de estados finitos en un único punto de referencia.
Practica lo aprendido
Ejercicio 1 · 1 punto
¿Cuál es la recomendación establecida como «regla de oro» para la máscara de colisión del objeto `obj_player`?
Ejercicio 2 · 1 punto
Relaciona cada carpeta de la jerarquía modular recomendada con los recursos correspondientes:
- [00_Core]
- [02_World_Environment]
- [03_Interface]
- [06_Rooms]
Ejercicio 3 · 1 punto
El objeto controlador `obj_game_manager` debe crearse sin un sprite asociado y marcarse con la propiedad 'Persistent' para poder sobrevivir al cambio de salas.
Ingresa para guardar tu progreso — Ingresar