Skip to content

Architecture

Package overview

tiledpy/
├── __init__.py               Public API — re-exports everything below
├── map/
│   ├── map.py                TileMap (pure data model) + OFFSET enum
│   ├── parser.py             Parser — file → TileMap (XML and JSON)
│   └── render.py             Pygame rendering + global surface caches
└── layer/
    ├── tileset.py            Tileset, TileMeta, TileFlags, decode_gid
    ├── tile.py               TileData (positioned tile), TileLayer
    └── object.py             TileObject, ObjectLayer

Each module has a single responsibility:

Module Responsibility
map/parser.py Parse .tmx / .tmj files into data structures
map/map.py Data model: dimensions, layers, tilesets, coordinate helpers
map/render.py Pygame rendering with viewport culling and surface caches
layer/tileset.py Spritesheet cropping (Pillow) + pygame surface cache
layer/tile.py Positioned tile with get_surface() / get_animated_surface()
layer/object.py Tiled objects (spawn points, triggers, hitboxes)

Class diagram

classDiagram
    class Parser {
        <<static>>
        +load(path) TileMap
        -_parse_xml(path) TileMap
        -_parse_json(path) TileMap
        -_parse_tsx(firstgid, path) Tileset
        -_parse_tile_layer(data) TileLayer
        -_parse_object_layer(data) ObjectLayer
    }

    class TileMap {
        +int width
        +int height
        +int tile_width
        +int tile_height
        +str orientation
        +bool infinite
        +str background_color
        +dict properties
        +list~Tileset~ tilesets
        +list layers
        +get_layer(name) LayerType
        +get_tile_layers() list~TileLayer~
        +get_object_layers() list~ObjectLayer~
        +world_to_tile(x, y, scale, offset) tuple
        +tile_to_world(tx, ty, scale, offset) tuple
    }

    class OFFSET {
        <<enum>>
        LEFT_TOP
        MIDDLE_TOP
        RIGHT_TOP
        LEFT_MIDDLE
        CENTER
        RIGHT_MIDDLE
        LEFT_BOTTOM
        MIDDLE_BOTTOM
        RIGHT_BOTTOM
    }

    class render {
        <<module>>
        -dict _surface_cache
        -dict _scaled_cache
        +draw_layer(surface, layer, tw, th, offset, scale)
        +draw_all_layers(surface, tilemap, offset, scale)
        +clear_cache()
        +cache_stats() dict
    }

    class Tileset {
        +str name
        +int firstgid
        +int tile_width
        +int tile_height
        +int columns
        +int tilecount
        +int spacing
        +int margin
        +dict~int,TileMeta~ tile_data
        -Image _sheet
        -dict _pil_cache
        -dict _pygame_cache
        +get_tile_image(local_id) Image
        +get_pygame_surface(local_id, flags) Surface
        +is_empty_tile(local_id) bool
        +get_dominant_color(local_id) tuple
        +contains_gid(gid) bool
        +global_to_local(gid) int
        +clear_pygame_cache()
    }

    class TileMeta {
        +int local_id
        +str tile_class
        +dict properties
        +list collision_objects
        +list animation
        +int width
        +int height
    }

    class TileFlags {
        +bool flip_h
        +bool flip_v
        +bool flip_d
    }

    class TileData {
        +int tx
        +int ty
        +int local_id
        +Tileset tileset
        +bool flip_h
        +bool flip_v
        +bool flip_d
        +meta TileMeta
        +is_animated bool
        +tile_class str
        +properties dict
        +collision_objects list
        +width(scale) int
        +height(scale) int
        +get_surface(scale) Surface
        +get_animated_surface(elapsed_ms, scale) Surface
    }

    class TileLayer {
        +int id
        +str name
        +bool visible
        +float opacity
        +float offset_x
        +float offset_y
        +dict properties
        -dict _data
        +load_from_flat(data, w, h)
        +load_from_chunks(chunks)
        +get_raw_gid(tx, ty) int
        +get_tile(tx, ty) TileData
        +iter_tiles() Iterator~TileData~
        +get_tiles_by_class(cls) list
        +get_tiles_by_property(prop, value) list
        +get_animated_tiles() list
    }

    class ObjectLayer {
        +int id
        +str name
        +bool visible
        +float opacity
        +str color
        +list~TileObject~ objects
        +dict properties
        +get_object(name) TileObject
        +get_objects_by_class(cls) list
    }

    class TileObject {
        +int id
        +str name
        +str object_class
        +float x
        +float y
        +float rotation
        +bool visible
        +dict properties
        +int gid
        +width(scale) float
        +height(scale) float
    }

    Parser ..> TileMap : creates
    Parser ..> Tileset : creates
    Parser ..> TileLayer : creates
    Parser ..> ObjectLayer : creates
    TileMap "1" --> "0..*" Tileset : tilesets
    TileMap "1" --> "0..*" TileLayer : layers
    TileMap "1" --> "0..*" ObjectLayer : layers
    TileMap ..> OFFSET : world_to_tile / tile_to_world
    render ..> TileLayer : reads _data
    render ..> Tileset : get_pygame_surface
    TileLayer "0..*" --> "0..*" Tileset : _tilesets
    TileLayer ..> TileData : iter_tiles / get_tile
    Tileset "1" --> "0..*" TileMeta : tile_data
    TileData --> Tileset : tileset
    TileData ..> TileMeta : meta (delegated)
    TileData ..> TileFlags : _flags()
    ObjectLayer "1" --> "0..*" TileObject : objects

GID decoding

flowchart LR
    A[raw GID\n32-bit int] --> B[decode_gid]
    B --> C[real_gid\nbits 0–28]
    B --> D[TileFlags\nflip_h · flip_v · flip_d]

    C --> E{Cache hit?}
    E -- yes --> F[pygame.Surface]
    E -- no --> G[_find_tileset\nbinary search]
    G --> H[global_to_local\ngid - firstgid]
    H --> I[Tileset._crop_tile\nPillow crop]
    I --> J{Flags?}
    J -- flip/rotate --> K[PIL.Image.transpose]
    J -- none --> L[PIL.tobytes]
    K --> L
    L --> M[pygame.image.fromstring\n.convert_alpha]
    M --> N[store in _surface_cache]
    N --> F

Cache architecture

Four cache levels from slowest (miss) to fastest (hit):

flowchart TD
    subgraph "render.py — global, shared across all layers"
        SC["_surface_cache\n(firstgid, local_id, fh, fv, fd)\n→ pygame.Surface"]
        KC["_scaled_cache\n(id surf, scale)\n→ pygame.Surface (scaled)"]
    end

    subgraph "tile.py — module-level, used by TileData"
        DC["_scaled_cache\n(id surf, scale)\n→ pygame.Surface (scaled)"]
    end

    subgraph "Tileset._pygame_cache — per instance"
        TC["(local_id, fh, fv, fd)\n→ pygame.Surface"]
    end

    subgraph "Tileset._pil_cache — per instance"
        PC["local_id\n→ PIL.Image"]
    end

    R1[render.draw_layer] --> SC
    SC -- miss --> TC
    TC -- miss --> PC
    PC -- miss --> CROP[Pillow crop\nfrom spritesheet]
    CROP --> PC
    SC -- scale != 1 --> KC

    R2[TileData.get_surface\nor get_animated_surface] --> TC
    TC -- miss --> PC
    R2 -- scale != 1 --> DC
Cache Cleared by
render._surface_cache render.clear_cache()
render._scaled_cache render.clear_cache()
tile._scaled_cache clear_tile_cache()
Tileset._pil_cache persists for life of the Tileset
Tileset._pygame_cache tileset.clear_pygame_cache()