Introducción al Desarrollo de Videojuegos con GameMaker · Module 11: Complete Development of a 2D Platformer
Full Integration of Audio, HUD, and Gameplay Logic
Lesson 54 of 60 · 55 min
Also available in: ES
Introduction: Orchestrating Game Subsystems
Throughout the previous modules, you built essential components in isolation: player movement and collisions, enemy patrol and attack behaviors, stats rendering on the user interface (HUD) via the Draw GUI event, and music and sound effect (SFX) playback. While each of these parts can run perfectly inside an isolated test room, the real challenge of game development lies in making all these subsystems interact cohesively, smoothly, and reliably.
A game never feels polished if taking damage doesn't immediately reflect on the health bar, if impact sounds play with noticeable latency, or if background music continues blasting at full volume after the player is defeated. In this lesson, you will learn how to integrate and synchronize gameplay logic, the user interface, and audio into a single, unified architecture. We will ensure data flows seamlessly from physics and input events down to the audiovisual feedback delivered to the player.

Communication Architecture: Centralization vs. Dispersion
One of the most common pitfalls for novice developers is scattering presentation logic across dozens of game objects—for instance, putting UI draw calls inside `obj_player`, starting background music directly inside the Create event of an `obj_coin`, or calling `room_restart()` from random collision events. This anti-pattern, colloquially termed 'spaghetti code', produces unpredictable bugs: overlapping sounds that clip the audio mixer, duplicate HUD elements across room transitions, and desynchronized variables upon restarting a session.
To avoid this chaos, we adopt a centralized architectural pattern where every element carries a single, well-defined responsibility:
| Subsystem | Primary Object / Resource | Key Responsibility | What It Must NEVER Do |
|---|---|---|---|
| Gameplay Logic | obj_player, obj_enemy, obj_coin | Compute movement, resolve collisions, update states, and notify numerical changes. | Draw complex screen overlays or manage looping background music across room transitions. |
| Interface (HUD) | obj_hud | Read game state variables and render them cleanly using the Draw GUI event. | Directly modify health values, apply damage, or alter player movement speeds. |
| Audio | obj_audio_manager or sound scripts | Load audio assets, manage mixer priorities, modulate pitch/gain, and sustain music across rooms. | Make gameplay decisions, like triggering game over screens or level transitions. |
| Central Controller | obj_game_manager | Store persistent global state (lives, score, active checkpoints, pause state). | Run frame-by-frame physics routines like jumping gravity or ground friction. |
💡 Golden rule of integration: In-world objects (such as coins or hazards) merely notify what happened. The global manager updates the authoritative data, and dedicated output objects (HUD and Audio) translate that new state into visuals and sound.
Setting Up the Central Controller: `obj_game_manager`
Flag `obj_game_manager` as **Persistent** in the GameMaker Object Inspector. This ensures that a single controller instance survives room transitions between levels, title screens, and game over states. Below is its initial setup.
// ====================================================================
// OBJECT: obj_game_manager
// EVENT: Create
// PURPOSE: Initialize persistent global game state
// ====================================================================
// Prevent duplicate instances if the manager is spawned again
if (instance_number(obj_game_manager) > 1) {
instance_destroy();
exit;
}
// Player and session state variables
global.player_max_hp = 3;
global.player_hp = global.player_max_hp;
global.player_lives = 3;
global.player_score = 0;
global.coins_collected = 0;
global.target_coins_for_life = 50;
// Flow and transition variables
global.game_paused = false;
global.level_timer = 0;
global.current_checkpoint_x = -1;
global.current_checkpoint_y = -1;
// Track active BGM to prevent unnecessary restarts
global.current_bgm = noone;
// Lock virtual GUI resolution (standard 1080p canvas)
display_set_gui_size(1920, 1080);With state centralized in globally accessible variables modified through clean rules, we can orchestrate room transitions using the **Room Start** event. This event relocates the player when an active checkpoint exists and ensures appropriate background music plays without restarting if the incoming room uses the same track.
// ====================================================================
// OBJECT: obj_game_manager
// EVENT: Room Start
// PURPOSE: Synchronize player position, music, and persistence per room
// ====================================================================
// 1. Reposition player to active checkpoint if valid
if (instance_exists(obj_player)) {
if (global.current_checkpoint_x != -1 && global.current_checkpoint_y != -1) {
obj_player.x = global.current_checkpoint_x;
obj_player.y = global.current_checkpoint_y;
}
}
// 2. Determine and route room-specific background music
var _desired_bgm = noone;
switch (room) {
case rm_title:
_desired_bgm = snd_bgm_title;
break;
case rm_level_1:
case rm_level_2:
_desired_bgm = snd_bgm_level;
break;
case rm_victory:
_desired_bgm = snd_bgm_victory;
break;
case rm_gameover:
_desired_bgm = snd_bgm_gameover;
break;
}
// Only switch tracks if the target track differs from the active one
if (_desired_bgm != noone && global.current_bgm != _desired_bgm) {
// Stop previous music track if playing
if (global.current_bgm != noone) {
audio_stop_sound(global.current_bgm);
}
// Play new track looping with normal priority
audio_play_sound(_desired_bgm, 100, true);
global.current_bgm = _desired_bgm;
}Binding the Audio Subsystem to Gameplay Mechanics
Audio should never be an afterthought triggered blindly 'whenever something hits something else'; it needs to actively reinforce game feel and player input. When an audio system is poorly integrated, common artifacts arise: a coin pickup plays its audio clip on every single collision frame, or footsteps saturate mixer channels, resulting in audible clicks and popping.
To prevent this, we write modular helper functions that manage gain, pitch modulation, and concurrency. We'll declare a utility script named `scr_audio_effects` to encapsulate sound execution rules.
// ====================================================================
// SCRIPT: scr_audio_effects
// PURPOSE: Helper functions for controlled SFX playback
// ====================================================================
/// @function play_sfx(_sound_id, _priority, _pitch_variation)
/// @param {Asset.GMSound} _sound_id Sound asset identifier
/// @param {Real} _priority Mixer priority level (1 to 100)
/// @param {Real} _pitch_variation Random pitch offset range (e.g., 0.1)
function play_sfx(_sound_id, _priority, _pitch_variation) {
var _instance = audio_play_sound(_sound_id, _priority, false);
if (_pitch_variation > 0) {
// Apply slight pitch variance to avoid repetitive acoustic fatigue
var _random_pitch = random_range(1.0 - _pitch_variation, 1.0 + _pitch_variation);
audio_sound_pitch(_instance, _random_pitch);
}
return _instance;
}Next, see how we integrate this helper directly into the player's movement and landing logic, ensuring audio triggers only on distinct state changes.
// ====================================================================
// OBJECT: obj_player
// EVENT: Step (Jump and landing evaluation snippet)
// ====================================================================
// Cache prior grounded state before vertical evaluation
var _was_on_ground = on_ground;
on_ground = place_meeting(x, y + 1, obj_solid);
// Ground impact detection (landing)
if (!_was_on_ground && on_ground) {
play_sfx(snd_land, 40, 0.15);
// Spawn dust impact particles beneath player feet
instance_create_layer(x, bbox_bottom, "Effects", obj_dust_impact);
}
// Jump execution on input press while grounded
if (key_jump_pressed && on_ground) {
v_speed = jump_strength;
play_sfx(snd_jump, 60, 0.08);
instance_create_layer(x, bbox_bottom, "Effects", obj_dust_jump);
}Synchronizing the HUD with Global Variables
The HUD must mirror the underlying game state with zero latency, but it should also use visual polish techniques so the interface feels dynamic rather than static. Rather than drawing raw numbers that abruptly change, we implement two core 'game juice' animations in `obj_hud`:
1. **Elastic Scale Bounce on Pickups**: Whenever the player collects a coin, the coin icon pops up in scale and smoothly springs back to its rest size. 2. **Health Indicator Flash**: When the player takes damage, health hearts or bars briefly flash red or toggle opacity to communicate immediate danger.
// ====================================================================
// OBJECT: obj_hud
// EVENT: Create
// PURPOSE: Initialize local animation and interpolation variables
// ====================================================================
// Scale factors for bounce/pulse visual effects
coin_scale = 1.0;
score_display = global.player_score;
// Base interface margins (in GUI layer coordinates)
gui_margin_x = 40;
gui_margin_y = 40;
heart_spacing = 48;In the **Step** event of `obj_hud`, we interpolate displayed numerical values and damp transform scales back to resting dimensions (1.0) using linear interpolation (`lerp`).
// ====================================================================
// OBJECT: obj_hud
// EVENT: Step
// PURPOSE: Update UI animation curves and interpolated values
// ====================================================================
// Smoothly interpolate displayed score toward authoritative score
score_display = lerp(score_display, global.player_score, 0.15);
// Damp coin icon scale back to 1.0 after a bounce
coin_scale = lerp(coin_scale, 1.0, 0.1);
// If a coin was collected this frame, trigger the scale pop
if (global.coin_just_collected) {
coin_scale = 1.5;
global.coin_just_collected = false;
}Next, in the **Draw GUI** event, we render the life hearts, animated coin counter, and rolling score display using standard font and sprite drawing functions.
// ====================================================================
// OBJECT: obj_hud
// EVENT: Draw GUI
// PURPOSE: Render all UI elements to the screen overlay
// ====================================================================
// 1. DRAW HEALTH HEARTS
for (var i = 0; i < global.player_max_hp; i++) {
var _heart_x = gui_margin_x + (i * heart_spacing);
var _heart_y = gui_margin_y;
// Render full or empty heart sprite depending on current health
if (i < global.player_hp) {
draw_sprite(spr_hud_heart_full, 0, _heart_x, _heart_y);
} else {
draw_sprite(spr_hud_heart_empty, 0, _heart_x, _heart_y);
}
}
// 2. DRAW COIN COUNTER WITH DYNAMIC SCALE POP
var _coin_icon_x = gui_margin_x;
var _coin_icon_y = gui_margin_y + 60;
draw_sprite_ext(
spr_hud_coin,
0,
_coin_icon_x,
_coin_icon_y,
coin_scale,
coin_scale,
0,
c_white,
1.0
);
draw_set_font(fnt_hud);
draw_set_color(c_white);
draw_set_halign(fa_left);
draw_set_valign(fa_middle);
var _coin_str = "x " + string(global.coins_collected);
draw_text_transformed(_coin_icon_x + 36, _coin_icon_y, _coin_str, coin_scale, coin_scale, 0);
// 3. DRAW SCORE COUNTER
var _score_text = "SCORE: " + string(ceil(score_display));
draw_set_halign(fa_right);
draw_text(display_get_gui_width() - gui_margin_x, gui_margin_y, _score_text);
// Reset text alignment defaults
draw_set_halign(fa_left);
draw_set_valign(fa_top);End-to-End Coordination in Critical Game Events
To confirm that our integration is robust, let us examine two demanding game interactions: collecting items that trigger thresholds and resolving player damage/death.
Now let us trace the reverse flow: what happens when the player takes damage from a hazard or enemy and health drops to zero.
// ====================================================================
// SCRIPT: scr_player_damage
// PURPOSE: Handle player damage, trigger audio, screen shake, and defeat
// ====================================================================
function take_player_damage(_amount) {
// Exit immediately if player is invincible or already dead
if (obj_player.is_invincible || global.player_hp <= 0) return;
global.player_hp -= _amount;
// Screen shake trigger
if (instance_exists(obj_camera)) {
obj_camera.shake_intensity = 8.0;
}
if (global.player_hp > 0) {
// Player survived hit: play hurt sound and start invincibility frames
play_sfx(snd_hurt, 80, 0.05);
obj_player.is_invincible = true;
obj_player.alarm[0] = 60; // 1 second of i-frames at 60 FPS
} else {
// Health depleted: play death sound and trigger defeat sequence
play_sfx(snd_player_death, 100, 0.0);
// Decrement remaining lives
global.player_lives -= 1;
// Disable player control and launch death hop
obj_player.state = "DEAD";
obj_player.v_speed = -6; // Brief upward hop before falling offscreen
// Spawn death sequence controller to coordinate transitions
instance_create_depth(0, 0, 0, obj_death_sequence);
}
}Inside `obj_death_sequence`, after a 2-second dramatic beat where the camera watches the player fall offscreen while BGM gain fades down via `audio_sound_gain`, the controller checks whether to respawn the player at the active checkpoint or transition to `rm_gameover` when `global.player_lives <= 0`.
Subsystem Integration Matrix
Before considering a level or gameplay slice complete, run through a cross-subsystem verification matrix. This table summarizes the expected behavior across all three pillars during standard gameplay events:
| Gameplay Event | Expected LOGIC Response | Expected HUD Response | Expected AUDIO Response |
|---|---|---|---|
| Ground Jump | v_speed = -jump_speed; switch to jump sprite; on_ground = false. | No numerical changes. | Play snd_jump with slight random pitch variation. |
| Coin Pickup | Coin destroyed; global.coins_collected++. | Coin counter bounces scale and updates text. | Play snd_coin with random pitch offset; medium priority. |
| Enemy Hit (Surviving) | HP decremented; i-frames enabled. | Heart turns empty; screen flashes briefly red. | Play snd_hurt immediately; invoke camera shake. |
| Player Death (Lives > 0) | Disable inputs; player hops and falls offscreen. | All hearts display empty; life counter decrements. | Play snd_player_death; fade level music down smoothly. |
| Game Pause | Lock velocities; suspend Step logic. | Render 'PAUSED' banner and dark translucent overlay. | Halt SFX via audio_pause_all(); low-pass or attenuate music. |
| Level Clear / Goal | Disable controls; trigger victory animation. | Stop level timer; tally bonus points to score. | Stop level BGM; play snd_victory fanfare once without looping. |
Pause Management: An End-to-End Integration Test
A game's pause state is the ultimate test of subsystem integration. A poorly coordinated pause system leads to glaring bugs: sound effects looping endlessly in the background, UI animations running out of sync, or the player continuing to fire projectiles while navigating menus.
Here is how to manage pausing by coordinating logic, HUD rendering, and audio states within `obj_game_manager`:
// ====================================================================
// OBJECT: obj_game_manager
// EVENT: Step
// PURPOSE: Toggle pause state and synchronize subsystem lifecycles
// ====================================================================
if (keyboard_check_pressed(vk_escape) || gamepad_button_check_pressed(0, gp_start)) {
// Disallow pausing during title screens, game over, or victory rooms
if (room != rm_title && room != rm_gameover && room != rm_victory) {
global.game_paused = !global.game_paused;
if (global.game_paused) {
// 1. AUDIO: Pause active sounds and play menu chime
audio_pause_all();
// Play pause chime independently
audio_play_sound(snd_pause, 100, false);
// 2. LOGIC: Deactivate gameplay instances
instance_deactivate_all(true);
instance_activate_object(obj_game_manager);
instance_activate_object(obj_hud);
} else {
// 1. LOGIC: Reactivate all room instances
instance_activate_all();
// 2. AUDIO: Resume previously paused sounds
audio_resume_all();
audio_play_sound(snd_unpause, 100, false);
}
}
}💡 Caution with instance_deactivate_all(): When you deactivate instances in GameMaker, their Step and Draw events are suspended entirely. This is ideal for freezing the world, but always ensure your controller (obj_game_manager) and UI layer (obj_hud) remain explicitly active using instance_activate_object().
Practical Exercises
Apply the architectural principles learned in this lesson by completing the following structured exercises. Each task requires coordinating at least two of the three subsystems.
**Exercise 1: Low-Health Audiovisual Warning** Create a system that triggers when the player has exactly 1 heart left (`global.player_hp == 1`): - The remaining heart icon in the HUD must oscillate its draw alpha between 0.3 and 1.0 twice per second. - A looping heartbeat warning sound (`snd_heartbeat`) must play quietly in the background until the player heals or dies. *Verification:* Intentionally take damage until 1 health remains. Verify that the heartbeat starts and the heart flashes, and confirm both turn off immediately upon collecting a health potion that restores HP to 2 or more.
**Exercise 2: Coin Combo Pitch Stacking** Update coin collection so that picking up consecutive coins within 1.0 second of each other increments the playback pitch of `snd_coin` by 5% per pickup (capped at 1.8x). If more than 1.0 second passes between pickups, reset the pitch back to 1.0x. *Verification:* Place a line of 5 coins in `rm_level_1`. Run through them in quick succession and verify by ear that each pickup plays at an audibly higher pitch than the last, and that waiting 2 seconds before collecting another resets the pitch to default.
**Exercise 3: Level Goal Sequence** Create an `obj_goal_door` exit trigger. When colliding with `obj_player`, it must: 1. Lock player inputs and switch the player sprite to a victory celebration animation. 2. Stop the level BGM and play a victory fanfare sound effect (`snd_victory`). 3. Display a modal clear screen on the HUD showing final coins collected, remaining time, and total computed score. 4. Wait 3 seconds before smoothly transitioning to the next stage (`rm_level_2` or `rm_victory`). *Verification:* Reach the goal with coins and time to spare. Confirm the level music stops cleanly, the fanfare plays fully, and the numbers displayed on the clear card exactly match your global state variables.
Summary
- Robust game integration relies on strict separation of concerns: gameplay logic updates numbers and states, the HUD renders those values visually, and the audio engine delivers immediate acoustic feedback.
- A persistent obj_game_manager acts as the central coordinator, safeguarding session data across rooms while orchestrating checkpoints, background music, and pause states.
- Applying slight pitch variation to frequent sound effects prevents auditory fatigue from repetitive actions like jumping, landing, or gathering items.
- Micro-animations on the HUD (such as lerping score numbers or popping coin icons) significantly enhance game feel without cluttering core game mechanics.
- Pausing requires full subsystem synchronization: freezing physics and step routines, managing mixer playback states, and rendering menu overlays via the GUI layer.
Practise what you learned
Exercise 1 · 1 point
Match each game component with its designated responsibility according to the centralized architecture covered in the lesson:
- World objects (coins, spikes)
- Global manager (obj_game_manager)
- Output objects (HUD and Audio)
Exercise 2 · 1 point
Complete the code snippet for the Create event of `obj_game_manager` used to prevent duplicate instances if the object is reloaded: if (instance_number(obj_game_manager) > ) { (); exit; }
Exercise 3 · 1 point
Why should the `obj_game_manager` object be set as Persistent in GameMaker?
Sign in to save your progress — Sign in