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 union grid: game_data.build_tile_map() stacks every tile layer Godot-style into {(col, row): ((gid, flipbits), ...)}, which is what PhysicsWorld holds. Overlapping layers never overwrite each other — a deco tile on top can't erase solid ground below, and each entry keeps its own flip flags.
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 on purpose: 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:
from tilemap_parser import iter_cell_entries
for entry in iter_cell_entries(world.tile_map.get((tx, ty))):
tile_data = world.resolve_stack_entry(entry) # GID-routed + flipped
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 platformer movement already handles it (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 stacked(gid, flipbits)entries. Same map, same gids — untiluse_gids=Truemakes the world's ids global and the two spaces diverge. Always query throughworld.tile_mapwithiter_cell_entries/world.resolve_stack_entry, never through the renderer. - Tiles with no collision entry draw fine but collide as air; layers in
exclude_layersor withcollision_enabled=Falsedraw 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=False(the default) also returns visual-only objects (with empty shapes — gate onhas_collisionbefore adding to the collision manager); passTrueto return only objects that have matching collision regions.- 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, only 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 visible image layer into TilemapData.background_layer; any image layer can be loaded on demand with get_image_layer_surface / get_image_layer_surfaces, which resolve ../../assets-style paths and cache results.
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)
# Lazy surface loading (visible layers by default):
surf = data.get_image_layer_surface("sky") # by name, id, or ParsedLayer
all_bgs = data.get_image_layer_surfaces() # [(layer, surface)] in z-order
hidden_too = data.get_image_layer_surfaces(include_hidden=True)
# One image, placed N times (editor duplicates): single composited surface
composed = data.get_placed_image_layer_surface("sky", render_scale=data.render_scale)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 visible image/background layer is exposed as
background_layer; all are still indata.parsed.layers. Useget_image_layer_surface(layer)/get_image_layer_surfaces()to load any image layer on demand — hidden layers are skipped unless you passinclude_hidden=True. Layers with editor duplicates exposeimage_placements;get_placed_image_layer_surface()composites base + copies into one surface (no manual tiling loops).