Introducción al Desarrollo de Videojuegos con GameMaker · Module 10: Level Design, Cameras, and Game Progression
Level Testing (Playtesting) and Balancing
Lesson 50 of 60 · 50 min
Also available in: ES
Introduction to Playtesting: The Novice Player's Perspective
Throughout this module, you have designed the spatial layout of your rooms, configured smooth camera tracking, and established data transitions between levels. However, every novice developer faces an invisible hurdle: loss of perspective. As the creator of your game, you know every pixel-perfect jump by heart, the exact patrol pattern of every enemy, and the precise moment a trap will trigger. You always know where to go because you placed the blocks yourself.
In game design, this phenomenon is known as the "curse of knowledge." It leads developers to perceive their own levels as overly easy or predictable, often tempting them to boost enemy speeds or shrink landing platforms. The usual outcome is an unbalanced game that feels unfair and confusing to a newcomer. This is where playtesting becomes essential: the deliberate, systematic practice of observing real people interact with your game to uncover discrepancies between what you intended to convey and what the player actually experiences.
It is crucial to distinguish between technical debugging and playtesting. Debugging targets errors in code—crashes, memory leaks, or syntax bugs, which we will address formally in Module 12. Playtesting evaluates the human experience: pacing, visual clarity, the tension curve, the frustration curve, and whether your design intentions read clearly. A level can be completely bug-free while still offering a tedious, exasperating experience.
Player Psychology: The Flow Channel and Frustration
In 2D platformer design, your primary goal is to keep the player inside the "flow channel" (a theory formulated by psychologist Mihály Csíkszentmihályi). This optimal psychological state occurs when a challenge balances harmoniously with the player's current skill level. If the challenge is too low relative to their abilities, boredom sets in; if the challenge drastically outpaces their skill or fails to provide the tools to learn, it induces anxiety, frustration, and ultimately a rage quit.

