Map Parsing & Rendering
Loading a map is one call; rendering is one call per frame. load_map() parses the tilemap-editor JSON into a TilemapData, and TileLayerRenderer draws it with chunked culling.
LOADING MAPS
from tilemap_parser import load_map
game_data = load_map("data/map.json")
print(f"Map size: {game_data.map_size}") # (cols, rows)
print(f"Tile size: {game_data.tile_size}") # (w, h), effective px
print(f"Render scale: {game_data.render_scale}")
# Layers, in draw order, filtered by type if you want:
for layer in game_data.get_layers(layer_type="tile"):
print(layer.name, layer.z_index)The raw parsed structure lives on game_data.parsed (see the API reference's parsed data classes). For collision you usually want the layer as a flat grid: game_data.build_tile_map() collapses the tile layers into {(col, row): tile_id}, which is what PhysicsWorld owns.
TILE RENDERING
TileLayerRenderer draws only the visible chunks, respects layer z_index and y_sort, and reports what it did. The camera offset is passed per call; nothing is stored:
from tilemap_parser import TileLayerRenderer
renderer = TileLayerRenderer(game_data)
# In your game loop:
stats = renderer.render(screen, camera.offset)
# stats: LayerRenderStats: drawn_tiles, skipped_tiles, visible_layersrender(target, camera_xy, viewport_size=None, *, extra_objects=None, current_time_ms=None):current_time_msdrives tileset animations.extra_objectslets you blit sprites in the same pass (any objects withsurface,x,y), after the tile layers.warm_cache()pre-bakes every tile variant up front, then frees the source data.
RENDERING & COLLISION OVERLAYS
The renderer is collision-blind: it draws tile textures from each layer's ttype / variant and has no idea whether a tile is solid, one-way, or air. Collision facts live in the world — world.tile_map plus the collision tileset. The split is deliberate: render everything, collide with a subset (exclude_layers only affects the world).
Want debug overlays — say, dashed edges on one-way platforms? Ask the world, not the renderer:
tile_id = world.tile_map.get((tx, ty)) # world id space
if tile_id is None:
continue
tile_data = world.tileset_collision.tiles.get(tile_id)
if tile_data is None:
continue
if any(s.one_way for s in tile_data.shapes):
# dashed top edge at (tx * world.tile_size[0], ty * world.tile_size[1])one_way is authored per polygon in the collision JSON — never in the map JSON — and the platformer family honors it automatically (blocks from above, passes from below), so the query is for visuals only. Two id-space caveats:
- The renderer keys tiles by layer
ttype; the world keys by the collision tile id. Same map, same ints — untiluse_gids=Truemakes the world's ids global and the two spaces diverge. Always query throughworld.tile_map, never through the renderer. - Tiles with no collision entry draw fine but collide as air; layers in
exclude_layersdraw fine but don't exist for physics.
EXTRACTING OBJECTS
Object layers become MapObjects: pre-scaled surfaces, positions and collision shapes, ready to feed an ObjectCollisionManager. load_map_objects takes the map and a directory containing the matching .object_collision.json files:
from tilemap_parser import load_map_objects, ObjectCollisionManager
objects = load_map_objects(game_data, "data/object_collision")
manager = ObjectCollisionManager()
for obj in objects:
manager.add_object(obj)
player_start = next((o for o in objects if o.name == "PlayerStart"), None)
if player_start is not None:
player.x, player.y = player_start.x, player_start.y- Every object layer is iterated; there is no per-layer filter. The first region's layer/mask are adopted by the object.
require_collision=True(the default) returns only objects that have matching collision regions; passFalseto also get visual-only objects (with empty shapes).- All spatial data is pre-scaled by the map's
render_scale; no scaling on your side.
BACKGROUND (IMAGE) LAYERS
Image layers hold a single external image — a parallax sky, backdrop, or full-screen art. They carry no tiles or objects, just image_path and image_rect. The parser parses all image-layer metadata as ParsedLayer with layer_type == "image" (aliases "background" / "background_layer" are also accepted) but TilemapData.load eagerly loads only the first image layer into TilemapData.background_layer; additional image layers remain in data.parsed.layers for manual loading.
data = load_map("data/map.json")
bg = data.background_layer # BackgroundLayer | None
if bg is not None and bg.surface is not None:
# bg.image_path, bg.image_rect (x,y,w,h), bg.surface
pos = (bg.image_rect[0], bg.image_rect[1]) if bg.image_rect else (0, 0)
screen.blit(bg.surface, pos)
# Or query any image layer directly:
for layer in data.get_layers(layer_type="image"):
print(layer.name, layer.image_path, layer.image_rect)image_path: project-relative path; resolved against the map directory (andextra_search_baseif given). Missing files produce a warning andsurface == Noneonly whenskip_missing_imagesis enabled (defaulttrue); whenskip_missing_images=False, loading raisesMapParseError.image_rect: pixel rect(x, y, w, h)where the image is drawn.Nonewhen not authored.- Only the first image/background layer is exposed as
background_layer; all are still indata.parsed.layers.