TileMap¶
SimVX provides grid-based 2D level design through three classes: TileSet (tile definitions), TileMapLayer (a single grid layer), and TileMap (a Node2D container with multiple layers).
TileData¶
Each tile in a TileSet is described by a TileData dataclass:
Field |
Type |
Description |
|---|---|---|
|
|
Rectangle in the texture atlas |
|
|
Polygon vertices for collision |
|
|
Walkable polygon for pathfinding |
|
|
Arbitrary per-tile metadata |
|
|
Tile IDs for animated tiles |
|
|
Animation FPS (default 5.0) |
|
|
Terrain label for auto-tiling |
|
|
Which terrain set this tile belongs to |
|
|
Bitmask for auto-tile neighbor matching |
TileSet¶
A TileSet holds tile definitions and terrain auto-tile rules:
from simvx.core import TileSet, TileData
ts = TileSet(tile_size=(16, 16))
# Add tiles manually
grass = ts.add_tile(TileData(texture_region=(0, 0, 16, 16), terrain_type="grass"))
water = ts.add_tile(TileData(texture_region=(16, 0, 16, 16), terrain_type="water"))
# Or auto-create from a grid atlas (256x256 image, 16x16 tiles)
tile_ids = ts.create_from_grid(atlas_width=256, atlas_height=256)
TileMapLayer¶
A TileMapLayer stores tiles in 32x32 chunks for efficient large-map support:
from simvx.core import TileMapLayer
layer = TileMapLayer("Ground")
layer.set_cell(5, 3, grass) # Place tile at grid (5, 3)
tid = layer.get_cell(5, 3) # Returns tile ID (-1 if empty)
layer.erase_cell(5, 3) # Remove tile
cells = layer.get_used_cells() # All non-empty (x, y) positions
rect = layer.get_used_rect() # Bounding (x, y, w, h) of used cells
TileMap Node¶
TileMap is a Node2D that manages multiple layers and provides coordinate conversion:
from simvx.core import TileMap, TileSet, TileData
ts = TileSet(tile_size=(16, 16))
ground = ts.add_tile(TileData(texture_region=(0, 0, 16, 16)))
wall = ts.add_tile(TileData(texture_region=(16, 0, 16, 16)))
tilemap = TileMap(tile_set=ts, cell_size=(16, 16))
# Paint on layer 0 (created automatically)
tilemap.set_cell(0, 0, 0, ground)
tilemap.set_cell(0, 1, 0, wall)
# Add a second layer
overlay = tilemap.add_layer("Decoration")
tilemap.set_cell(overlay, 0, 0, ground)
# Coordinate conversion
grid_pos = tilemap.world_to_map((48.0, 32.0)) # -> (3, 2)
world_pos = tilemap.map_to_world((3, 2)) # -> (56.0, 40.0) center of cell
Terrain Auto-Tiling¶
Auto-tiling selects tile variants based on a 4-bit neighbor bitmask (up=1, right=2, down=4, left=8). Neighbors match when they share the same terrain_type.
ts.add_terrain_set(0)
ts.set_terrain_tile(0, 0b0000, lone_tile) # No matching neighbors
ts.set_terrain_tile(0, 0b0101, vertical) # Up + down
ts.set_terrain_tile(0, 0b1010, horizontal) # Right + left
ts.set_terrain_tile(0, 0b1111, center) # All four neighbors
# Apply auto-tiling at a cell
tilemap.auto_tile(layer=0, x=5, y=3, terrain_set=0)
Movement-Range Highlights¶
TileMap.highlight_cells(cells, colour) overlays a translucent fill on each named grid cell: the building block for tactics/strategy UIs that need to show movement range, attack range, targeting reticles, or area-of-effect previews. Multiple groups stack so distinct overlays (move + attack, ally + enemy) can render in one frame.
move_cells = [(cx, cy) for cx in range(2, 7) for cy in range(2, 7)]
attack_cells = [(8, 5), (5, 8)]
tilemap.highlight_cells(move_cells, colour=(0.3, 0.6, 1.0, 0.45)) # blue
tilemap.highlight_cells(attack_cells, colour=(1.0, 0.25, 0.25, 0.5)) # red
# Later, when the unit deselects:
tilemap.clear_highlights()
Notes:
The colour is a 4-tuple in 0.0-1.0 range. 3-tuples receive a default alpha of
0.45: enough to read tile detail underneath while staying clearly visible.Each call adds a new group. Use
tilemap.highlight_groups(a read-only defensive copy) to inspect the current overlays in tests.Fills render via
TileMap.draw(renderer)attilemap.position + cell * cell_size: they follow whatever 2D camera transform the renderer is using.
Y-sort caveats¶
Y-sort (whether via YSortContainer or any per-child depth pass) applies to direct children only. Nesting a group of per-cell sprites under an intermediate Node2D like _floor or _units causes the group to sort as a single unit, not per descendant. This is fine for flat ground where everything in the group shares one logical depth, but wrong for isometric scenes or any layout with elevation.
tilemap = TileMap()
# WRONG for iso / elevation: the whole group sorts as one unit.
floor = Node2D(name="_floor")
tilemap.add_child(floor)
for x, y in cells:
floor.add_child(Sprite2D(...)) # all sprites share floor's Y for sort
# RIGHT: direct children sort individually by their own Y.
for x, y in cells:
tilemap.add_child(Sprite2D(position=Vec2(x*16, y*16)))
To catch the bug pattern early, TileMap.add_child emits a single WARNING the first time a nested-group child is added:
TileMap 'TileMap' received nested child '_floor' with 64 descendant(s).
Y-sort applies only to direct TileMap children: the nested group sorts
as a unit, not per descendant. Acceptable for flat ground; for isometric
/ elevated scenes attach sprites as direct children. See
docs/core/tilemap.md (Y-sort caveats). Set tilemap.warn_on_nested_ysort
= False to silence.
For flat-ground use cases where the warning is noise, disable it with tilemap.warn_on_nested_ysort = False before adding the group. Ports with mixed flat-and-elevated regions should attach the elevated sprites directly under the TileMap and reserve nested groups for purely decorative layers that don’t participate in Y-sort.
API Reference¶
See simvx.core.tilemap for the complete tilemap API.