Physics and Enemy AI

Pygame Game Development Course

Felipe A. Moreno-Vera

Note

In these modules you will learn:

  • Gravity, velocity, acceleration, friction, variable jump height, coyote time, jump buffering, one-way platforms, moving platforms, and wall jumping
  • Steering behaviors (seek, flee, patrol, wander), line of sight, vision cones, and a full enemy Finite State Machine

⚙️ Basic Physics

The Physics Update Loop

Platformer physics is built on two fundamental equations applied every frame:

velocity += acceleration × dt      # Euler integration
position += velocity   × dt

The Physics Update Loop

Platformer physics is built on two fundamental equations applied every frame:

velocity += acceleration × dt      # Euler integration
position += velocity   × dt

dt (delta time) is the time elapsed since the last frame in seconds. Using dt makes the simulation frame-rate independent: the same behavior at 30 FPS and 120 FPS.

# Every frame:
vy    += GRAVITY * dt       # Gravity accelerates the fall
y     += vy * dt            # Velocity moves the position

The Physics Update Loop

Platformer physics is built on two fundamental equations applied every frame:

velocity += acceleration × dt      # Euler integration
position += velocity   × dt

dt (delta time) is the time elapsed since the last frame in seconds. Using dt makes the simulation frame-rate independent: the same behavior at 30 FPS and 120 FPS.

# Every frame:
vy    += GRAVITY * dt       # Gravity accelerates the fall
y     += vy * dt            # Velocity moves the position

Important

Always multiply forces and velocities by dt. Without it, your game runs at different speeds on different hardware.

Gravity

Gravity is simply a constant downward acceleration applied every frame:

GRAVITY      = 900.0    # px/s²  — feel free to tune
TERMINAL_VEL = 850.0    # cap: prevents infinite acceleration

vy = min(vy + GRAVITY * dt, TERMINAL_VEL)
y  += vy * dt

Gravity

Gravity is simply a constant downward acceleration applied every frame:

GRAVITY      = 900.0    # px/s²  — feel free to tune
TERMINAL_VEL = 850.0    # cap: prevents infinite acceleration

vy = min(vy + GRAVITY * dt, TERMINAL_VEL)
y  += vy * dt
Freefall simulation (60 FPS, gravity=900 px/s²)
  frame    vy (px/s)      y (px)       notes
  ----------------------------------------------------
  0        15.0           0.2          
  10       165.0          16.5         
  20       315.0          57.8         
  30       465.0          124.0        
  40       615.0          215.2        
  50       765.0          331.5        
  60       850.0          469.8        terminal vel reached

Jumping

A jump is just an instantaneous upward velocity applied when the player is on the ground:

JUMP_VY = -460.0    # Negative = upward (Y grows downward in Pygame)

if player_pressed_jump and on_ground:
    vy = JUMP_VY

Variable Jump Height

Hold the jump button for a higher jump; release early for a short hop. The trick: reduce gravity while the button is held and the player is still rising.

JUMP_FRAMES_MAX = 20   # Max frames of reduced gravity
JUMP_FRAMES_MIN = 6    # Must hold at least this long

if jumping:
    jump_frames += 1
    if not keys[K_SPACE] and jump_frames > JUMP_FRAMES_MIN:
        jumping = False          # Cut jump short
    if jump_frames >= JUMP_FRAMES_MAX:
        jumping = False

grav_mult = 0.4 if jumping else 1.0   # Reduced gravity = higher jump
vy += GRAVITY * grav_mult * dt

Coyote Time

The coyote time grace period lets the player jump for a few frames after walking off a ledge — feels fair and responsive:

COYOTE_FRAMES = 8

if on_ground:
    coyote_timer = COYOTE_FRAMES
elif coyote_timer > 0:
    coyote_timer -= 1

can_jump = on_ground or coyote_timer > 0

Jump Buffering

If the player presses jump just before landing, buffer the input and execute it on contact:

BUFFER_FRAMES = 10

# On KEYDOWN event:
jump_buffer = BUFFER_FRAMES

# In update():
if jump_buffer > 0:
    jump_buffer -= 1
if jump_buffer > 0 and can_jump:
    vy = JUMP_VY
    jump_buffer = 0

Jump Buffering

Coyote time and jump buffer values
=============================================
  Coyote frames  : 8 frames = 133 ms
  Buffer frames  : 10 frames = 167 ms

