Introducción al Desarrollo de Videojuegos con GameMaker · Module 11: Complete Development of a 2D Platformer
Structuring the Final Integrated Project
Lesson 51 of 60 · 50 min
Also available in: ES
Structuring the Final Integrated Project
Welcome to the start of Module 11. Across the previous ten modules, you have built the core mechanics of a 2D platformer piece by piece: horizontal and vertical movement physics, pixel-perfect or tilemap-based collision handling, enemy patrol AI, collectible and health systems, GUI-layer HUD rendering, audio playback, particle effects, and room transitions. However, when developing a project progressively across multiple milestones, assets, variable names, and scripts naturally end up scattered, redundant, or slightly misaligned.
In professional game development, before starting final assembly—where polished levels, narrative sequences, menus, and win/loss conditions come together—teams undergo a mandatory consolidation phase known as technical refactoring and structuring. The goal of this session is to thoroughly audit the Asset Browser, unify naming conventions, align sprite origins and collision masks, and build a clean architecture of scripts and manager objects that ensures your game runs predictably, scales easily, and remains free of memory leaks.
1. Asset Browser Audit and Naming Conventions
GameMaker allows you to organize the Asset Browser using custom folders and color tags. A common mistake among beginners is leaving dozens of sprites, objects, and audio files floating in the root directory or inside generic default folders. Once a project exceeds fifty assets, hunting for the right file slows down your workflow and increases the risk of referencing duplicate or obsolete assets.
The first rule of clean project engineering in GameMaker is enforcing a strict prefix convention. A prefix clearly identifies the asset type in GML code, preventing identifier collisions—such as having a sprite and an object share the exact same name, which breaks compilation.
| Prefix | Asset Type | Project Example | Specific Purpose |
|---|---|---|---|
| spr_ | Sprite | spr_player_run | Static graphics or animation strips for entities and environments. |
| obj_ | Object | obj_player | Logical entities containing events, variables, and gameplay code. |
| snd_ | Sound | snd_jump | Sound effects (SFX) or compressed/uncompressed music tracks. |
| rm_ | Room | rm_level_01 | Rooms or levels where instances and layers interact. |
| fnt_ | Font | fnt_hud_retro | Rasterized fonts for menus and user interface text. |
| ts_ | Tile Set | ts_industrial_ruins | Optimized tile collections used for building level geometry. |
| scr_ | Script | scr_game_init | Files containing global functions, enum definitions, and macros. |
| ps_ | Particle System | ps_dust_run | Particle systems and emitters configured via the visual editor or code. |
| seq_ | Sequence | seq_game_intro | Multi-track cinematic and UI animations. |
Beyond prefixes, grouping assets by functional domain is far more effective than throwing everything into massive, monolithic 'Objects' or 'Sprites' folders. The following modular hierarchy is the recommended standard for our platformer:
── [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. Standardizing Sprite Origins and Collision Masks
One of the most persistent bugs when combining assets made across different sessions is origin point (`xorigin`, `yorigin`) and bounding box (`bbox`) mismatches. In a platformer, an origin offset of just a single pixel can cause the player character to sink into the floor or get stuck when switching from `spr_player_idle` to `spr_player_run`.
To prevent these issues, adhere strictly to the following origin and collision standards across all game assets:
| Entity Type | Recommended Origin | Collision Mask Alignment | Technical Rationale |
|---|---|---|---|
| Characters (Player, Enemies) | Bottom-Center | Manual / Fixed Rectangle | When flipping horizontal direction (`image_xscale = -1`), the character pivots along its vertical center without shifting position. Feet consistently align with the floor contact plane. |
| Solid Blocks & Terrain | Top-Left | Automatic / Full Rectangle (16x16 or 32x32) | Allows instances to snap cleanly to the Room grid without fractional pixel offsets. |
| Collectibles (Coins, Gems) | Middle-Center | Manual / Concentric Circle or Rectangle | Simplifies programmatic animations (such as `image_angle` rotation or scaling pulses with `image_xscale`) centered on the object's anchor. |
| HUD & Menu Elements | Top-Left or Middle-Center | No collision required | Allows precise alignment and margin calculations relative to the Draw GUI surface viewport. |
💡 Golden Rule for the Player: Assign a single, immutable collision mask using the 'Collision Mask' property in the `obj_player` object inspector. Pick a dedicated invisible sprite like `spr_player_mask`, or lock the mask permanently to `spr_player_idle`. If you leave it set to 'Same as sprite', every frame of an action animation can alter the bounding box dimensions, causing the character to snag on ceilings and walls.
3. Room Layer Normalization
When the engine loads a room, draw order and depth sorting depend directly on defined layers. If your collision layer is named 'Collisions' in `rm_level_01` but named 'Solid_Collisions' in `rm_level_02`, any script looking up tilemap collisions by layer name will fail silently or crash the game.
Standardize all rooms in your project to use a single, universal layer hierarchy, ordered from foreground to background (top to bottom in the Room Layer Editor):
[Lowest Depth - Visual Foreground]
- GUI_Layer (Virtual layer governed by the Draw GUI event; not drawn in Room editor)
- Instances_UI (Instance layer for interactive menus in static screens)
- Instances_Effects (Particles, hit sparks, explosions, jump dust)
- Instances_Entities (Player, enemies, projectiles, interactive collectibles)
- Instances_Environment (Doors, levers, chests, moving platforms)
- Collisions (Invisible obj_solid_wall instances or solid collision tilemaps)
- Tiles_Foreground (Visual decorative tiles drawn in front of entities, e.g., foliage)
- Tiles_Main (Primary walkable ground and walls)
- Background_Front (Near-field parallax scrolling layers)
- Background_Back (Distant sky or infinite backdrop layer)
[Highest Depth - Furthest Background]4. Centralizing Global Constants, States, and Enums
Earlier in your learning, you may have initialized global variables or physical constants (such as gravity or state flags) inside the player's Create event, or hardcoded 'magic numbers' directly into step events. This makes fine-tuning gameplay frustrating: tweaking jump arc or checking game state requires hunting through multiple objects and events.
In modern GameMaker, professional practice defines global enums, macros, and initialization routines inside a GML script file that executes automatically before the first room loads. We will implement this setup in a script named `scr_game_init`.
/// @file scr_game_init.gml
/// @description Defines fundamental enums and macros for the entire game.
// 1. GLOBAL MACROS (Immutable configuration values)
#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. STATE ENUMS (Finite State Machines)
// Player movement and life states
enum PLAYER_STATE {
IDLE,
RUNNING,
JUMPING,
FALLING,
HURT,
DEAD
}
// Core game loop states
enum GAME_STATE {
BOOT,
MAIN_MENU,
PLAYING,
PAUSED,
LEVEL_TRANSITION,
GAME_OVER,
VICTORY
}
// Ground enemy behavior states
enum ENEMY_STATE {
PATROL,
ALERT,
ATTACK,
HURT,
DEAD
}
// Collectible item categories
enum COLLECTIBLE_TYPE {
COIN,
GEM,
EXTRA_LIFE,
HEALTH_HEART
}By placing these definitions at the root scope of a script outside of any function declaration, GameMaker compiles them at startup. As a result, you can reference `if (state == PLAYER_STATE.RUNNING)` anywhere in your codebase without worrying whether the identifier has been initialized.
5. Creating the Central Controller: obj_game_manager
Rather than forcing the player instance to track how many coins were collected across three rooms or handle game pause state, we delegate this to a dedicated, persistent object called `obj_game_manager`. This object serves as the system's central authority: it persists between rooms, oversees session progression, tracks global scores and lives, and triggers game-over or victory transitions.
Let us walk through the implementation of its core lifecycle events.
// =====================================================================
// OBJECT: obj_game_manager
// EVENT: Create
// =====================================================================
// Singleton enforcement: ensure only one manager instance exists
if (instance_number(obj_game_manager) > 1) {
instance_destroy();
exit;
}
// Retain instance across room transitions
persistent = true;
// Initial system state
current_game_state = GAME_STATE.BOOT;
// Global player progression variables
global.player_max_health = 3;
global.player_health = global.player_max_health;
global.player_lives = 3;
global.player_score = 0;
global.player_coins = 0;
// Level progression tracking
global.current_level_index = 1;
global.total_levels = 2;
global.checkpoint_x = -1;
global.checkpoint_y = -1;
global.checkpoint_active = false;
// Audio volume settings
global.volume_sfx = 0.8;
global.volume_music = 0.6;
// Set game window caption
window_set_caption("Retro Platformer 2D - GM Engine");
// Transition immediately from the boot room to the Main Menu
room_goto(rm_menu_main);// =====================================================================
// OBJECT: obj_game_manager
// EVENT: Room Start
// =====================================================================
// Update context based on the newly entered room
switch (room) {
case rm_menu_main:
current_game_state = GAME_STATE.MAIN_MENU;
// Reset session stats when returning to main menu
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:
// Any room other than menus is treated as an active gameplay level
current_game_state = GAME_STATE.PLAYING;
// Reposition player if an active checkpoint exists for this level
if (global.checkpoint_active && instance_exists(obj_player)) {
obj_player.x = global.checkpoint_x;
obj_player.y = global.checkpoint_y;
}
break;
}// =====================================================================
// OBJECT: obj_game_manager
// EVENT: Step
// =====================================================================
// Fullscreen toggle hotkey (F11)
if (keyboard_check_pressed(vk_f11)) {
var _is_full = window_get_fullscreen();
window_set_fullscreen(!_is_full);
}
// High-level session checks during gameplay
if (current_game_state == GAME_STATE.PLAYING) {
// Check for game over condition
if (global.player_lives <= 0) {
current_game_state = GAME_STATE.GAME_OVER;
room_goto(rm_gameover);
}
}6. Reusing and Refactoring Helper Functions
A classic sign of technical debt is duplicated math or utility logic across objects. For example, smoothly moving a value toward zero (for deceleration or friction) or playing an audio clip with random pitch variation to prevent acoustic fatigue should live in a shared script, not be rewritten inside every enemy and obstacle.
We will create a script named `scr_utilities` to centralize these reusable routines for our upcoming polish and assembly stages:
/// @file scr_utilities.gml
/// @description Collection of reusable helper functions across all game objects.
/**
* Moves a current value toward a target value by a given step without overshooting.
* @param {Real} _current The starting value.
* @param {Real} _target The destination target value.
* @param {Real} _step The maximum rate of change per step.
* @returns {Real} The updated value.
*/
function approach(_current, _target, _step) {
if (_current < _target) {
return min(_current + _step, _target);
} else {
return max(_current - _step, _target);
}
}
/**
* Plays an audio sound asset with randomized pitch variance
* to maintain audio freshness without clipping or fatigue.
* @param {Asset.GMSound} _sound_asset The sound resource to trigger.
* @param {Real} _pitch_min Minimum pitch factor (e.g., 0.92).
* @param {Real} _pitch_max Maximum pitch factor (e.g., 1.08).
*/
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;
}
/**
* Safely applies damage to the player and handles knockback and state transitions.
* @param {Real} _amount Total damage points to subtract.
*/
function player_take_damage(_amount) {
if (!instance_exists(obj_player)) return;
with (obj_player) {
// Check if player is currently invulnerable or dead
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; // Dramatic vertical pop on death
h_speed = 0;
} else {
current_state = PLAYER_STATE.HURT;
is_invincible = true;
invincibility_timer = room_speed * 1.5; // 1.5 seconds of invincibility
v_speed = -3;
h_speed = -facing_direction * 2.5; // Directional knockback
}
// Play hurt sound effect
play_sfx_pitched(snd_player_hurt);
}
}
}7. Pre-Assembly Checklist
Before completing project restructuring and moving on to the final screen and level assembly, run your project through this diagnostic audit checklist:
| Component | Checkpoint | Passing Criterion |
|---|---|---|
| Orphaned Assets | Unused files & test assets | No blank test sprites, placeholder sounds, or generic scripts (e.g., 'Script1', 'Sprite4') remain in the project. |
| Origin Points | Entity anchor consistency | All character sprites for `obj_player` and enemies are set to Bottom-Center with matching bounding box dimensions. |
| Boot Sequence | Startup room configuration | `rm_boot` is configured as the first room in the Room Order manager; it contains only `obj_game_manager` and immediately navigates to `rm_menu_main`. |
| Persistent Objects | Instance duplication guard | `obj_game_manager` implements singleton check logic (`instance_number > 1`) to prevent multiple instances when re-entering rooms. |
| Room Layers | Case-sensitive layer names | All gameplay rooms feature identical, correctly cased layer names: 'Collisions', 'Instances_Entities', and 'Tiles_Main'. |
Practical Exercises
Apply what you have learned by completing these three structured activities within your platformer project.
Summary
- Structuring a project prior to final assembly ensures code maintainability, scalability, and stability against naming collision bugs.
- A strict prefix standard (spr_, obj_, snd_, rm_, etc.) combined with a modular folder hierarchy speeds up development and prevents compiler reference conflicts.
- Consistent sprite origins (Bottom-Center for characters, Top-Left for tiles and walls) and locked collision masks prevent geometry snags and collision glitches.
- Using a dedicated boot room (rm_boot) with a persistent central controller (obj_game_manager) cleanly separates high-level game logic from player instances.
- Defining enums and macros inside global initialization scripts centralizes physics parameters and state machine logic in a single point of truth.
Practise what you learned
Exercise 1 · 1 point
What is the recommended "golden rule" regarding the collision mask for the `obj_player` object?
Exercise 2 · 1 point
Match each folder in the recommended modular hierarchy with its corresponding assets:
- [00_Core]
- [02_World_Environment]
- [03_Interface]
- [06_Rooms]
Exercise 3 · 1 point
The controller object `obj_game_manager` should be created without an assigned sprite and marked as 'Persistent' so that it survives room transitions.
Sign in to save your progress — Sign in