The Pipeline: one world, one runner
The entire flow in one readable script: load map → build the world → attach the runner → move the player → push a kinematic crate → draw everything. This is the update loop from docs/physics-world.md, assembled exactly as the examples run it.
import copy
import pygame
from tilemap_parser import (
Body, CollisionRunner, PhysicsWorld, RectangleShape,
load_map, load_tileset_collision,
)
TILE = 32
COLS, ROWS = 24, 14
SCREEN_W, SCREEN_H = COLS * TILE, ROWS * TILE
FLOOR_ROW = 12
PUSH_SPEED = 260.0
class Player:
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
self.vx = 0.0
self.vy = 0.0
self.on_ground = True
self.collision_shape = RectangleShape(width=24, height=28)
def build_scene():
"""Ground rows + a wall column. Returns the world and its crates."""
tile_map = {}
for x in range(COLS):
for y in (12, 13):
tile_map[(x, y)] = 0
for y in range(8, 12):
tile_map[(16, y)] = 0
tileset = load_tileset_collision("data/collision/terrain.collision.json")
world = PhysicsWorld(tile_map=tile_map, tileset_collision=tileset, tile_size=(TILE, TILE))
crates = [
Body(RectangleShape(width=TILE, height=TILE), x=8 * TILE, y=12 * TILE - TILE, mode="kinematic"),
Body(RectangleShape(width=TILE, height=TILE), x=10 * TILE, y=12 * TILE - TILE, mode="kinematic"),
]
for crate in crates:
world.add_body(crate)
return world, crates
def body_ahead(world, sprite, axis, probe=8.0):
"""Find the body the sprite is pressed against.
The runner stops a sprite just short of a body (sub-pixel skin gap),
so a static collides_with_body check can miss. Probe a few px in.
"""
s = copy.copy(sprite)
s.x = sprite.x + axis * probe
return world.collides_with_body(s)
def main():
pygame.init()
screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
clock = pygame.time.Clock()
world, crates = build_scene()
runner = CollisionRunner.from_world(world, game_type="platformer")
player = Player(96, 12 * TILE - 28)
running = True
while running:
dt = clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
keys = pygame.key.get_pressed()
axis = (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT])
jump = keys[pygame.K_SPACE]
# 1. player vs tiles + bodies
result = runner.move_platformer(player, None, None, dt, input_x=float(axis), jump_pressed=jump)
# 2. push: pressed against a kinematic crate? hand it a velocity
if result.hit_wall_x and axis != 0:
block = world.collides_with_body(player)
if block is None:
block = body_ahead(world, player, axis)
if block is not None and block.mode == "kinematic":
block.vx = axis * PUSH_SPEED
# 3. drive every crate that has a velocity
for crate in crates:
if crate.vx:
crate_result = runner.move_grounded(crate, None, None, dt, velocity=(crate.vx, crate.vy))
if crate_result.hit_wall_x:
crate.vx = 0.0
else:
crate.vx *= 0.9
if abs(crate.vx) < 1.0:
crate.vx = 0.0
# 4. draw: world, then sprites at (x, y)
screen.fill((35, 35, 45))
for (tx, ty), tile_id in world.tile_map.items():
pygame.draw.rect(screen, (70, 70, 90), (tx * TILE, ty * TILE, TILE, TILE))
for body in world.bodies:
pygame.draw.rect(screen, (230, 150, 60), (body.x, body.y, TILE, TILE))
pygame.draw.rect(screen, (100, 170, 255), (player.x, player.y, 24, 28))
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
main()WHAT EACH BLOCK DOES
build_scene()
hand-rolled tile layer
loads tileset collision
from_world(world)
preset + attach
None, None tile args
move_platformer
player vs tiles + bodies
gravity, jump, step-up
push block
hit_wall_x → find body
probe the skin gap
crate drive
move_grounded velocity=
crates vs tiles + crates
draw
bodies draw at (x, y)
box == visual box
| Block | Job |
|---|---|
build_scene() | Hand-rolls the collision tile layer and loads the tileset collision. The world owns tiles, geometry and bodies. add_body is what makes crates solid. |
CollisionRunner.from_world(world, game_type="platformer") | Preset + attach in one call. From here, None, None tile args mean "use the world". |
move_platformer(...) | Player vs tiles + bodies: gravity, jump, step-up, landings. Returns the result you branch on. |
| Push block | On hit_wall_x, identify the solid (collides_with_body, probing for the skin gap), and only kinematic bodies get a velocity. |
| Crate drive | move_grounded(crate, ..., velocity=...): explicit velocity, no gravity. The runner resolves the crate against tiles and other crates; hit_wall_x stops it. |
| Draw | Sprites and bodies draw at their (x, y); the collision box and visual box are the same rectangle. |
WHAT BREAKS IF YOU SKIP A STEP
- Skip
add_body→ crates are invisible to physics; the player walks through them. - Skip
from_world/attach→ the runner never sees the world's tiles or bodies; the player falls through the floor. - Skip the
velocity=→ the crate gets gravity and drops; or worse, nothing moves it at all. - Skip the probe → pushes randomly fail right at the moment of contact.