Platformer
A playbook for 2D platformers — the run/jump controller "feel", level structure, hazards,
and goals. This is a compositional skill: it wires an engine movement skill, a tilemap
skill, and design skills into a working game. It does not re-teach physics or tilemaps;
it tells you what to build and how to make jumping feel good.
When to use
- Use when building a side-scrolling or single-screen platformer, a "Mario-like" /
"Celeste-like", or any game whose core verb is jump between surfaces.
- Use when a jump feels floaty, unresponsive, or "unfair" and you need feel fixes
(coyote time, jump buffering, variable height, corner correction).
When not to use: top-down movement with no gravity → use the engine movement skill
directly. 3D first-person traversal → fps-shooter. Grid/turn movement → roguelike.
For the raw kinematic body API, use godot-2d-movement (or your engine's controller skill).
Core loop
Observe a gap/hazard → commit to a jump or move → land safely (or die) → reach the next
checkpoint/goal. A platformer lives or dies on the moment-to-moment feel of that single
jump, repeated thousands of times. Tighten the controller first; everything else is content.
Must-have systems
- Run/jump controller — horizontal accel/decel, gravity, jump, with the feel aids below.
- Solid + one-way collision — ground, walls, and "jump-through" platforms.
- Level geometry — a tilemap or hand-placed colliders; the playable space.
- Hazards + death/respawn — spikes, pits, enemies; reset to the last checkpoint.
- Checkpoints / level goal — progress markers and a win condition (flag, door, exit).
- Camera — follows the player with a deadzone and look-ahead, clamped to level bounds.
- Juice — landing dust, squash/stretch, hit-stop, sound. Cheap, huge feel payoff.
Design knobs (make the jump feel right)
Tune these by outcome (height in tiles, time to apex in seconds), not by raw numbers.
| Knob |
Effect |
Sane starting point |
| Max jump height |
reach |
3–4 tiles |
| Time to apex |
"weight"/snappiness |
0.30–0.40 s |
| Fall gravity multiplier |
snappy, non-floaty fall |
1.5–2.0× rise gravity |
| Coyote time |
jump just after leaving a ledge |
0.08–0.12 s (~5–7 frames @60) |
| Jump buffer |
press just before landing still jumps |
0.10–0.15 s |
| Variable jump cut |
tap = short hop, hold = full |
cut upward velocity ×0.4–0.5 on release |
| Apex hang |
brief float at the top for air control |
reduce gravity ×0.5 near ` |
| Ground accel / friction |
responsiveness vs. ice |
reach top speed in 0.05–0.1 s |
| Corner correction |
nudge past a ledge clipped by 1–2 px |
nudge up to ~4 px sideways |
Derive gravity and jump velocity from the feel values rather than guessing — see Pattern 1.
Patterns
1. Solve jump physics from height + time (not magic numbers)
# Pseudocode. Pick the FEEL you want, then derive the physics. y-axis points DOWN.
# From kinematics: h = (g * t^2) / 2 and v0 = g * t.
JUMP_HEIGHT = 3.5 * TILE # how high, in world units
TIME_TO_APEX = 0.35 # seconds to reach the top
gravity = (2 * JUMP_HEIGHT) / (TIME_TO_APEX ** 2) # rising gravity
jump_velocity = -(2 * JUMP_HEIGHT) / TIME_TO_APEX # negative = upward
fall_gravity = gravity * 1.8 # heavier on the way down → less floaty
2. Coyote time + jump buffer + variable height (the feel core)
# Pseudocode in the per-frame update. dt = seconds since last frame.
# Timers count DOWN; refresh coyote while grounded, buffer on a fresh press.
if on_floor:
coyote_timer = COYOTE_TIME # 0.1
if jump_pressed_this_frame:
buffer_timer = JUMP_BUFFER # 0.12
coyote_timer -= dt
buffer_timer -= dt
# A jump is allowed if we pressed recently AND were grounded recently.
if buffer_timer > 0 and coyote_timer > 0:
velocity.y = jump_velocity
buffer_timer = 0
coyote_timer = 0 # consume both so we can't double-jump
# Variable height: releasing jump early while still rising cuts the arc short.
if jump_released_this_frame and velocity.y < 0:
velocity.y *= 0.45
# Asymmetric gravity: snappier fall than rise.
g = fall_gravity if velocity.y > 0 else gravity
velocity.y += g * dt
3. One-way platforms
Solid from above, pass-through from below. Most engines expose a "one-way collision" flag on
the tile/collider; enable it and let the player drop through by disabling that collision
for a few frames when the player holds Down + Jump. Do not re-implement collision math.
Pitfalls / failure modes
- Per-frame movement not scaled by
dt → speed changes with frame rate. Every velocity
integration and timer must use dt. (See physics-tuning.)
- Floaty jumps → symmetric gravity. Make fall gravity heavier than rise gravity.
- "The jump didn't register" → no input buffering. Buffer presses for ~0.1 s before landing.
- "I fell off and couldn't jump" → no coyote time. Allow a jump for ~0.1 s after leaving ground.
- Sticking to walls / catching on tile seams → use a single capsule/box collider, not
per-tile colliders, and add corner correction.
- Tunneling through floors at high speed → enable continuous collision / smaller fixed
timestep for fast bodies (see
physics-tuning).
- Camera snaps and induces nausea → smooth/lerp the follow, add a deadzone, clamp to bounds.
- Difficulty wall from bad teaching → introduce one mechanic per area before combining them.
Composition (build it from these skills)
- Controller body:
godot-2d-movement (Godot CharacterBody2D); for other engines use
the engine core + physics skill (unity-physics, phaser-arcade-physics, pygame-core).
- Levels:
godot-tilemap / unity-tilemap-2d for geometry; level-design for layout,
pacing, and teaching order.
- Feel/physics:
physics-tuning for timestep, CCD, and stability.
- Input:
input-systems for buffering, rebinding, and gamepad support.
- Polish:
audio-design for SFX/music; the engine animation skill for squash/stretch.
- Process:
prototype-fast to greybox the controller before building content.
References
- For jump math derivation, a full feel-tuning table, corner correction, moving/one-way
platforms, and camera follow, read
references/feel-tuning.md.
1---2name: platformer3description: Build a 2D platformer: run/jump control with coyote time, jump buffering, and variable jump height, plus tiled levels and hazards. Use for a platformer or Mario/Celeste-like, or tuning jump feel.4---5
6# Platformer
7
8A playbook for 2D platformers — the run/jump controller "feel", level structure, hazards,
9and goals. This is a **compositional** skill: it wires an engine movement skill, a tilemap
10skill, and design skills into a working game. It does **not** re-teach physics or tilemaps;
11it tells you what to build and how to make jumping feel good.
12
13## When to use
14
15- Use when building a side-scrolling or single-screen platformer, a "Mario-like" /
16 "Celeste-like", or any game whose core verb is **jump between surfaces**.
17- Use when a jump feels floaty, unresponsive, or "unfair" and you need feel fixes
18 (coyote time, jump buffering, variable height, corner correction).
19
20**When *not* to use:** top-down movement with no gravity → use the engine movement skill
21directly. 3D first-person traversal → `fps-shooter`. Grid/turn movement → `roguelike`.
22For the raw kinematic body API, use `godot-2d-movement` (or your engine's controller skill).
23
24## Core loop
25
26**Observe a gap/hazard → commit to a jump or move → land safely (or die) → reach the next
27checkpoint/goal.** A platformer lives or dies on the *moment-to-moment* feel of that single
28jump, repeated thousands of times. Tighten the controller first; everything else is content.
29
30## Must-have systems
31
321. **Run/jump controller** — horizontal accel/decel, gravity, jump, with the feel aids below.
332. **Solid + one-way collision** — ground, walls, and "jump-through" platforms.
343. **Level geometry** — a tilemap or hand-placed colliders; the playable space.
354. **Hazards + death/respawn** — spikes, pits, enemies; reset to the last checkpoint.
365. **Checkpoints / level goal** — progress markers and a win condition (flag, door, exit).
376. **Camera** — follows the player with a deadzone and look-ahead, clamped to level bounds.
387. **Juice** — landing dust, squash/stretch, hit-stop, sound. Cheap, huge feel payoff.
39
40## Design knobs (make the jump feel right)
41
42Tune these by **outcome** (height in tiles, time to apex in seconds), not by raw numbers.
43
44| Knob | Effect | Sane starting point |
45|------|--------|---------------------|
46| Max jump height | reach | 3–4 tiles |
47| Time to apex | "weight"/snappiness | 0.30–0.40 s |
48| Fall gravity multiplier | snappy, non-floaty fall | 1.5–2.0× rise gravity |
49| Coyote time | jump just after leaving a ledge | 0.08–0.12 s (~5–7 frames @60) |
50| Jump buffer | press just before landing still jumps | 0.10–0.15 s |
51| Variable jump cut | tap = short hop, hold = full | cut upward velocity ×0.4–0.5 on release |
52| Apex hang | brief float at the top for air control | reduce gravity ×0.5 near `|vy|`<threshold |
53| Ground accel / friction | responsiveness vs. ice | reach top speed in 0.05–0.1 s |
54| Corner correction | nudge past a ledge clipped by 1–2 px | nudge up to ~4 px sideways |
55
56Derive gravity and jump velocity from the *feel* values rather than guessing — see Pattern 1.
57
58## Patterns
59
60### 1. Solve jump physics from height + time (not magic numbers)
61
62```python
63# Pseudocode. Pick the FEEL you want, then derive the physics. y-axis points DOWN.
64# From kinematics: h = (g * t^2) / 2 and v0 = g * t.
65JUMP_HEIGHT = 3.5 * TILE # how high, in world units
66TIME_TO_APEX = 0.35 # seconds to reach the top
67
68gravity = (2 * JUMP_HEIGHT) / (TIME_TO_APEX ** 2) # rising gravity
69jump_velocity = -(2 * JUMP_HEIGHT) / TIME_TO_APEX # negative = upward
70fall_gravity = gravity * 1.8 # heavier on the way down → less floaty
71```
72
73### 2. Coyote time + jump buffer + variable height (the feel core)
74
75```python
76# Pseudocode in the per-frame update. dt = seconds since last frame.
77# Timers count DOWN; refresh coyote while grounded, buffer on a fresh press.
78if on_floor:
79 coyote_timer = COYOTE_TIME # 0.1
80if jump_pressed_this_frame:
81 buffer_timer = JUMP_BUFFER # 0.12
82coyote_timer -= dt
83buffer_timer -= dt
84
85# A jump is allowed if we pressed recently AND were grounded recently.
86if buffer_timer > 0 and coyote_timer > 0:
87 velocity.y = jump_velocity
88 buffer_timer = 0
89 coyote_timer = 0 # consume both so we can't double-jump
90
91# Variable height: releasing jump early while still rising cuts the arc short.
92if jump_released_this_frame and velocity.y < 0:
93 velocity.y *= 0.45
94
95# Asymmetric gravity: snappier fall than rise.
96g = fall_gravity if velocity.y > 0 else gravity
97velocity.y += g * dt
98```
99
100### 3. One-way platforms
101
102Solid from above, pass-through from below. Most engines expose a "one-way collision" flag on
103the tile/collider; enable it and let the player **drop through** by disabling that collision
104for a few frames when the player holds Down + Jump. Do not re-implement collision math.
105
106## Pitfalls / failure modes
107
108- **Per-frame movement not scaled by `dt`** → speed changes with frame rate. Every velocity
109 integration and timer must use `dt`. (See `physics-tuning`.)
110- **Floaty jumps** → symmetric gravity. Make fall gravity heavier than rise gravity.
111- **"The jump didn't register"** → no input buffering. Buffer presses for ~0.1 s before landing.
112- **"I fell off and couldn't jump"** → no coyote time. Allow a jump for ~0.1 s after leaving ground.
113- **Sticking to walls / catching on tile seams** → use a single capsule/box collider, not
114 per-tile colliders, and add corner correction.
115- **Tunneling through floors at high speed** → enable continuous collision / smaller fixed
116 timestep for fast bodies (see `physics-tuning`).
117- **Camera snaps and induces nausea** → smooth/lerp the follow, add a deadzone, clamp to bounds.
118- **Difficulty wall from bad teaching** → introduce one mechanic per area before combining them.
119
120## Composition (build it from these skills)
121
122- **Controller body:** `godot-2d-movement` (Godot `CharacterBody2D`); for other engines use
123 the engine core + physics skill (`unity-physics`, `phaser-arcade-physics`, `pygame-core`).
124- **Levels:** `godot-tilemap` / `unity-tilemap-2d` for geometry; `level-design` for layout,
125 pacing, and teaching order.
126- **Feel/physics:** `physics-tuning` for timestep, CCD, and stability.
127- **Input:** `input-systems` for buffering, rebinding, and gamepad support.
128- **Polish:** `audio-design` for SFX/music; the engine animation skill for squash/stretch.
129- **Process:** `prototype-fast` to greybox the controller before building content.
130
131## References
132
133- For jump math derivation, a full feel-tuning table, corner correction, moving/one-way
134 platforms, and camera follow, read `references/feel-tuning.md`.