Learning Goals
3 minBy the end of this lesson you can:
- Store a sprite's speed in
vxandvyvariables (velocity). - Write
update(dt)and applyactor.x += vx * dtfor frame-rate-independent movement. - Explain why time-based movement produces the same result on a fast and a slow computer.
Warm-Up · The Frame-Rate Problem
5 minLast lesson you wrote bird.x = bird.x + 3 in update(). Predict what happens on two different computers:
# Computer A: runs at 60 frames per second # Computer B: runs at 30 frames per second # After 1 second, how far has the bird moved on each machine? # A: 60 frames × 3 px = ? # B: 30 frames × 3 px = ?
Show the answer
Computer A moves the bird 180 px per second; Computer B moves it only 90 px. The same code produces different speeds — that is the frame-rate problem.
New Concept · Velocity and dt
12 minImagine a car with a speed dial. No matter how often the speedometer updates — once a second or a hundred times — the car travels the same distance in an hour because distance = speed × time. We apply the same formula to sprites.
Storing velocity
Velocity is speed in a direction. We store it as two variables:
vx = 200 # pixels per second, moving right vy = 0 # no vertical movement
vx = 200 means "travel 200 pixels every second". That is the target — we have not applied it yet.
update(dt)
Add dt as a parameter to update. Pygame Zero passes in the elapsed time automatically:
def update(dt): bird.x += vx * dt bird.y += vy * dt
On a 60 FPS machine each frame is about 0.017 seconds. 200 × 0.017 ≈ 3.4 px per frame. On a 30 FPS machine each frame is 0.033 seconds. 200 × 0.033 ≈ 6.6 px — a bigger step, but the same total distance per second. The maths balances itself.
Wrapping still works the same way
def update(dt): bird.x += vx * dt if bird.x > WIDTH: bird.x = 0
Why it matters
Time-based movement makes your game fair: every player experiences the same character speed, whether they are on an old laptop from Ipoh or a gaming PC in Kuala Lumpur. It is the standard used in every professional game engine.
Worked Example · A Smooth-Sliding Satay
12 minThe story
Mei Ling wants her satay sprite to glide smoothly at exactly 200 pixels per second, regardless of the computer. Save this as smooth_satay.py:
# smooth_satay.py — time-based movement with dt import pgzrun WIDTH = 600 HEIGHT = 400 TITLE = "Smooth Satay" satay = Actor("alien") satay.x = 0 satay.y = 200 vx = 200 # pixels per second vy = 0 def draw(): screen.fill("peru") satay.draw() screen.draw.text( f"x = {satay.x:.0f}", topleft=(10, 10), fontsize=22, color="white", ) def update(dt): satay.x += vx * dt if satay.x > WIDTH: satay.x = 0 pgzrun.go()
What you will see
The f-string f"x = {satay.x:.0f}" rounds to zero decimal places so the display stays tidy. Notice we used += as a shorthand for satay.x = satay.x + ....
Try It Yourself
13 minIn smooth_satay.py, change vx to 400. Then try 50. Notice how the on-screen x counter updates at different rates, but the sprite always travels the right distance per second.
Hint
vx = 400 # twice as fast vy = 0
Set vx = 150 and vy = 100. Wrap on both x and y edges. The actor should glide diagonally and reappear at the opposite corner.
Hint
vx = 150 vy = 100 def update(dt): satay.x += vx * dt satay.y += vy * dt if satay.x > WIDTH: satay.x = 0 if satay.y > HEIGHT: satay.y = 0
Mini-Challenge · Diagonal Bouncer
8 minArjun wrote a diagonal bouncing script but the ball escapes off the screen immediately. The concept combines today's dt movement with the bouncing logic from PZ-08. Find the two bugs:
# arjun_bouncer.py — buggy
import pgzrun
WIDTH = 600
HEIGHT = 400
ball = Actor("alien")
ball.pos = (300, 200)
vx = 180
vy = 120
def draw():
screen.fill("black")
ball.draw()
def update(dt):
ball.x += vx * dt
ball.y += vy * dt
if ball.x > WIDTH or ball.x < 0:
vx = -vx
if ball.y > HEIGHT or ball.y < 0:
vy = -vy
pgzrun.go()It works if…
the sprite bounces off all four walls and never escapes
Show the fix
# arjun_bouncer.py — fixed import pgzrun WIDTH = 600 HEIGHT = 400 ball = Actor("alien") ball.pos = (300, 200) vx = 180 vy = 120 def draw(): screen.fill("black") ball.draw() def update(dt): global vx, vy ball.x += vx * dt ball.y += vy * dt if ball.x > WIDTH or ball.x < 0: vx = -vx if ball.y > HEIGHT or ball.y < 0: vy = -vy pgzrun.go()
The only fix needed is global vx, vy — both velocity variables must be declared global before they can be reassigned inside update.
Recap
3 minStoring speed in vx / vy and multiplying by dt in update(dt) gives frame-rate-independent movement. A faster computer gets a smaller dt; a slower one gets a larger dt — the product is the same distance per second either way.
Vocabulary Card
- velocity (vx, vy)
- Speed in a direction, measured in pixels per second. Positive vx moves right; positive vy moves down.
- dt
- Delta time — the number of seconds elapsed since the last frame. Pygame Zero passes it automatically to
update(dt). - Frame-rate-independent movement
- Movement that covers the same distance per second on any computer, achieved by multiplying velocity by dt.
- +=
- Short for "add to itself":
x += 5is the same asx = x + 5.
Homework
4 minCreate speedometer.py. Display a sprite moving at vx = 300 px/s using dt. Show the sprite's current x position on screen (rounded) so you can see the movement is consistent. Save a screenshot and bring it to the next class.
Stretch. Add a simple on-screen "speed" label that shows the current vx value. Then change vx using a clock.schedule call to triple the speed after 3 seconds.
Sample · speedometer.py
# speedometer.py — time-based movement with position display import pgzrun WIDTH = 600 HEIGHT = 400 runner = Actor("alien") runner.x = 0 runner.y = 200 vx = 300 def draw(): screen.fill("darkgreen") runner.draw() screen.draw.text( f"x = {runner.x:.0f} speed = {vx} px/s", topleft=(10, 10), fontsize=22, color="white", ) def update(dt): runner.x += vx * dt if runner.x > WIDTH: runner.x = 0 pgzrun.go()
The key non-negotiable is update(dt) with runner.x += vx * dt. Background colour and sprite are your own.