A 2D level is a sequence of decisions compressed into a flat plane. You see almost everything at once. There is no fog of war hiding the next encounter, no camera you must rotate to understand the geometry, no stealthy verticality concealing the...
In This Chapter
- The Jump Arc Determines Everything
- Designing the Platform Layout
- Enemy Placement and Encounter Design
- Tile-Based Construction vs. Procedural Generation
- Camera Behavior in 2D
- Parallax Scrolling and Depth Cues
- 2D Genre Variations
- Screen-Based Level Design
- Rhythm in 2D Levels
- Progressive Project: Building Level 1
- Closing Thought
Chapter 17: 2D Level Design — Platformers, Top-Down, and Side-Scrolling Worlds
A 2D level is a sequence of decisions compressed into a flat plane. You see almost everything at once. There is no fog of war hiding the next encounter, no camera you must rotate to understand the geometry, no stealthy verticality concealing the puzzle. The player sees the platforms, sees the gap, sees the spike, and decides. Because the player sees so much, you — the designer — must control what they see, when they see it, and what they understand about it. Every pixel on screen is a teaching opportunity or a lie.
This chapter is the practitioner's deep dive into 2D level design. You will design jump arcs, place enemies, configure cameras, and build a tilemap level in Godot. You will learn why coyote time changed platformers forever, why the camera in Super Mario World looks ahead when Mario runs right, and why Celeste's introduce-test-twist-master pattern is now industry orthodoxy. By the end, Level 1 of your platformer will exist as a working Godot scene, with collision, enemies, and a camera that respects rooms.
The Jump Arc Determines Everything
Before you draw a single platform, you must know how high your character jumps and how far. These two numbers — peak height and horizontal distance — are the constants of your universe. Every platform spacing, every enemy gap, every coin placement is downstream of the jump arc.
Consider a character with a jump height of 3 tiles and a horizontal jump distance of 5 tiles (at 16-pixel tiles, that is 48 pixels up and 80 pixels across). You now know the maximum gap they can cross from a flat surface: less than 5 tiles. You know the maximum height of a platform they can reach from below: less than 3 tiles. You know that a corridor with a 3-tile-high ceiling will allow them to barely jump beneath it. The arc is the ruler. You design with it.
💡 Intuition: If your jump arc changes mid-development, every level you have already built becomes either trivial or impossible. Lock the jump physics before you build levels — or expect to rebuild every level you have.
The classic 2D jump is not a single number. It is a curve. Players experience the jump as height-over-time, and the shape of that curve has a name: the jump apex. The character rises quickly, slows near the top, hangs for a fraction of a second, and falls. That hang at the top — the apex peak control — is what makes a jump feel "good." Without it, the jump feels mechanical and abrupt. With too much of it, the jump feels floaty and untethered.
The trick is variable gravity. During the rise phase, you apply a moderate gravity. Near the top of the jump, you reduce gravity (or even zero it out for a few frames). During the fall, you apply a higher gravity to make the descent feel weighty. Mario does this. Celeste does this. Hollow Knight does this. Almost every great 2D platformer does this. The unmodified gravity = 9.8 of beginner physics is a beginner's mistake.
# In CharacterController.gd, _physics_process:
if velocity.y < 0 and abs(velocity.y) < APEX_THRESHOLD:
# Near peak — reduce gravity for hang time
velocity.y += GRAVITY * APEX_GRAVITY_MULT * delta
elif velocity.y > 0:
# Falling — heavier gravity for snappy descent
velocity.y += GRAVITY * FALL_GRAVITY_MULT * delta
else:
# Rising — normal gravity
velocity.y += GRAVITY * delta
Variable jump height is the second arc-shaping technique. If the player releases the jump button early, the character should not complete the full jump — they should peak short. This is implemented by clamping upward velocity when the button is released:
if Input.is_action_just_released("jump") and velocity.y < 0:
velocity.y *= 0.5 # Cut jump short
This single line — the "jump cut" — gives players precise control over jump height without adding any new inputs. A tap is a small hop. A held button is a full jump. Players learn this in seconds and use it for the rest of the game.
🚪 Threshold Concept: A jump in a 2D platformer is not a physics simulation. It is a controllability illusion built from variable gravity, jump cuts, coyote time, and input buffering. Once you understand that the "jump" is a designed feel, not a physics fact, you can design jumps that feel like nothing else.
Coyote Time
Coyote time is the small grace period after the player walks off a ledge during which the jump button still works. The name comes from Wile E. Coyote, who famously runs off cliffs and only falls when he notices. In a real platformer, if the player presses jump on the same frame the character leaves the ledge, you want the jump to succeed — even though, technically, the character is no longer on the ground.
Without coyote time, players miss jumps they "should" have made. They blame themselves. With coyote time (typically 4-8 frames, or about 0.05-0.13 seconds), they make the jump and never know the system helped them.
var coyote_time := 0.1 # 100ms grace
var coyote_timer := 0.0
func _physics_process(delta):
if is_on_floor():
coyote_timer = coyote_time
else:
coyote_timer -= delta
if Input.is_action_just_pressed("jump") and coyote_timer > 0:
velocity.y = JUMP_VELOCITY
coyote_timer = 0 # Consume
✅ Best Practice: Tune coyote time on real players, not in isolation. Too little and players feel the game is unfair. Too much and the game feels mushy and forgiving. Celeste uses ~6 frames. Hollow Knight uses ~5. Start at 6 and adjust based on playtest data.
Input Buffering
Input buffering is coyote time's mirror. If the player presses jump just before landing, you want the jump to execute on the frame they land — not be ignored because they were "in the air." Buffer the input for a few frames, and consume it the moment it becomes valid.
var jump_buffer_time := 0.1
var jump_buffer_timer := 0.0
func _physics_process(delta):
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = jump_buffer_time
else:
jump_buffer_timer -= delta
if is_on_floor() and jump_buffer_timer > 0:
velocity.y = JUMP_VELOCITY
jump_buffer_timer = 0
Coyote time and input buffering are the two halves of "forgiving" platformer controls. They make the game feel responsive and the player feel competent. Together, they are responsible for more good reviews than any other single design choice in the genre.
⚠️ Common Pitfall: First-time platformer designers often skip coyote time and input buffering, believing that "real" platformers require frame-perfect inputs. They are wrong. Even Super Meat Boy — the genre's hardness benchmark — has coyote time and input buffering. Forgiveness is not difficulty's enemy. It is its prerequisite.
Designing the Platform Layout
You have your jump arc. You have your forgiving controls. Now you place platforms.
The fundamental rule: every platform should be reachable through a known move set. If the player has only a basic jump, every reachable platform must lie within the basic jump arc from somewhere they can stand. If the player has a double jump, the second jump can extend reach — but only after they've learned the double jump in a safe context.
A typical sequence in a tutorial area: 1. Introduce: A single gap of 3 tiles. Easy. Player learns "jump." 2. Introduce more: A gap of 4 tiles. Slightly harder. Player learns "timing matters." 3. Test: A gap of 4 tiles followed by a higher platform of 2 tiles up. Player must combine timing with arc estimation. 4. Twist: A 4-tile gap with a moving platform on the far side. Player must time their jump to a moving target. 5. Master: A sequence of three gaps in rapid succession, requiring rhythm.
This is the introduce-test-twist-master pattern, popularized by Mark Brown's analysis of Super Mario games. We will revisit it in detail in the Celeste case study, but you should internalize it now: every mechanic in your level should follow this arc. Show it. Make them try it. Change something about it. Then ask them to use it under pressure.
📝 Note: Mario 3, Mario World, and every modern Nintendo platformer use this pattern almost religiously. Open the first world of any of these games and count the introductions, tests, twists, and masters. They are everywhere, and they are intentional.
The Golden Ratio of Challenge Density
How many challenges should appear per screen? Too few, and the level feels empty. Too many, and the player feels overwhelmed. The "golden ratio" — and this is craft heuristic, not a formula — is roughly 2-3 distinct challenges per screen of action, with breathing room between them.
A "challenge" is anything that demands a decision: a jump, an enemy, a moving hazard, a destructible block. A screen full of identical jumps is one challenge repeated, not many challenges. A screen with one tricky jump, one enemy to avoid, and one optional collectible to risk reaching is three challenges and feels satisfyingly dense.
Pacing varies by genre. A precision platformer like Celeste packs 4-6 distinct challenges into many screens. A relaxed Sonic level might have 1-2 with long stretches of pure speed. A Metroidvania varies — exploration screens might have 1, combat arenas 4-5.
🎯 Tradeoff Spotlight: High challenge density makes levels feel dense and meaty but increases frustration risk. Low density makes levels feel breezy and atmospheric but risks boredom. The right answer depends on your game's emotional target. A horror platformer wants long quiet stretches punctuated by sharp scares. A score-attack arcade game wants relentless density.
Enemy Placement and Encounter Design
Enemies are not decoration. They are level design. Where you place them, how they move, and what they force the player to do are choices about pacing, threat, and skill expression.
Begin with the simplest principle: enemies should be visible before they are dangerous. The player must see the enemy, understand its behavior, and then engage. Enemies that appear from off-screen and instantly damage the player feel unfair. Enemies that walk into view and pace back and forth telegraph their patrol and let the player plan.
Position enemies at decision points. A goomba in the middle of a long corridor is filler. A goomba on the platform you must land on is a puzzle. A goomba on the moving platform you are tracking is a multi-variable challenge.
🎮 Play This: Open Super Mario World, Donut Plains 1. Walk through the first 30 seconds. Notice how every enemy is positioned at a jump landing point, a corridor pinch, or a coin lure. None are random. None are ignorable. Each one teaches something.
Enemy combinations multiply complexity. A koopa alone is a simple obstacle. A koopa on a platform with a piranha plant below is a stack: dodge the plant, defeat the koopa, kick the shell. Layering enemies creates emergent puzzles without writing new code.
A useful rule for enemy encounters in early levels: one new enemy or one new hazard per screen, never both at once. Players need cognitive bandwidth to learn. Throwing two unfamiliar threats at them simultaneously fragments attention and turns learning into trial-and-error.
# EnemySpawner.gd — position-based enemy placement tied to level flow
extends Node2D
@export var enemy_scene: PackedScene
@export var spawn_positions: Array[Vector2] = []
@export var spawn_when_camera_visible: bool = true
var spawned: Array[Node2D] = []
func _ready() -> void:
for pos in spawn_positions:
var enemy := enemy_scene.instantiate() as Node2D
enemy.position = pos
add_child(enemy)
spawned.append(enemy)
if spawn_when_camera_visible:
enemy.set_process(false) # Pause until visible
In the Godot inspector, you populate spawn_positions by placing markers in the scene where you want enemies. The spawner reads the array and instantiates enemies at those exact spots. The spawn_when_camera_visible flag ensures enemies don't burn CPU until they're actually on screen — important for large levels.
🛠️ Design Exercise: Take a screen of your level. Mark every spot where you want the player to make a decision. Now mark every spot where you have placed an enemy. Do they overlap? They should. Decisions without consequences are filler. Consequences without decisions are punishment. Their overlap is where the design lives.
Tile-Based Construction vs. Procedural Generation
There are two philosophies for building a 2D level: hand-place every tile, or generate them algorithmically. They produce profoundly different games.
Tile-based hand-placement is the traditional approach. You open a tilemap editor and paint platforms one at a time. Every collision shape, every spike, every coin is exactly where you put it. The result is craftsmanship: levels that feel composed, intentional, and replayable in the same way a song is replayable. Mario, Celeste, Hollow Knight — all hand-built.
Procedural generation is the alternative. An algorithm constructs the level at runtime from rules and seed values. Spelunky, The Binding of Isaac, Dead Cells — all procedural. The result is variety: every run is different, no level can be memorized, and the game's depth comes from systems interacting unpredictably.
🎯 Tradeoff Spotlight: Hand-built levels are denser, more memorable, and tighter — but finite. Procedural levels are infinite and surprising — but rarely as tight or memorable. Your game can use one, the other, or a hybrid (Spelunky uses hand-built room templates assembled procedurally). Pick based on whether you value re-experience or re-discovery.
This chapter focuses on hand-built tilemap design because it teaches the fundamentals of intentional placement. You cannot design good procedural rules until you understand what makes hand-built levels work. Once you can hand-build a great level, you can write rules that approximate that quality.
Setting Up a Tilemap in Godot
Godot's TileMap node is your canvas. A TileMap consists of a TileSet (the source palette of tiles) and a grid of cells where you paint tiles. Each tile in the TileSet can have collision shapes, custom data layers, and properties.
# TileMapSetup.gd — programmatic TileMap configuration
extends TileMap
@export var ground_layer: int = 0
@export var hazard_layer: int = 1
func _ready() -> void:
# Configure layers
if get_layers_count() < 2:
add_layer(-1)
set_layer_name(ground_layer, "Ground")
set_layer_name(hazard_layer, "Hazards")
# Enable collision on ground layer
set_layer_modulate(ground_layer, Color.WHITE)
set_layer_y_sort_enabled(ground_layer, false)
# Hazards on a separate layer for collision masking
set_layer_modulate(hazard_layer, Color(1, 0.8, 0.8))
print("TileMap initialized: ", get_layers_count(), " layers")
func paint_floor(start: Vector2i, length: int, tile_id: int) -> void:
for i in range(length):
set_cell(ground_layer, start + Vector2i(i, 0), 0, Vector2i(tile_id, 0))
This script wires up two layers — ground and hazards — and exposes a paint_floor helper. In practice, you'll paint most tiles by hand in the editor, but having programmatic helpers is invaluable for procedural touches (random decoration, generated platform sequences) or for dynamically destroyed tiles (a wall that crumbles).
The tile collision shapes are configured in the TileSet itself. Open the TileSet resource, select a tile, and open the Physics tab. Draw a polygon over the tile to define its collision shape. Square tiles get full-cell rectangles. Sloped tiles get triangular polygons. Half-height platforms get half-cell rectangles. The CharacterController will collide with these shapes automatically.
⚡ Quick Reference: Common collision shapes for 2D platformer tiles: - Solid block: Full 16×16 rectangle - Half-floor: Bottom 16×8 rectangle - 45° slope (rising right): Triangle from bottom-left to bottom-right to top-right - One-way platform: Top edge only — use Godot's
one_way_collisionflag - Spike: Small triangle, place in hazard layer with damage flag
The "Tag Layer" Trick
Power-tilemap designers add custom data layers to their TileSets. A boolean "is_climbable" layer marks tiles you can grip. A string "biome" layer marks which musical track to play in each region. An integer "damage" layer says how much hurt a hazard tile causes.
In Godot, you add these in the TileSet's "Custom Data Layers" panel, then read them at runtime:
var damage = get_cell_tile_data(hazard_layer, cell_pos).get_custom_data("damage")
if damage > 0:
player.take_damage(damage)
This lets you author behavior at the tile level instead of writing a switch statement for every special tile. It is the quiet superpower of modern tilemap engines.
🎓 Advanced: Custom data layers can store anything: callbacks, dictionaries of properties, state flags. A team mate of mine once stored entire enemy spawn tables in a tile's custom data, so painting one tile would generate an encounter. Use sparingly — too much tile-side logic becomes hard to debug — but the technique is powerful.
Camera Behavior in 2D
The camera is a character. It has personality, intent, and timing. A bad camera ruins a great level. A great camera elevates an average one.
The four basic 2D camera behaviors:
1. Follow Camera (Center-Lock)
The camera is locked to the player's center. Wherever the player moves, the camera moves the same distance. Simple, predictable, and used in Pokémon, Stardew Valley, and many top-down games. Its weakness: it shows nothing the player isn't looking at, which can make exploration feel cramped.
2. Window Camera
The camera doesn't move until the player approaches the edge of a "window" rectangle in the center of the screen. As long as the player stays inside the window, the camera holds still. When the player reaches the window's edge, the camera starts scrolling. This is what most arcade-era platformers used. It allows fast players to see ahead while letting slow players orient themselves.
3. Dead Zone Camera
A more flexible window: the camera has a dead zone where it doesn't react, and outside that zone, it accelerates to catch up. The dead zone might be small (responsive camera) or large (lazy, cinematic camera). Modern platformers usually use this with smoothing.
4. Lookahead Camera
The camera offsets in the direction the player is moving. If you're running right, the camera shifts right so you can see what's coming. If you stop, it slowly recenters. This is what Sonic and Super Mario World do. The lookahead transforms the camera from a follower into a scout.
# Camera2DController.gd — smooth follow with room boundaries and lookahead
extends Camera2D
@export var target: Node2D
@export var follow_speed: float = 8.0
@export var lookahead_distance: float = 80.0
@export var lookahead_smoothing: float = 4.0
@export var room_min: Vector2 = Vector2.ZERO
@export var room_max: Vector2 = Vector2(2000, 600)
var current_lookahead: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
if not target:
return
# Compute lookahead based on target's velocity
var velocity := Vector2.ZERO
if "velocity" in target:
velocity = target.velocity
var desired_lookahead := Vector2(sign(velocity.x) * lookahead_distance, 0)
current_lookahead = current_lookahead.lerp(desired_lookahead, lookahead_smoothing * delta)
# Compute desired camera position
var desired := target.global_position + current_lookahead
# Clamp to room boundaries
desired.x = clamp(desired.x, room_min.x, room_max.x)
desired.y = clamp(desired.y, room_min.y, room_max.y)
# Smooth toward desired position
global_position = global_position.lerp(desired, follow_speed * delta)
This controller does four jobs at once. It follows the target. It looks ahead in the direction of motion. It smooths its motion over time. It respects room boundaries so the camera doesn't show you what's beyond the level. Each of these is a tunable value: tweak follow_speed for tightness, lookahead_distance for how far the camera scouts, lookahead_smoothing for how snappy the lookahead reacts.
💡 Intuition: A camera that snaps instantly to the player feels jarring. A camera that drifts slowly feels lazy. The right speed depends on the game's pace. A frenetic action platformer wants
follow_speed = 12-15. A calm exploration game wantsfollow_speed = 4-6. Tune by feel, not by formula.
Room Boundaries and Camera Constraints
In old platformers (Castlevania, Metroid), the world was divided into rooms. The camera locked at room boundaries. Crossing a doorway transitioned to a new room. This room-based design is the foundation of Metroidvania level structure.
Modern 2D games still use room boundaries for camera control even when transitions are seamless. The boundary tells the camera "do not show beyond here." It prevents you from seeing the next area's secrets prematurely. It also enables boss arenas where the camera locks during combat and unlocks when the boss dies.
Implement room boundaries with simple Area2D nodes that, on entry, update the camera's room_min and room_max. The camera continues following the player but clamps its position within the new room. Smooth transitions between rooms become a matter of lerping room_min and room_max over a fraction of a second.
🔄 Check Your Understanding: Why does the camera need to be a separate node from the player? Could you not just attach a Camera2D as a child of the player? You can — but then the camera moves rigidly with the player, with no smoothing, no lookahead, and no room constraints. A separate camera with the player as
targetis the standard pattern because it decouples motion from observation.
Parallax Scrolling and Depth Cues
A 2D world is flat. The player knows it's flat. But you can suggest depth by having background layers move slower than foreground layers when the camera moves. This is parallax scrolling, and it's been a 2D staple since Moon Patrol (1982).
Distant mountains move at 10% of the camera's speed. Mid-range trees move at 50%. The foreground moves at 100%. The result: when the player walks right, the world feels three-dimensional even though it's painted on flat planes.
In Godot, the ParallaxBackground node and its ParallaxLayer children handle this automatically. You set each layer's motion_scale to a value between 0 and 1, and the engine moves the layer by that fraction of the camera's motion. Setting motion_scale = 0.1 puts a layer effectively at infinity. Setting it to 0.9 puts it just behind the foreground.
✅ Best Practice: Three parallax layers is enough for most games: far background (clouds, mountains), mid background (trees, distant buildings), foreground (the playable plane). More layers add visual richness but also performance cost and authoring complexity. If you find yourself wanting four or five layers, ask whether one of them is doing real work or just decorating.
Other depth cues to consider: - Atmospheric perspective: Distant objects are lower contrast and tinted toward the sky color - Scale: Smaller silhouettes read as further away - Occlusion: Foreground elements partially obscure midground; midground partially obscures background - Lighting: A warm-lit foreground against a cool-tinted background sells separation
2D Genre Variations
The principles above apply to all 2D games, but each genre warps them differently.
Platformer (Vertical Emphasis)
The defining axis is vertical. Jumps are the primary verb. Falls are the primary risk. Levels read mostly left-to-right but reward looking up and down. The camera lookahead is horizontal but anticipates vertical leaps.
Examples: Super Mario Bros., Celeste, Hollow Knight, Ori and the Blind Forest.
Top-Down (Zelda-Style)
The camera looks down. Movement is on a horizontal plane (north, south, east, west). There are no jumps, but there are walls, water, holes, and bridges. Combat is melee or ranged in eight directions. Levels are often designed as interconnected rooms with locked doors.
Examples: The Legend of Zelda (NES), A Link to the Past, Hyper Light Drifter, Tunic.
Side-Scrolling Action (Castlevania-Style)
A platformer with combat as primary verb. Jumps exist but are short and tactical. Enemy variety is high; encounters are arena-like. Levels often progress through linear corridors punctuated by combat rooms.
Examples: Castlevania, Symphony of the Night, Blasphemous, Salt and Sanctuary.
Metroidvania (Interconnected 2D Worlds)
A side-scroller where the entire world is one connected map. New abilities unlock new paths. The map is the puzzle. Save rooms and warp points enable backtracking. Often combines platformer movement with action combat and lock-and-key progression.
Examples: Super Metroid, Hollow Knight, Ori, Axiom Verge, Dead Cells.
Twin-Stick Shooter
Top-down, but movement is on the left stick and aiming is on the right stick (or mouse). Levels are often arenas: rooms where enemies pour in until you survive. Bullet patterns and cover use are primary skills.
Examples: Smash TV, Geometry Wars, Enter the Gungeon, Helldivers (top-down portions).
Puzzle Platformer
A platformer where movement skill is secondary to spatial reasoning. Each room is a puzzle. Death is cheap (instant retry). The tension is mental, not mechanical.
Examples: Braid, Limbo, Inside, Fez, Patrick's Parabox.
📝 Note: These genres aren't watertight. A Metroidvania might have puzzle rooms. A platformer might have twin-stick combat sections. The genre labels help you communicate intent and inherit player expectations — but the actual design is yours to combine.
Screen-Based Level Design
Some 2D games eschew scrolling entirely. The level is a single screen. When you complete it, you transition to the next. VVVVVV, original Donkey Kong, Bubble Bobble, and notably the structure within Celeste's individual rooms all use this pattern.
The constraint of a single screen is a creative gift. With no scrolling, you cannot hide content. Every challenge is visible from the start. You must design challenges that read fully from a glance and resolve in seconds. The screen becomes a puzzle box.
VVVVVV is the canonical example. Each room has a name (like a museum exhibit's title card), a color scheme, and a single mechanical idea. "Edge Games" tests the gravity flip near edges. "Doing Things The Hard Way" tests precision under time pressure. "I'm Sorry" — well, you have to play it.
🎮 Play This: Play VVVVVV's first 20 rooms. Observe how each room introduces a single idea, resolves in under a minute, and rewards you with a transition to the next. Now compare to a Mario level, where many ideas are stacked into one continuous flow. Which feels denser? Which feels easier to master? Both work — but they teach different things.
Celeste is a hybrid. The chapters are continuous, but each "screen" within a chapter is a self-contained challenge. When you die, you respawn at the start of the current screen, not the start of the chapter. This dovetails with the introduce-test-twist-master pattern: each screen can introduce one twist, test it once, and end. The next screen builds on it.
🚪 Threshold Concept: Screen-based design forces clarity. If your room makes sense in one frame, it has been designed well. If it requires the player to scroll-explore to understand, you have either added complexity for its own sake or you have not committed to the single-screen idea. Either outcome is a design decision worth making consciously.
Rhythm in 2D Levels
A great platformer level has rhythm. The platforms are spaced like beats in a song. Jumps cadence to a tempo. The player feels the level rather than analyzes it.
Rayman Origins (and especially Rayman Legends) made this literal: certain levels are explicitly musical, with platforms that appear on the beat and enemies that move to the rhythm. But even non-musical games have implicit rhythm. Mario's "1-1" has a clear cadence: jump, jump, big-jump, descend, jump-jump-jump-flagpole. Replay it humming, and you'll find a song.
The rhythm comes from spacing. Equal-distance gaps create steady rhythm. Variable spacing creates syncopation. A long pause before a hard jump creates anticipation. A burst of three quick jumps in a row creates intensity.
🛠️ Design Exercise: Pick a 30-second song. Sketch a Mario-style level whose challenges align with its beats. Where the song has a drum hit, place a jump. Where it crescendos, escalate the difficulty. Where it has a quiet moment, give the player a coin or a vista. Even if you never use this level, the exercise teaches you to feel level design as music.
Progressive Project: Building Level 1
You will now build the first level of your platformer in Godot. We'll use a tilemap, place enemies, configure the camera, and apply the introduce-test-twist-master pattern.
Step 1: Create the Tilemap
Open Godot. Create a new scene with a TileMap node as the root. Import or create a 16×16 tileset with at least: solid blocks, floor edges, slopes, spikes, and one-way platforms. Configure collision shapes for each tile in the TileSet editor.
Step 2: Paint the Level
Lay out the level in three sections, each ~3 screens wide:
Section A — Introduce: A flat starting area. One small gap (3 tiles wide). One enemy patrolling between two walls. Player learns: walk, jump, enemy avoidance.
Section B — Test: A series of three gaps of increasing size (3, 4, 5 tiles). One enemy on a platform between them. Player must combine jumping with enemy avoidance.
Section C — Twist + Master: A vertical climb section with one-way platforms. Two enemies positioned at decision points. Final platform is a jump from a moving floor (if you have one — otherwise a tight jump under a low ceiling).
📐 Project Checkpoint: By the end of Section B, you should have a player who walks, jumps, and avoids one enemy across three screens of content. If your character cannot make any of the jumps, your jump arc is wrong — or your gaps are too wide. Adjust gap widths first; rebuilding physics affects every section.
Step 3: Add the Camera
Add a Camera2D node to your scene. Attach Camera2DController.gd from the code above. Set its target to your player. Set room_min and room_max to the corners of your level. Test: walk left and right, watch the lookahead engage. Walk to the edges of your level — the camera should clamp.
Step 4: Place Enemies
Add an EnemySpawner node. Set enemy_scene to your enemy scene. Add positions in the inspector for each enemy spawn point. Run the level. Verify enemies appear and behave.
Step 5: Apply Introduce-Test-Twist-Master
Walk through your level as a playtester. Mark every spot where the player must use a skill. For each skill, ask: - Did I introduce it (show it in safe context)? - Did I test it (require it to progress)? - Did I twist it (combine it with something else)? - Did I master it (require it under pressure)?
If any answer is no, redesign that section. This is the iteration loop.
🪞 Learning Check-In: Stop. You've now built a level. Before reading further, play it three times. What feels good? What feels bad? Which platform are you most proud of? Which enemy do you wish you could move? The answers are your design intuition emerging — write them down.
Closing Thought
A 2D level is honest. The player can see what you've done. There is no hiding behind atmosphere or fog. Every decision about a tile, a jump, a camera lookahead is visible in seconds. This is intimidating. It is also liberating.
Once you accept that the player will see everything, you stop designing for surprise and start designing for clarity. Once you accept that they will master your jump within seconds, you start designing twists. Once you accept that the camera is a character, you start giving it personality.
The next chapter takes us into 3D — where the camera becomes a beast of its own, and the player can no longer see everything. But the principles you've learned here — introduce-test-twist-master, visibility before threat, rhythm, room-based pacing — all carry forward. 2D is where 3D learns to walk.
Now go build your level. The tools are in your hands.