Learning Goals
3 minBy the end of this lesson you can:
- Explain why a jump belongs in
on_key_downand not inupdate(). - Use
on_groundto stop mid-air jumping, and work out how high a jump reaches. - Resolve sideways collisions in a separate pass, and explain why one axis at a time is not optional.
- Flip a sprite with
flip_xonly on the frame the direction actually changes.
Warm-Up · Held or Pressed?
5 minYou have met two ways to read the keyboard:
keyboard.ainsideupdate()— true on every frame the key is held down.on_key_down(key)— called once, at the moment the key goes down.
Decide which one suits each of these, and say why in one sentence:
- Walking left while the key is held.
- Firing a single arrow.
- Jumping.
- Charging a shot for as long as you hold the button.
If you test the jump with keyboard.space inside update(), holding the key re-launches the player 60 times a second and he skims along the ceiling. It is a good bug to see once, because it explains the rule better than the rule does.
New Concept · One Axis at a Time
12 minLast lesson you repaired overlaps after moving vertically. Walls are the same trick on the other axis — but it has to be its own separate pass, immediately after the horizontal move.
Your update() ends up with a strict shape, and the order is not decoration:
- read the keys into
vx - move on x — then fix any overlap sideways only
- apply gravity to
vy, move on y — then fix any overlap vertically only
Suppose you moved both directions at once and then found an overlap. The player is inside a tile — but did he arrive through the side or through the top? You would have to guess, and the usual guess (whichever overlap is smaller) fails exactly when it matters: run along a flat floor and every so often the game decides you came in sideways and shoves you to a halt. Players describe this as "catching on invisible corners".
Handling one axis at a time removes the question. After the x move, the only way in was sideways. After the y move, the only way in was vertically. There is nothing left to guess.
How high is 14? You can work it out rather than guess. A jump rises until gravity has cancelled the launch speed, which gives a height of JUMP_SPEED² / (2 × GRAVITY) — here 14² / 1.2, about 163 pixels. The level from lesson 3 puts platforms 80–110 pixels apart vertically, comfortably inside that. Make a platform more than 163 pixels above the one below it and it becomes unreachable.
Build Log · Stages 07–09
14 minJump
A jump is one shove upward. Gravity, already running, takes care of the arc and the landing for free.
platformer.py · add these pieces
JUMP_SPEED = -14 # negative because y grows downward def on_key_down(key): if key == keys.SPACE and player.on_ground: player.vy = JUMP_SPEED player.on_ground = False
on_key_down is another magic name: Pygame Zero calls it once at the exact moment a key is pressed. The and player.on_ground is what stops mid-air jumping — and it is the whole reason lesson 3 bothered to record it.
Run it — checkpoint 07
- Space makes the player jump, and he lands again.
- Holding Space does nothing extra, and you cannot jump while already in the air.
- You can now jump onto the floating platforms and stand on them.
Stop at walls
Right now you can walk sideways straight through the edge of a platform. Same repair as lesson 3, other axis, own pass.
platformer.py · add directly after player.x += player.vx
for p in platforms: if overlaps(player, p): if player.vx > 0: player.right = p.left # walked into a left-hand face elif player.vx < 0: player.left = p.right # walked into a right-hand face
Run it — checkpoint 08
- Jump onto a floating platform, then walk into the side of a taller one: you stop dead against it instead of sliding inside.
- Walking along the flat ground stays perfectly smooth — no stutter, no snagging where two tiles meet.
Face the right way
A small touch that makes an enormous difference. pgzhelper gives every Actor a flip_x switch.
platformer.py · add after the vx if-statements
if player.vx < 0 and not player.flip_x: player.flip_x = True elif player.vx > 0 and player.flip_x: player.flip_x = False
The conditions look fussier than player.flip_x = (player.vx < 0), and there is a reason. Assigning to flip_x makes pgzhelper rebuild the sprite image from scratch. Done every frame for no reason, that is 60 pointless rebuilds a second. The and not player.flip_x means you only pay for it on the frame the direction actually changes.
Notice that standing still leaves flip_x untouched, so the player keeps facing whichever way he last walked instead of snapping back.
Run it — checkpoint 09
- The character turns to face left when you hold A and right when you hold D.
- He keeps facing that way after you let go.
Try It Yourself
13 minTry GRAVITY = 0.2 for a floaty moon jump, then JUMP_SPEED = -8 for a heavy one. This pair of numbers, more than anything else, is what your game feels like.
For each pair, use the formula from §03 to predict the jump height first, then check whether you can still reach the highest platform. Put the numbers back to 0.6 and -14 afterwards.
Add a platyear 800 pixels above the top ledge in your LEVEL. Confirm you cannot get to it. Now, without moving the platform, change one tuning number so you can — and say which of your other platforms became too easy as a result.
Hint
Height is JUMP_SPEED² / (2 × GRAVITY). To clear 200 pixels at GRAVITY = 0.6 you need JUMP_SPEED of about -15.5 or stronger; lowering gravity works too, but everything else in the level then feels like the moon.
Mini-Challenge 🔥 · Debug: The Snagging Floor
8 minFirdaus decided two collision loops was one too many, so he merged them into one at the end of update(). Now his player runs along the flat ground and every so often stops dead for no visible reason — always near where two tiles meet.
player.x += player.vx
player.vy = min(player.vy + GRAVITY, MAX_FALL)
player.y += player.vy
player.on_ground = False
for p in platforms:
if overlaps(player, p):
if player.vx > 0:
player.right = p.left
elif player.vx < 0:
player.left = p.right
if player.vy > 0:
player.bottom = p.top
player.on_ground = True
player.vy = 0Explain, in terms of what the player did that frame, why he stops. Then write the two-line fix.
Answer
While running along the floor the player is moving both sideways and (a little) downward every frame, because gravity keeps pulling him into the tile he is standing on. When the merged loop finds that overlap it applies the sideways repair first — player.right = p.left — because vx is not zero. He is shoved back to the edge of a tile he was never entering from the side.
Fix: split it back into two passes, and put the sideways loop immediately after player.x += player.vx, before gravity is applied at all. After each move, only one direction of entry is possible, so only one repair can be chosen.
Recap
3 minA jump is one negative vy, applied in on_key_down so one press means one jump, and guarded by on_ground so it cannot happen in mid-air. Jump height is JUMP_SPEED² / (2 × GRAVITY), which tells you how far apart your platforms may be. Sideways collisions get their own pass right after the x move, because handling one axis at a time removes any guesswork about which side you came in from. flip_x is set only when the direction changes, because assigning to it rebuilds the sprite.
Vocabulary Card
- on_key_down(key)
- A magic name called once, the moment a key goes down. Right for jumps and single shots; wrong for walking.
- axis separation
- Moving and repairing x and y in two separate passes, so the direction of entry is always known.
- jump height
JUMP_SPEED² / (2 × GRAVITY)— the ceiling your level design has to respect.- flip_x
- A pgzhelper switch that mirrors a sprite. Assigning to it rebuilds the image, so only do it on a change.
Homework
4 minTake the level you designed for PLT-03 and make it playable: every platform must be reachable, and reaching the highest one should take at least three jumps. Use the height formula rather than trial and error — write the maximum step your settings allow at the top of your sketch, then check each gap against it.
Bring the sketch with the reachable heights marked, and a screenshot of the player standing on the highest ledge.
Hint: a jump also carries you sideways. At MOVE_SPEED = 5 you travel about 5 pixels per frame for roughly 47 frames — well over 200 pixels — so horizontal gaps are far more forgiving than vertical ones.
Sample · the reachability check
GRAVITY = 0.6, JUMP_SPEED = -14 max step = 14 * 14 / (2 * 0.6) = 163 pixels <- write this at the top ground 570 step 1 480 (570 - 480 = 90) ok step 2 400 (480 - 400 = 80) ok step 3 320 (400 - 320 = 80) ok top 240 (320 - 240 = 80) ok <- 3 jumps from the ground? no, 4 landing 400 reachable by falling from the top
The useful part of this table is the last column. Anything above 163 is a design bug, not a difficulty setting — and if the top ledge takes fewer jumps than you intended, the fix is to move a platform, not to weaken the jump.