Code of the Day
AdvancedPhysics and Simulation

Velocity and acceleration

Implement Euler integration so objects accelerate smoothly and move at the same speed on any machine — frame-rate independence in practice.

Game DevAdvanced10 min read
Recommended first
By the end of this lesson you will be able to:
  • Implement Euler integration using position += velocity * dt and velocity += acceleration * dt
  • Obtain delta time from pygame.time.Clock and use it to scale all motion
  • Apply a constant force to an object over time

Movement in most beginner games looks like rect.x += speed. That works until you run on a faster or slower machine: at 120 fps the player crosses the map in half the time it takes at 60 fps. The fix is delta time — scale every displacement by the number of seconds the last frame actually took.

Pyodide (the in-browser Python runner) does not support pygame's display system. Read through this code carefully in the browser, then run it locally: pip install pygame followed by python physics.py.

Euler integration

The simplest way to integrate physics is Euler integration:

velocity  += acceleration * dt
position  += velocity     * dt

Apply this once per frame, where dt is the elapsed time in seconds. The resulting motion is physically plausible for moderate time steps and cheap enough for 2D games.

pygame.time.Clock.tick(fps) returns milliseconds since the last call. Divide by 1000 to get seconds:

dt = clock.tick(60) / 1000.0   # seconds; capped at ~1/60

Cap dt at something sensible (e.g. min(dt, 0.05)) so that resuming from a breakpoint or tab-switch does not send the player flying across the map.

PhysicsBody class

import pygame
import sys

pygame.init()
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption("Euler integration demo")
clock  = pygame.time.Clock()

Vector2 = pygame.math.Vector2

# ── PhysicsBody ───────────────────────────────────────────────────────────────

class PhysicsBody:
    def __init__(self, x, y):
        self.position     = Vector2(x, y)
        self.velocity     = Vector2(0, 0)
        self.acceleration = Vector2(0, 0)

    def apply_force(self, force):
        """Accumulate a force for this frame (mass = 1 for simplicity)."""
        self.acceleration += force

    def update(self, dt):
        self.velocity     += self.acceleration * dt
        self.position     += self.velocity     * dt
        self.acceleration  = Vector2(0, 0)     # reset each frame

    def draw(self, surface):
        pygame.draw.circle(surface, (100, 200, 120),
                           (int(self.position.x), int(self.position.y)), 14)


# ── Setup ─────────────────────────────────────────────────────────────────────

ball        = PhysicsBody(320, 240)
WIND_FORCE  = Vector2(40, 0)   # 40 px/s² toward the right

running = True
while running:
    dt = min(clock.tick(60) / 1000.0, 0.05)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Apply a constant wind force every frame
    ball.apply_force(WIND_FORCE)

    # Arrow keys: let the player counteract the wind
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:  ball.apply_force(Vector2(-120, 0))
    if keys[pygame.K_RIGHT]: ball.apply_force(Vector2( 120, 0))
    if keys[pygame.K_UP]:    ball.apply_force(Vector2(0, -120))
    if keys[pygame.K_DOWN]:  ball.apply_force(Vector2(0,  120))

    ball.update(dt)

    # Wrap at screen edges
    ball.position.x %= 640
    ball.position.y %= 480

    screen.fill((20, 20, 35))
    ball.draw(screen)
    pygame.display.flip()

pygame.quit()
sys.exit()

How it works

Force accumulationapply_force() adds to acceleration rather than setting it. Multiple forces (wind, gravity, player input) all contribute to the same acceleration for that frame. After update() the accumulator is zeroed so stale forces do not carry into the next frame.

Mass = 1 — Strictly, Newton's second law says acceleration = force / mass. For simple prototypes, treating mass as 1 makes force and acceleration equivalent, which simplifies the code without changing the qualitative feel. Add a self.mass field when you need lighter or heavier objects.

Velocity damping — Multiply velocity by a damping factor each frame (self.velocity *= 0.98) to simulate friction or air resistance. Without it, an object accelerated by a constant force accelerates forever.

Euler integration accumulates a small error each step. For most 2D arcade games the error is imperceptible. If you are building a physics-critical simulation (billiards, trebuchet), look into Verlet or Runge-Kutta integration instead.

Where to go next

Next: gravity and jumping — applying a constant downward force and the concept of jump impulses, coyote time, and jump buffering.

Finished reading? Mark it complete to track your progress.

On this page