tilemap-parserv5.2.0GITHUB

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.

pipeline.pyPYTHON
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

pipelineDIAGRAM
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
BlockJob
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 blockOn hit_wall_x, identify the solid (collides_with_body, probing for the skin gap), and only kinematic bodies get a velocity.
Crate drivemove_grounded(crate, ..., velocity=...): explicit velocity, no gravity. The runner resolves the crate against tiles and other crates; hit_wall_x stops it.
DrawSprites 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.