-
Notifications
You must be signed in to change notification settings - Fork 0
Tile Maps and Collision
A map is a list of rows of integers, a tile size in pixels, and the set of ids that block movement:
from texastoast import TileMap
tilemap = TileMap(
[
[1, 1, 1, 1],
[1, 0, 0, 1],
[1, 1, 1, 1],
],
tile_size=16,
solid_tiles={1},
)The ids mean nothing to the engine. 0 is conventionally empty ground and 1 a
wall, but that is convention, not a rule — solid_tiles is the only thing that
gives an id behavior.
tilemap.get(col, row) # tile id, or -1 out of bounds
tilemap.set(col, row, tile_id) # ignored if out of bounds
tilemap.is_solid(col, row)
tilemap.is_solid_at(world_x, world_y)
tilemap.to_grid_coords(world_x, world_y) # -> (col, row)
tilemap.rows, tilemap.cols # in tiles
tilemap.width, tilemap.height # in pixelsTwo behaviors to know:
-
get()returns-1out of bounds, which is also what a ragged row returns past its end. If you use-1as a real tile id, it will be indistinguishable from "not there". -
Out of bounds counts as solid.
is_solid(-1, 0)isTrue. This means an entity cannot walk off the edge of the map even without a wall drawn there, and a short row in a ragged grid behaves as a wall.
tilemap.save("map.json")
tilemap = TileMap.from_file("map.json")
tilemap = TileMap.from_file("map.json", tile_size=32, solid_tiles={1, 2})Arguments passed to from_file override what the file says. The format is
plain JSON:
{
"grid": [[1, 1, 1], [1, 0, 1], [1, 1, 1]],
"tile_size": 16,
"solid_tiles": [1]
}This is what the Tile Editor writes.
from texastoast import Entity
player = Entity(x=0, y=0, width=16, height=16, speed=100)speed is pixels per second. move() takes the frame's dt:
player.move(dx, dy, dt, tilemap) # collide against the map
player.move(dx, dy, dt) # no collision, just integratedx/dy are a direction, normally -1, 0 or 1 per axis — exactly what
InputState.dx/dy give you. A diagonal is normalized, so holding two
directions is the same speed as one. A partial direction like 0.5 is left
alone, so analog input still works.
player.center_x, player.center_y # useful for camera follow and distances
player.vel_x, player.vel_y # px/second, set by the last move()
player.aabb # AABB for overlap tests
player.collides_with(other)update(dt) is a no-op hook for subclasses.
check_tile_collision resolves the two axes independently: horizontal
first, then vertical from the corrected horizontal position. That is what makes
an entity slide along a wall instead of catching on it — pushing diagonally into
a wall still moves you along it.
Three properties are worth relying on:
It stops flush. A blocked entity is placed exactly against the wall face, not returned to where it started. Walking into a wall leaves no gap.
It cannot tunnel. Movement longer than one tile is split into sub-steps of
at most tile_size, so a fast entity is stopped by a wall it would otherwise
skip over in a single frame. The subdivision is capped at
collision.MAX_SUBSTEPS (256); past that — a step of thousands of pixels in one
frame — tunneling becomes possible again.
Boxes are half-open. A box at x with width w covers [x, x + w). A box
whose right edge lands exactly on a tile boundary does not occupy the tile
beyond it, which is what keeps flush contact stable frame after frame.
Collision assumes the entity starts somewhere legal. An entity spawned inside a solid tile is stuck — nothing pushes it out, because only the leading edge of a move is tested. Check your spawn points:
col, row = tilemap.to_grid_coords(player.x, player.y)
assert not tilemap.is_solid(col, row), "spawned inside a wall"Remember an entity covers more than one tile if it is larger than tile_size or
straddles a boundary.
A plain axis-aligned box for overlap tests, independent of the tile map:
from texastoast import AABB
box = AABB(x=0, y=0, width=10, height=10)
box.left, box.right, box.top, box.bottom
box.intersects(other) # touching edges do NOT count as overlapping
box.contains_point(px, py) # edges DO count as insideThe two differ on edges deliberately: intersects is used for collision, where
flush contact must not read as overlap, while contains_point is for hit
testing, where clicking the border should hit.
texastoast · PyPI · Apache-2.0