pyodide: loading…

[concept]Game Development with Pygame

Movement, Velocity & Physics

# theory

from the videos

Basic Platform Movement (Brick Breakaway Video 3)

  • keys = pygame.key.get_pressed() for continuous input
  • Moving the paddle with rect.x += speed
  • Boundary clamping so paddle doesn't leave the screen

Delta Time and Smooth Movements (Brick Breakaway Video 4)

  • clock.tick(60) returns milliseconds
  • dt = clock.tick(60) / 1000.0 converts to seconds
  • Multiplying velocity by dt for frame-rate independence

Vector Normalization (General Concepts)

  • Why diagonal movement is faster without normalization
  • Using .normalize() to get consistent speed at any angle
  • Essential for anything with 8-directional movement

delta time movement: the right way

Gerber teaches this early because it's foundational. Here's the pattern:

clock = pygame.time.Clock()

# Define speed in pixels PER SECOND, not per frame
SPEED = 300

while running:
    dt = clock.tick(60) / 1000.0  # dt in seconds

    keys = pygame.key.get_pressed()
    if keys[pygame.K_RIGHT]:
        x += SPEED * dt  # moves 300 pixels per second, regardless of FPS

Why this matters: If you just do x += 5 every frame, your game runs at different speeds on different computers. Delta time fixes that.

keyboard input

keys = pygame.key.get_pressed()  # snapshot of current key state

if keys[pygame.K_LEFT]:
    x -= SPEED * dt
if keys[pygame.K_RIGHT]:
    x += SPEED * dt
if keys[pygame.K_UP]:
    y -= SPEED * dt
if keys[pygame.K_DOWN]:
    y += SPEED * dt

Common keys: pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN, pygame.K_SPACE, pygame.K_RETURN, pygame.K_a, pygame.K_w, etc.

boundary clamping

Keep the player on screen. Gerber shows two ways:

Manual clamping:

# After moving
if player_rect.left < 0:
    player_rect.left = 0
if player_rect.right > WIDTH:
    player_rect.right = WIDTH

Using clamp_ip(); cleaner:

# Clamp rect to stay within screen bounds
player_rect.clamp_ip(screen.get_rect())

The _ip suffix means "in place"; it modifies the rect directly instead of returning a new one.

bouncing off walls

When the object hits a wall, flip the velocity:

WIDTH, HEIGHT = 800, 600
BALL_RADIUS = 20

# Bounce off left/right walls
if x - BALL_RADIUS <= 0 or x + BALL_RADIUS >= WIDTH:
    vx = -vx

# Bounce off top/bottom
if y - BALL_RADIUS <= 0 or y + BALL_RADIUS >= HEIGHT:
    vy = -vy

Better bouncing with abs():

# Using abs() prevents "stuck in wall" bugs
if x - BALL_RADIUS <= 0:
    x = BALL_RADIUS
    vx = abs(vx)  # force positive (moving right)
elif x + BALL_RADIUS >= WIDTH:
    x = WIDTH - BALL_RADIUS
    vx = -abs(vx)  # force negative (moving left)

acceleration and friction

This is what makes movement feel good instead of robotic:

# Physics constants
ACCELERATION = 500    # pixels per second squared
FRICTION = 0.85       # multiplier each frame (must be < 1.0)
MAX_SPEED = 400       # pixels per second

vx = 0  # velocity starts at 0

while running:
    dt = clock.tick(60) / 1000.0

    keys = pygame.key.get_pressed()

    # Apply acceleration when key pressed
    if keys[pygame.K_RIGHT]:
        vx += ACCELERATION * dt
    if keys[pygame.K_LEFT]:
        vx -= ACCELERATION * dt

    # Apply friction (slows you down when not pressing)
    vx *= FRICTION

    # Clamp to max speed
    vx = max(-MAX_SPEED, min(MAX_SPEED, vx))

    # Stop completely at very low speeds (prevents drift)
    if abs(vx) < 0.5:
        vx = 0

    x += vx * dt

Gerber tip: Friction below 1.0 means velocity shrinks each frame when you're not pressing anything; that's the "sliding to a stop" feel.

vector normalization

Without normalization, diagonal movement is ~41% faster than horizontal/vertical. That's because you're adding two velocities together:

# BAD; diagonal is faster
if keys[pygame.K_RIGHT]: vx = SPEED
if keys[pygame.K_DOWN]:  vy = SPEED
# Moving right+down = sqrt(SPEED^2 + SPEED^2) = SPEED * 1.414

The fix; normalize the direction:

from pygame.math import Vector2

direction = Vector2(0, 0)
if keys[pygame.K_LEFT]:  direction.x -= 1
if keys[pygame.K_RIGHT]: direction.x += 1
if keys[pygame.K_UP]:    direction.y -= 1
if keys[pygame.K_DOWN]:  direction.y += 1

if direction.length() > 0:
    direction = direction.normalize()  # length becomes 1
    pos += direction * SPEED * dt

storing float position separately

Gerber tip: Rects use integers, which causes jitter with small movements. Store a float position separately:

class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((40, 40))
        self.rect = self.image.get_rect()

        # Float position for smooth movement
        self.float_x = float(self.rect.x)
        self.float_y = float(self.rect.y)

    def update(self, dt):
        self.float_x += self.vx * dt
        self.float_y += self.vy * dt

        # Sync rect to float position
        self.rect.x = int(self.float_x)
        self.rect.y = int(self.float_y)

tips

  • Gerber tip: Always multiply velocity by dt for frame-rate independence
  • Tip: Store float positions separately from the rect for smooth sub-pixel movement
  • Tip: Use rect.clamp_ip(screen.get_rect()) for easy boundary clamping
  • Tip: Friction values around 0.85-0.92 feel natural for most games
  • Tip: Normalize direction vectors when allowing 8-directional movement

common mistakes

  • Not using delta time: game runs at different speeds on different machines
  • Using integer division on rect positions; causes visible jitter
  • Forgetting to check direction.length() > 0 before normalizing; divides by zero
  • Just flipping velocity on wall bounce: ball can get stuck, use abs() instead
  • Diagonal movement 41% faster: forgot to normalize the direction vector

# examples [2]

# example 01 · acceleration & friction movement

Smooth movement that builds up speed and slides to a stop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# example 02 · normalized 8-Directional movement

Same speed in all directions, including diagonals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# challenges [2]

# challenge 01/02todo
A ball moves at 300 pixels per second. If dt = 0.016 (60fps), how many pixels does it move in one frame?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
🐍
Loading PythonSetting up pandas & numpy...
# challenge 02/02todo
Why do you multiply velocity by a friction value (like 0.88) instead of subtracting a fixed amount?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
🐍
Loading PythonSetting up pandas & numpy...