Frustration in a platformer rarely stems from high difficulty alone. Celebrated titles like Celeste or Super Meat Boy feature punishing difficulty curves, yet players praise them for feeling fair. Destructive frustration arises from three primary design flaws:
1. **Lack of Information (Blindness):** The player dies to obstacles they could neither predict nor anticipate, such as blind leaps of faith into unseen pits or falling hazards with no telegraphing.
2. **Rule Inconsistency:** A solid surface stops the player in one instance but lets an enemy projectile pass through in another, or collision boxes (hitboxes) mismatch the visible sprites.
3. **Disproportionate Punishment:** Forcing the player to replay three minutes of trivial, slow platforming just to re-attempt a tricky one-second jump creates resentment and mental fatigue.
Spatial Metrics and Mechanical Tolerances
Balancing is achieved through measurement, not guesswork. Before placing enemies or hazard pits in a GameMaker room, you must formalize your avatar's core spatial metrics. These metrics depend directly on the movement physics variables implemented in Modules 4 and 5 (`vspd`, `hspd`, `grav`, `jump_speed`).
| Physics Parameter | obj_player Variable | Typical Value | Resulting Spatial Capacity (32x32 Tiles) |
|---|---|---|---|
| Max Vertical Jump | jump_speed = -9; grav = 0.4; | Approx. 101 pixels | 3 tiles high with a safe clearance margin (96 px) |
| Horizontal Jump Reach | walk_speed = 4; full jump arc | Approx. 180 pixels | 5 tiles across on a standard horizontal jump |
| Human Reaction Time | Enemy patrol speed (1.5 px/step) | Average: 250 ms | Minimum 4-tile visual cue before impact |
| Landing Clearance | Base platform width | 32 to 64 pixels | 1 tile for precision jumps, 2 tiles for standard jumps |
If you design a gap that matches the player's exact mathematical maximum reach (for example, exactly 5 full tiles to the single pixel), a one-frame error will result in death. In a standard level—outside of dedicated precision-platformers or "Kaizo" hacks—your maximum designed jump distance should fall between 70% and 85% of the character's physical limit. Providing this margin of error absorbs human execution variance without inflicting frustrating penalties.
In-Engine Diagnostic Tools: Basic Telemetry
To balance objectively, you need concrete data. During playtests, you cannot rely solely on tester recall. If three people play your level, how many times did each person die in the midsection? Exactly how long did they spend trying to clear the moving platform sequence?
You can implement a lightweight telemetry and diagnostic system directly in GameMaker. We will build a persistent manager object, `obj_playtest_manager`, that tracks deaths per room, elapsed time, and renders an on-screen debug dashboard toggled by a hotkey during testing sessions.
/// @description obj_playtest_manager Create Event
// Make this object persistent so it tracks the session across rooms.
persistent = true;
// Toggle flag for the playtest overlay
show_playtest_overlay = true;
// Tracked metrics
session_timer = 0; // Total elapsed frames across the session
room_timer = 0; // Elapsed frames in the current room
deaths_current_room = 0; // Deaths recorded in the current room
total_deaths = 0; // Cumulative deaths across the entire session
coins_collected_room = 0; // Coins collected in the current room
// Death coordinate history (used to identify spatial chokepoints)
death_positions_x = [];
death_positions_y = [];In the same object's Step event, increment the timers and configure a hotkey to toggle data visibility on screen without disrupting the playtester's run.
/// @description obj_playtest_manager Step Event
// Increment timers
session_timer++;
room_timer++;
// Toggle visual metrics overlay with F1
if (keyboard_check_pressed(vk_f1)) {
show_playtest_overlay = !show_playtest_overlay;
}
// Reset local counters upon room restart or level progression
// (Room transitions will trigger the 'Room Start' event)In the Room Start event of `obj_playtest_manager`, reset the per-room counters while preserving your cumulative session totals:
/// @description obj_playtest_manager Other -> Room Start Event
room_timer = 0;
deaths_current_room = 0;
coins_collected_room = 0;
// Clear death coordinates from the previous room
death_positions_x = [];
death_positions_y = [];Next, connect player deaths to this manager. Inside `obj_player`, wherever your character loses a life (such as colliding with a hazard or falling out of bounds, before restarting or respawning at a checkpoint), record the event:
/// Snippet to place inside obj_player death logic
if (instance_exists(obj_playtest_manager)) {
obj_playtest_manager.total_deaths++;
obj_playtest_manager.deaths_current_room++;
// Record the exact coordinates where death occurred
array_push(obj_playtest_manager.death_positions_x, x);
array_push(obj_playtest_manager.death_positions_y, y);
// Print confirmation to the GameMaker output console
show_debug_message("PLAYTEST: Death recorded in Room [" + room_get_name(room) + "] Coords: (" + string(x) + ", " + string(y) + ")");
}Finally, render these diagnostics cleanly on the screen using the Draw GUI event so you can monitor them while observing a tester:
/// @description obj_playtest_manager Draw GUI Event
if (!show_playtest_overlay) exit;
// Convert frames to seconds (assuming a target game speed of 60 FPS)
var _seconds_room = floor(room_timer / 60);
var _seconds_total = floor(session_timer / 60);
// Dashboard background styling
draw_set_font(-1);
draw_set_color(c_black);
draw_set_alpha(0.65);
draw_rectangle(10, 10, 310, 140, false);
draw_set_alpha(1.0);
draw_set_color(c_lime);
draw_text(20, 20, "-- PLAYTEST DIAGNOSTICS (F1) --");
draw_set_color(c_white);
draw_text(20, 42, "Room: " + room_get_name(room));
draw_text(20, 62, "Room Time: " + string(_seconds_room) + " s");
draw_text(20, 82, "Room Deaths: " + string(deaths_current_room));
draw_text(20, 102, "Total Deaths: " + string(total_deaths));
draw_text(20, 122, "Session Time: " + string(_seconds_total) + " s");💡 Pro tip: During a formal session with external testers, hide the overlay using F1 so the UI does not distract them or influence their play style. Let the system record data quietly to the console via show_debug_message().
Playtesting Methodology in Practice
Inviting friends, family, or colleagues to try your game sounds simple, but gathering valid, actionable feedback requires a structured protocol. Many developers make the mistake of sitting next to the player and offering running commentary: "Jump there," "Watch out, that enemy is fast," "Don't go left." "Backseat gaming" invalidates the test by masking whether the game can communicate clearly on its own.
Keep these four golden rules in mind when running effective playtest sessions:
1. **Practice Strict Silence:** Do not explain how to play, do not specify buttons, and do not hint at puzzle solutions or jump timings. If the player gets lost or confused, it is an unmistakable sign that your visual design, tutorial pacing, or signposting needs improvement.
2. **Observe Non-Verbal Cues:** Pay close attention to physical reactions. Are they furrowing their brow? Sighing? Relaxing their shoulders in relief upon landing on a platform? If a player attempts a jump five times with neutral focus, but on the sixth attempt lets go of the keyboard or checks their phone, you have pinpointed their disengagement threshold.
3. **Use the "Think Aloud" Protocol:** Politely ask participants to verbalize their thought process as they play: "I think I need to climb up there," "I'm not sure what this lever does," "That enemy felt cheap because I couldn't see it coming." This gives you direct insight into their mental model without needing to interrupt them.
4. **Ask Open-Ended Follow-Up Questions:** When the session concludes (or if the player gives up), avoid leading questions like "Did you find Level 2 fun?" Instead, ask neutral, probing questions: "Which part felt the most confusing?", "If you could remove one obstacle from that room, which would it be?", "How would you describe the character's movement pacing?"
Identifying Chokepoints and Mitigation Techniques
A chokepoint is a section of a level that accounts for an abnormally high percentage of player deaths or stall time without being an intentional narrative or mechanical climax. If your level contains 10 distinct challenges and 80% of all tester deaths occur on obstacle 4, that specific section is poorly balanced.
To smooth out chokepoints without removing satisfying challenge, developers rely on **invisible polish techniques**:
**Coyote Time:** Gives the avatar a grace window of 5 to 8 frames to execute a jump after walking off a ledge. Physiologically, the human eye perceives the character as still on the platform even after the physics engine detects empty air. Without Coyote Time, players will frequently feel that the game "ate their input."
**Jump Buffering:** If the player presses the jump button 5 frames before touching the ground, the engine stores the input and executes the jump the exact frame landing occurs. This prevents rhythmic, consecutive jumps from feeling unresponsive or clunky.
**Breadcrumbing:** Guiding player attention through visual cues. Arranging collectible coins, background torches, or gems along an arc hints at the required jump trajectory, signaling the safe path forward without explicit UI markers.
Playtesting Observation Matrix Template
Maintaining an organized log prevents you from overreacting to an isolated opinion from a single tester. A foundational rule of game design states: *"If a player tells you something feels off, they are almost always right. If they tell you how to fix it, they are almost always wrong."* Focus on identifying the underlying symptoms rather than blindly implementing player-suggested solutions.
| Tester ID | Area / Room | Observed Issue | Identified Root Cause | Balance Adjustment Applied |
|---|---|---|---|---|
| Tester_01 (Novice) | rm_level1_middle | Fell into the pit three times in a row | Blind jump: camera framing did not reveal the landing zone below | Shifted destination platform 64 px to the left and placed a guiding coin path |
| Tester_02 (Advanced) | rm_level1_enemy_corridor | Hit by patrolling enemy from behind immediately after descending ladder | Enemy speed (3 px/step) leaves insufficient reaction time | Reduced obj_enemy_patrol speed to 1.8 px/step in that corridor |
| Tester_03 (Casual) | rm_level2_boss_entry | Ignored the doorway and doubled back through the level, confused | Doorway sprite blended into the background tilemap | Added a soft light effect and an accented frame sprite to obj_door |
| Tester_04 (Average) | rm_level1_climb | Visibly frustrated having to replay the entire room after failing the final jump | No midpoint save in a vertically tall room (1800 px) | Placed an obj_checkpoint at the midpoint of the vertical climb |
Tuning Parameters in GameMaker: The Centralized Balance File
When tuning difficulty, scattering numeric values across dozens of individual objects makes balancing chaotic and error-prone. If you decide the character needs to jump 10% higher, opening `obj_player`, manually adjusting ceiling heights across four different rooms, and recalculating hazard gaps is inefficient.
A best practice in GameMaker is to centralize core balance constants inside a globally accessible Script using macros. Let's create a script in our Asset Browser named `scr_game_balance`:
/// @description scr_game_balance
// Centralized gameplay balance parameters and constants
// Player jump and movement tolerances
#macro PLAYER_GRAVITY 0.45
#macro PLAYER_WALK_SPEED 3.8
#macro PLAYER_JUMP_FORCE -8.8
#macro PLAYER_COYOTE_FRAMES 6
#macro PLAYER_BUFFER_FRAMES 5
// Standard enemy parameters
#macro ENEMY_PATROL_SPEED 1.6
#macro ENEMY_AGGRETION_RANGE 128
// Level design and pacing rules
#macro CHECKPOINT_HEAL_PLAYER true
#macro INVULNERABILITY_TIME 60 // Post-damage invulnerability duration in framesThen, inside the Create Event of `obj_player`, initialize your mechanical variables by referencing these balance macros directly:
/// @description obj_player Create Event using balance macros
grav = PLAYER_GRAVITY;
walk_speed = PLAYER_WALK_SPEED;
jump_speed = PLAYER_JUMP_FORCE;
// Dynamic tolerance counters
coyote_max = PLAYER_COYOTE_FRAMES;
coyote_counter = 0;
buffer_max = PLAYER_BUFFER_FRAMES;
buffer_counter = 0;
hspd = 0;
vspd = 0;
grounded = false;Whenever a playtest reveals that jumps feel slightly heavy or horizontal movement needs more snap, you can retune the entire game cohesively from this single file.
Practical Exercises
**Exercise 1: Implementing the Playtest Black Box** Create the `obj_playtest_manager` object according to the specifications covered in this lesson. Add it to your game's initial room and ensure its `persistent` property is set to `true`. Implement the logic required to record player death coordinates `(x, y)` and log them to the console using `show_debug_message()`. Validate its behavior by intentionally jumping into a hazard and checking the console output.
**Validation Criteria:** Pressing F1 in-game toggles the metrics overlay in the top-left corner. The GameMaker console logs an entry on every death, reporting the room name and exact pixel coordinates without causing frame drops.
**Exercise 2: Spatial Jump Metric Audit** Create a dedicated test room (`rm_testing_grounds`) outside your main game loop. Build five consecutive platforms separated by gaps of 2, 3, 4, 5, and 6 tiles (32 pixels each). Have your avatar jump across them at standard running speed, and document at which distance the gap becomes mathematically impossible to cross without outside momentum.
**Validation Criteria:** Identify your avatar's exact horizontal jump reach in tiles. Document this value and verify that no mandatory hazard gap in your campaign levels (`rm_level1`, `rm_level2`) exceeds 80% of this maximum limit.
**Exercise 3: External Playtesting Session** Export an executable build or seat an external tester (a peer, friend, or family member who has not seen the source code) in front of your two complete levels without offering verbal guidance. Take notes using the Playtesting Observation Matrix format described above. Identify at least two points of confusion or excessive friction, and apply a targeted level design or code change to resolve each.
**Validation Criteria:** Produce a completed observation table listing at least 2 observed issues, their analyzed root causes, and the specific balancing adjustments applied to your rooms or `scr_game_balance`.
Summary
- Playtesting evaluates the subjective human experience, clarity, and pacing of a game, whereas debugging focuses on finding software defects.
- Frustration stems primarily from a lack of information, inconsistent rules, or disproportionate punishment, rather than high difficulty alone.
- Level design decisions should rely on measured spatial metrics, maintaining a 15% to 30% safety buffer below the character's absolute mechanical limits.
- Basic telemetry—such as room death counters, timers, and console logs—provides objective data to identify chokepoints beyond subjective player opinions.
- During playtests with real users, remain silent, observe body language, and ask neutral, open-ended questions after the session.
- Invisible polish techniques like Coyote Time, Jump Buffering, and visual breadcrumbs prevent mechanics from feeling unfair or unresponsive.
- Centralizing movement, combat, and enemy values using macros in a dedicated balance script streamlines iterative level tuning.
Practise what you learned
Exercise 1 · 1 point
Match each cause of destructive frustration discussed in the lesson with the scenario that illustrates it:
- Lack of information (Blindness)
- Rule inconsistency
- Disproportionate punishment
Exercise 2 · 1 point
What is the fundamental difference between technical debugging and playtesting?
Exercise 3 · 1 point
For the `obj_playtest_manager` object to retain data across different rooms, its property must be set to `true`. Furthermore, when designing standard pit hazards, the recommended maximum jump distance should stay between % and 85% of the character's maximum physical reach to accommodate human margin of error.
Sign in to save your progress — Sign in