[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 millisecondsdt = clock.tick(60) / 1000.0converts 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]
Smooth movement that builds up speed and slides to a stop
pygame needs a real window — copy this into a .py file and run it locally.
Same speed in all directions, including diagonals
pygame needs a real window — copy this into a .py file and run it locally.
# challenges [2]