Why it matters:
  Without coyote: walk off ledge → can't jump (feels unfair)
  With coyote:    8 frames grace → feels responsive

  Without buffer: press jump 1 frame early → nothing happens
  With buffer:    input held for 10 frames → auto-fires on land

Collision Resolution

The most reliable pattern: move on one axis, resolve, then move on the other.

# Step 1: move horizontally
x += vx * dt
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vx > 0: x = platform.rect.left  - player_w   # Pushed left
        if vx < 0: x = platform.rect.right               # Pushed right
        vx = 0

# Step 2: move vertically
y += vy * dt
on_ground = False
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vy > 0:                                        # Falling
            y        = platform.rect.top - player_h
            vy       = 0
            on_ground = True
        elif vy < 0:                                      # Rising
            y  = platform.rect.bottom
            vy = 0

Collision Resolution

The most reliable pattern: move on one axis, resolve, then move on the other.

# Step 1: move horizontally
x += vx * dt
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vx > 0: x = platform.rect.left  - player_w   # Pushed left
        if vx < 0: x = platform.rect.right               # Pushed right
        vx = 0

# Step 2: move vertically
y += vy * dt
on_ground = False
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vy > 0:                                        # Falling
            y        = platform.rect.top - player_h
            vy       = 0
            on_ground = True
        elif vy < 0:                                      # Rising
            y  = platform.rect.bottom
            vy = 0

Tip

Always resolve X and Y separately. Combined resolution causes the player to clip corners and stick to walls incorrectly.

One-Way Platforms

Only solid from the top — the player can jump through from below:

for platform in one_way_platforms:
    if player_rect.colliderect(platform.rect):
        # Only resolve if falling AND was above the platform top last frame
        if vy > 0 and (prev_y + player_h) <= platform.rect.top + 4:
            y        = platform.rect.top - player_h
            vy       = 0
            on_ground = True
        # If rising: ignore (pass through)

Moving Platforms — Velocity Inheritance

When standing on a moving platform, add the platform’s velocity to the player:

class MovingPlatform:
    def update(self, dt):
        prev_x     = self.rect.x
        self.rect.x += self.direction * self.speed * dt
        self.vel_x  = (self.rect.x - prev_x) / dt   # Derived velocity

# In player update:
if on_ground and standing_on:
    x += standing_on.vel_x * dt   # Inherit platform movement

Acceleration-Based Movement

Instant velocity feels stiff. Acceleration gives weight and momentum:

MOVE_ACCEL   = 1800.0   # px/s² — ground acceleration
AIR_ACCEL    = 900.0    # Less control in the air
MAX_VX       = 240.0

accel = MOVE_ACCEL if on_ground else AIR_ACCEL

if pressing_right:
    vx += (MAX_VX - vx) * (accel / MAX_VX) * dt   # Accelerate toward target
else:
    vx -= math.copysign(MOVE_ACCEL * dt, vx)       # Decelerate

Acceleration-Based Movement

Instant velocity feels stiff. Acceleration gives weight and momentum:

MOVE_ACCEL   = 1800.0   # px/s² — ground acceleration
AIR_ACCEL    = 900.0    # Less control in the air
MAX_VX       = 240.0

accel = MOVE_ACCEL if on_ground else AIR_ACCEL

if pressing_right:
    vx += (MAX_VX - vx) * (accel / MAX_VX) * dt   # Accelerate toward target
else:
    vx -= math.copysign(MOVE_ACCEL * dt, vx)       # Decelerate
Velocity profile over 30 frames
  Frame    Instant      Accelerated
  ----------------------------------
  0        240.0        30.0
  5        240.0        132.3
  10       240.0        184.8
  15       240.0        211.7
  20       240.0        225.5
  25       240.0        232.5

Accelerated movement reaches max speed gradually → feels weighted

Physics — Summary

  • velocity += acceleration × dt then position += velocity × dt — always use dt
  • Gravity = constant downward acceleration + terminal velocity cap
  • Variable jump = reduce gravity while holding jump button
  • Coyote time = jump grace period (8 frames) after walking off a ledge
  • Jump buffering = remember jump input (10 frames) before landing
  • Separate X and Y collision resolution to avoid corner-clipping
  • One-way platforms: only resolve collision when falling and above the top edge
  • Moving platforms: derive vel_x from position delta, inherit into player
  • Acceleration-based movement: vx += (target - vx) * accel_factor * dt