Gravity in code
Add gravity, a jump impulse, ground collision, and coyote time to a pygame player — the complete platformer physics foundation.
- Add gravity to a player's vertical velocity each frame
- Zero vertical velocity on ground collision to prevent sinking
- Implement a jump impulse gated on the coyote-time counter
- Track coyote time with a frame counter
This lesson assembles the concepts from the previous two lessons into a working player class. The code below is a complete, runnable pygame program showing a player that falls, jumps, and lands correctly — including coyote time.
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 platformer.py.
The player with gravity
import pygame
import sys
pygame.init()
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption("Gravity demo")
clock = pygame.time.Clock()
Vector2 = pygame.math.Vector2
# ── Constants ─────────────────────────────────────────────────────────────────
GRAVITY = 900 # px/s² downward
JUMP_SPEED = 420 # px/s upward (negative y)
COYOTE_FRAMES = 6 # grace frames after leaving the ground
JUMP_BUFFER_F = 8 # frames a buffered jump press stays active
GROUND_Y = 400 # y-coordinate of the ground surface
FLOOR_RECT = pygame.Rect(0, GROUND_Y, 640, 80)
# ── Player ────────────────────────────────────────────────────────────────────
class Player:
WIDTH = 32
HEIGHT = 48
def __init__(self, x, y):
self.pos = Vector2(x, y)
self.vel = Vector2(0, 0)
self.rect = pygame.Rect(x, y, self.WIDTH, self.HEIGHT)
self.on_ground = False
self.coyote = 0 # frames remaining
self.jump_buffer = 0 # frames remaining
# ── Input / physics ───────────────────────────────────────────────────────
def handle_jump_input(self, keys_just_pressed):
"""Call once per frame with this frame's freshly-pressed keys."""
if keys_just_pressed.get(pygame.K_SPACE):
self.jump_buffer = JUMP_BUFFER_F
def update(self, dt):
# Horizontal movement
keys = pygame.key.get_pressed()
self.vel.x = 0
if keys[pygame.K_LEFT]: self.vel.x = -220
if keys[pygame.K_RIGHT]: self.vel.x = 220
# Gravity
self.vel.y += GRAVITY * dt
# Move
self.pos += self.vel * dt
self.rect.topleft = (int(self.pos.x), int(self.pos.y))
# Ground collision
self.on_ground = False
if self.rect.colliderect(FLOOR_RECT) and self.vel.y >= 0:
self.on_ground = True
self.vel.y = 0
self.pos.y = FLOOR_RECT.top - self.HEIGHT
self.rect.top = int(self.pos.y)
# Coyote time: reset when on ground, tick down in the air
if self.on_ground:
self.coyote = COYOTE_FRAMES
elif self.coyote > 0:
self.coyote -= 1
# Consume a buffered jump
if self.jump_buffer > 0:
self.jump_buffer -= 1
if self.coyote > 0:
self.vel.y = -JUMP_SPEED
self.coyote = 0 # consume the coyote window
self.jump_buffer = 0
# Screen bounds
self.pos.x = max(0, min(640 - self.WIDTH, self.pos.x))
self.rect.x = int(self.pos.x)
# ── Rendering ─────────────────────────────────────────────────────────────
def draw(self, surface):
pygame.draw.rect(surface, (100, 200, 120), self.rect)
# ── Main loop ─────────────────────────────────────────────────────────────────
player = Player(300, 300)
running = True
while running:
dt = min(clock.tick(60) / 1000.0, 0.05)
just_pressed = {}
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
just_pressed[event.key] = True
player.handle_jump_input(just_pressed)
player.update(dt)
screen.fill((20, 20, 35))
pygame.draw.rect(screen, (80, 60, 40), FLOOR_RECT)
player.draw(screen)
pygame.display.flip()
pygame.quit()
sys.exit()Key design decisions
Separating handle_jump_input from update — the jump buffer is set from
the event queue (one call per keydown), while the buffer is consumed inside
update (once per frame). Keeping them separate means the buffer logic is not
sensitive to event polling order.
Coyote time is a frame counter, not a bool — self.coyote = COYOTE_FRAMES
resets on every grounded frame, so the grace window is always fresh. Decrementing
in the air naturally expires it. The jump check if self.coyote > 0 works
equally well for "actually on ground" and "just left the ground."
vel.y >= 0 guard on ground collision — without this check, the player
would be snapped to the floor while jumping upward through a floor from below.
Only cancel downward velocity when moving downward.
min(dt, 0.05) cap — if the window loses focus for several seconds and then
regains it, dt could be huge, sending the player through geometry. Capping at
0.05 s (the equivalent of 20 fps) limits the maximum physics step.
This uses a single flat ground rectangle. In a real platformer you would loop over a list of platform rects (or use pygame sprite collision) and apply the same logic to each. The collision resolution must also handle horizontal walls and ceilings — check which axis the overlap occurs on first.
Where to go next
Next: lab — physics system — integrate this Player physics into the
tile-based platformer from the intermediate tier, and add bouncing projectiles.