CollisionRunner guide
The runner is the single public surface for movement. It composes five movement implementations behind one object: configure it once, call a move_* every frame, read the result. Everything below is the runner's actual behavior from runtime/movement/.
GAME-TYPE PRESETS
CollisionRunner.from_game_type(name, tile_size=(32, 32), strict=False, render_scale=1.0) is the recommended constructor. Presets:
| game_type | Mode | Gravity | Jump | Sprite needs |
|---|---|---|---|---|
platformer | PLATFORMER | 800 px/s² | -400 px/s | x, y, collision_shape, vx, vy, on_ground |
topdown | SLIDE | 0 | 0 | x, y, collision_shape |
rpg | RPG | 0 | 0 | x, y, collision_shape |
Unknown names raise ValueError. Everything the presets set is just attributes; tweak after construction. The generic CollisionRunner(tile_size, mode, render_scale) constructor also exists;runner.move(sprite, tileset, tile_map, delta_x, delta_y, dt, ...) dispatches on the configured mode (slide → move_and_slide, platformer → move_platformer, rpg → move_rpg, grounded → move_grounded).
TUNABLES
| Attribute | Default | What it does | Change it when… |
|---|---|---|---|
gravity | 800.0 | px/s² applied to airborne sprites each frame (physics modes) | your jump arcs feel floaty or stiff |
max_fall_speed | 600.0 | terminal velocity cap on falling | sprites punch through thin floors at high fall speed |
jump_strength | -400.0 | negative vy applied on jump (negative = up) | tuning jump height |
horizontal_speed | 200.0 | built-in input_x * horizontal_speed sets vx | walk/run speed feels wrong |
step_height | 4.0 | max stair/step height a grounded sprite climbs (px) | you want to hop small ledges (raise) or not (lower) |
ground_snap_tolerance | 2.0 | how far the runner snaps a sprite onto ground | sprites slide off 1px lips |
max_walk_angle | 60.0 | degrees from horizontal; steeper slopes are walls in move_platformer_with_slide | slopes feel too climbable / not climbable enough |
slide_friction | 0.1 | accepted for editor/game-type compatibility; no movement method reads it (must be in [0, 1]) | — |
rpg_snap_to_grid | False | RPG-mode config flag (kept false; movement stays free) | — |
ATTACH / DETACH / FROM_WORLD
Without an attached world the runner resolves against whatever (tileset_collision, tile_map) you pass per call. Attach a PhysicsWorld and it resolves against the world's tiles and bodies uniformly; you pass None, None:
runner = CollisionRunner.from_world(world, game_type="platformer") # preset + attach
# or the legacy two-step:
runner = CollisionRunner()
runner.attach(world) # adopts world.tile_size + render_scale
runner.detach() # back to per-call tile arguments
# one-off override of an attached world (does not change the attachment):
result = runner.move_platformer(player, None, None, dt, input_x=1.0, world=other_world)- Attaching overrides the tile source and grid geometry;
detach()falls back to per-call args. - Multiple maps → one world per map, re-attach (or the per-call
world=override). - The runner also offers
get_tile_at(world_x, world_y)andget_nearby_tile_shapes(...)for queries outside movement.
GID ROUTING — MULTI-TILESET MAPS
Maps with several type:"tile" resources encode cells as global ids: gid = firstgid + local_variant, stacked in resource order. Collision files, however, are keyed by the local variant of their own tileset. Feeding a plain local-keyed file into a GID world used to miss every lookup — floors silently stopped being solid.
Since 5.0.5, PhysicsWorld.from_map(..., use_gids=True) fixes this automatically. It records every grid resource's [firstgid, firstgid + tile_count) window and stem-matches tileset_collision.tileset_name against them. Every tile lookup is then range-routed:
1. find the grid resource whose window contains the gid
2. owner != collision file's tileset -> None (decoration grids are never solid)
3. owner == collision file's tileset -> tiles[gid - firstgid]col = load_tileset_collision("data/collision/tileset.collision.json") # local keys ("2", "575", ...)
world = PhysicsWorld.from_map(map_data, col, use_gids=True) # map has N grid tilesets
world.has_collision_gid(92) # True -> jungle local 2
world.has_collision_gid(1813) # False -> belongs to another grid resource, never aliasesCOLLISIONRESULT FLAGS
One dataclass, reset before each call: collided, final_x, final_y, hit_wall_x, hit_wall_y, hit_ceiling, on_ground, slide_vector. Full semantics on the Physics & Bodies page; the short version: branch on hit_wall_x for walls, on_ground for landing, and read slide_vector in slide mode.
PER-MODE WIRING
Top-down (slide)
runner = CollisionRunner.from_game_type("topdown", tile_size=(32, 32))
# sprite needs only x, y, collision_shape
dx = (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 160.0 * dt
dy = (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * 160.0 * dt
result = runner.move(player, tileset, tile_map, delta_x=dx, delta_y=dy)
if result.slide_vector:
# move_and_slide kept a component you can build on
passPlatformer
runner = CollisionRunner.from_game_type("platformer", tile_size=(32, 32))
# sprite needs x, y, collision_shape, vx, vy, on_ground
axis = (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) # -1 / 0 / 1
result = runner.move_platformer(
player, tileset, tile_map, dt,
input_x=float(axis), jump_pressed=keys[pygame.K_SPACE],
)
if result.on_ground:
# step-up worked; you can jump next frameExplicit velocity (knockback, crates, custom controllers)
# velocity= skips gravity, input and jump; adopts (vx, vy) onto the sprite
result = runner.move_grounded(enemy, None, None, dt, velocity=(enemy.vx, enemy.vy))
if result.hit_wall_x:
enemy.vx = 0.0 # runner zeroed vy on landing for youVALIDATE_CONFIG & STRICT
Presets call validate_config() automatically. Manual rules worth knowing:
PLATFORMERmode requiresgravity > 0; zero gravity is aValueError.RPGmode withgravity > 0is aValueError; top-down with gravity just warns (it's ignored inmove_and_slide).gravity < 0andmax_fall_speed < 0are errors; positivejump_strengthwarns.strict=Trueturns warnings into raisedValueError.