Learning Goals
3 minBy the end of this lesson you can:
- Explain what
update()does thatdraw()must not. - Move an Actor by storing a velocity in
vxrather than changingxdirectly. - Write gravity as a change to a speed, and explain why that makes the fall accelerate.
- Say what
MAX_FALLprotects you from, using the tile height in your answer.
Warm-Up · Which Way Is Down?
5 minScreen coordinates are upside down compared with maths class. y = 0 is the top edge of the window and y = 600 is the bottom.
Predict, out loud or in your notebook:
player.y += 5— does the player move up or down?- To move him left, does
vxneed to be positive or negative? - Gravity pulls down. So is
GRAVITYa positive or a negative number?
Down is bigger y. Gravity is therefore a positive number, and a jump — in lesson 4 — will be a negative one. Nearly every "my player flies into the sky" bug in this module is a sign flip.
New Concept · update(), Velocity and Acceleration
12 mindraw() paints the picture. Its partner update() changes the numbers, and Pygame Zero calls it about 60 times a second too. All movement goes in update(). If you move things inside draw(), the speed of your game becomes the speed of your graphics card, which is a bad idea in ways that only show up on someone else's laptop.
Velocity, not just position. You could write player.x -= 5 when A is held, and today it would work. Instead we store the speed in a name of its own:
player.vx = 0 # made once, at the top of the file def update(): player.vx = 0 # forget last frame's decision if keyboard.a: player.vx = -MOVE_SPEED # negative = left if keyboard.d: player.vx = MOVE_SPEED player.x += player.vx
vx means "velocity in x". It is a memory of which way you were heading, and in lesson 4 the collision code has to ask exactly that question to decide which side of a wall to put you on. Storing it now saves rewriting later. Resetting it to 0 at the top of every frame is what makes the player stop when you let go of the key.
Gravity is not a position. It is a change to a speed. Every frame the falling speed gets a little bigger, and only then does the player move by it:
player.vy = player.vy + GRAVITY # the speed grows... player.y += player.vy # ...and then we move by it
Because the speed itself grows each frame, the player starts slow and gets faster. That accelerating fall is what makes it read as real gravity rather than a lift descending.
min(...) caps the falling speed at 18 pixels per frame. That is not about realism, it is about not falling through the floor.
The game only looks at the world 60 times a second. A platform tile is 30 pixels tall. If the player were ever moving faster than 30 pixels per frame, he could be above the tile in one frame and below it in the next, having never once overlapped it. The collision check in lesson 3 would find nothing, and he would drop straight through. Capping the speed below the tile height makes that impossible.
Build Log · Stages 03–04
14 minWalk with A and D
Three additions: a speed setting, a velocity on the player, and the update() function itself.
platformer.py · add these pieces
MOVE_SPEED = 5 player.vx = 0 def update(): player.vx = 0 if keyboard.a: player.vx = -MOVE_SPEED if keyboard.d: player.vx = MOVE_SPEED player.x += player.vx
Put MOVE_SPEED = 5 up with WIDTH and HEIGHT, player.vx = 0 just after player.pos, and update() above draw().
Run it — checkpoint 03
- Hold A and the player slides left; hold D and he slides right.
- Release and he stops instantly.
- He walks straight off both edges of the window and disappears. Correct for now — lesson 5 fixes it.
Gravity
Two settings, one more velocity, and two lines at the bottom of update().
platformer.py · add these pieces
GRAVITY = 0.6 MAX_FALL = 18 player.vy = 0 # at the bottom of update(), after the horizontal move: player.vy = min(player.vy + GRAVITY, MAX_FALL) player.y += player.vy
The min() is the cap from §03: add gravity, but never let the result climb above 18.
Run it — checkpoint 04
- The player drops off the bottom of the window, speeding up as he goes, and never comes back.
- You can still steer him with A and D on the way down.
- This is correct — there is nothing to land on until the next lesson.
Your update() now has a shape worth memorising, because every remaining lesson adds to it in the same order: read the keys into vx → move on x → apply gravity to vy → move on y.
Try It Yourself
13 minRun the game three times, changing one number each time, and write one sentence about how each feels:
GRAVITY = 0.2— the moon.GRAVITY = 1.5— a very heavy character.MOVE_SPEED = 12— with gravity back at0.6.
Put both numbers back to 0.6 and 5 before the next exercise.
Add print(player.vy) as the last line of update() and run. Watch the terminal while he falls. Answer two questions: what does the number do at the start, and what does it do after about a second?
Hint
It should climb by 0.6 each frame — 0.6, 1.2, 1.8 … — and then stop dead at 18 and stay there. That flat 18 is MAX_FALL doing its job. Delete the print afterwards; 60 lines a second makes the terminal unusable.
Mini-Challenge 🔥 · Predict the Bug
8 minLeo thinks MAX_FALL is pointless — the player is going to land on a platform anyway, so why cap anything? He sets MAX_FALL = 100.
Before you touch the code, write down your prediction:
- The tile is 30 pixels tall and the game checks 60 times a second. At 100 pixels per frame, how far does the player move between two checks?
- How many frames does he spend overlapping a 30-pixel tile?
- So what will he do when he reaches the ground in lesson 3?
Now set MAX_FALL = 100 and leave a sticky note on your screen. In the next lesson, when there is finally a floor, run it once with 100 and once with 18. Leo is wrong, and you should get to watch him be wrong.
Answer
100 pixels per frame. A 30-pixel tile is crossed entirely between one check and the next, so on no frame do the two rectangles overlap. The collision test never fires and the player tunnels straight through the ground — the single most common bug in homemade platformers, and the reason the cap has to sit below the tile height.
Recap
3 minupdate() changes numbers, draw() paints them, and Pygame Zero calls both about 60 times a second. Sideways movement is stored as vx so that later code can ask which way you were going. Gravity adds to vy every frame, so the fall accelerates by itself, and MAX_FALL caps that speed below the tile height so the player can never skip past a platform between two frames.
Vocabulary Card
- velocity (vx, vy)
- How far something moves per frame, kept as its own value so other code can read the direction.
- acceleration
- A change to a velocity. Gravity is acceleration:
vygrows every frame, so the fall speeds up. - terminal velocity
- The cap on falling speed —
MAX_FALLhere. Keeps the fall below the tile height so collisions cannot be skipped. - tunnelling
- Moving so far in one frame that you pass clean through a solid object without ever overlapping it.
Homework
4 minDesign the feel of your platformer. Try at least four combinations of GRAVITY and MOVE_SPEED, and record them in a table: the two numbers, one sentence on how it felt, and a character it would suit (a ninja? an elephant? a balloon?).
Mark the row you want to keep. Bring the table next class — in lesson 4 you add a jump, and your choice decides how high it goes.
Hint: the pair matters more than either number alone. Fast and floaty feels nothing like fast and heavy.
Sample · Firdaus's feel table
GRAVITY MOVE_SPEED how it felt suits 0.2 5 floaty, slow to come down an astronaut 0.6 5 normal, predictable <- KEEPING THIS a hooded adventurer 0.6 12 skiddy, hard to stop on a small tile a speed-runner 1.5 5 heavy, drops like a stone an elephant
Any honest table is a correct answer. The one thing to notice: high MOVE_SPEED with small platforms is unfair, because you overshoot the landing before you can react. Level design and tuning are the same job.