BGE/TileMap
Alias: BGE.TileMap
Static Methods
getNeighbourMask4(
grid: object,
r: integer,
c: integer,
options?: BGE.TileMap.AutoTileOptions,
): integer
Which of a cell's four side neighbours are the same surface as it, as a 4-bit mask: N = 1, E = 2, S = 4, W = 8 (clockwise from the top). A bit is set when that neighbour matches. The result (0-15) indexes a 16-tile set directly.
The grid is a list of rows (row 0 at the top). Each row is either a string, one character per cell, or an array of cell values such as integer tile ids. Two cells are the same surface when their values are equal. Use one value type throughout a grid - comparing a string with an integer is a runtime error. A missing cell (past the end of a short row) or an invalid value counts as out of the map.
Parameters
grid(object) — the map: an array of rows, each a string or an array of cell valuesr(integer) — row index (0 = top)c(integer) — column index (0 = left)options(BGE.TileMap.AutoTileOptions, optional, default: "{}")
Returns
integer— 0-15
Example
rows = [
"....",
".cc.",
"...."
]
mask = BGE.TileMap.getNeighbourMask4(rows, 1, 1) ' 2 - only the east neighbour matches
' The same map as tile ids
grid = [
[0, 0, 0, 0],
[0, 3, 3, 0],
[0, 0, 0, 0]
]
mask = BGE.TileMap.getNeighbourMask4(grid, 1, 1) ' 2getNeighbourMask8(
grid: object,
r: integer,
c: integer,
options?: BGE.TileMap.AutoTileOptions,
): integer
Which of a cell's eight neighbours are the same surface as it, as an 8-bit mask, clockwise from the top: N = 1, NE = 2, E = 4, SE = 8, S = 16, SW = 32, W = 64, NW = 128. This is the common "blob" numbering - pass the result to getBlobTileIndex() to pick one of 47 tiles.
Parameters
grid(object) — the map: an array of rows, each a string or an array of cell valuesr(integer) — row index (0 = top)c(integer) — column index (0 = left)options(BGE.TileMap.AutoTileOptions, optional, default: "{}")
Returns
integer— 0-255
reduceBlobMask(mask8: integer): integer
Simplifies an 8-bit neighbour mask (see getNeighbourMask8()) to the form a 47-tile blob set uses: a corner neighbour only matters when both sides next to it also match, so its bit is cleared otherwise. Every possible mask reduces to one of 47 values.
Use this to look tiles up yourself when your sheet's 47 tiles are laid out in some other order than getBlobTileIndex() assumes.
Parameters
mask8(integer) — 0-255
Returns
integer— the reduced mask, still 0-255
getBlobTileIndex(mask8: integer): integer
Picks one of the 47 tiles in a blob tile set (0-46) from a cell's 8-bit neighbour mask. Tiles are numbered by their reduced mask (see reduceBlobMask()) in ascending order: tile 0 is an isolated cell with no matching neighbours, tile 46 is fully surrounded.
Blob sheets don't share one layout, so if yours is arranged differently, map reduceBlobMask()'s value to your own tile positions instead.
Parameters
mask8(integer) — 0-255, from getNeighbourMask8()
Returns
integer— 0-46
Example
mask = BGE.TileMap.getNeighbourMask8(grid, r, c)
tile = BGE.TileMap.getBlobTileIndex(mask)
region = CreateObject("roRegion", sheet, (tile mod 8) * 32, (tile \ 8) * 32, 32, 32)getEdgePiece3x3(
grid: object,
r: integer,
c: integer,
options?: BGE.TileMap.AutoTileOptions,
): object
Picks a piece from a 3x3 edge set - four corners, four edges and a centre, drawn as a surface bordered by another - using a cell's four side neighbours. This is the simplest auto-tile sheet, but it has no inner-corner or one-cell-wide pieces: an inner corner (only a diagonal neighbour differs) and a one-cell-wide run both get the centre piece. Use a 47-tile set with getBlobTileIndex() if you need those.
Parameters
grid(object) — the map: an array of rows, each a string or an array of cell valuesr(integer) — row index (0 = top)c(integer) — column index (0 = left)options(BGE.TileMap.AutoTileOptions, optional, default: "{}")
Returns
object— {col, row}, 0-2 each: the piece's position within the 3x3 set
Example
piece = BGE.TileMap.getEdgePiece3x3(grid, r, c)
region = CreateObject("roRegion", sheet, piece.col * 32, piece.row * 32, 32, 32)autoTileAxisPiece(
leadingDiffers: boolean,
trailingDiffers: boolean,
): integer
0 = leading edge (west/north), 2 = trailing edge (east/south), 1 = centre.
Parameters
leadingDiffers(boolean)trailingDiffers(boolean)
Returns
integer
autoTileOutOfBoundsMatches(
options: BGE.TileMap.AutoTileOptions,
): boolean
Parameters
options(BGE.TileMap.AutoTileOptions)
Returns
boolean
autoTileMatches(
grid: object,
r: integer,
c: integer,
surface: dynamic,
outOfBoundsMatches: boolean,
): boolean
Parameters
grid(object)r(integer)c(integer)surface(dynamic)outOfBoundsMatches(boolean)
Returns
boolean
autoTileCell(grid: object, r: integer, c: integer): dynamic
A cell's value, or invalid when the column is past the end of its row.
Parameters
grid(object)r(integer)c(integer)
Returns
dynamic
getBlobTileIndexTable(): object
Reduced mask -> 0-46, built once on first use and kept on the global AA.
Returns
object
bakeTileMapImages(
tiles: Array.<BGE.TileMap.TileSpec>,
chunkSize: float,
): Array.<BGE.TileMap.BakedChunk>
Bakes a sparse grid of stationary tiles into one or more chunked bitmaps - so a whole screen's worth of static tiles (a platformer level's ground, say) costs one SceneObject/draw call per chunk instead of one per tile. Only chunks that actually contain at least one tile are returned.
Each tile is drawn into its chunk exactly once, at bake time (when this function runs) - not per frame - so this is a one-time cost paid when the level is built, not a recurring one.
chunkSize only controls which tiles get grouped together (tiles are bucketed by Int(worldX / chunkSize), Int(worldY / chunkSize)) - each resulting chunk's bitmap is then sized/anchored to the actual bounding box of the tiles bucketed into it, not to a fixed chunkSize x chunkSize square. This means a tile's footprint is never clipped, even when a visual offset/nudge (e.g. examples/platformer's Level.bs +-5px surface/fill adjustment) pushes it past a grid-aligned bucket boundary - it just makes that one chunk's bitmap a little larger than chunkSize, rather than losing part of the tile.
A screen-aligned Image above ~192px used to silently fail to draw on both the BrightScript Simulator and real hardware, even with fully correct content (confirmed via GetByteArray/tmp:/ PNG round-trip) and a draw call reporting success - traced to Renderer.render() never calling Finish() on its destination before the frame proceeds (see Renderer.bs's own comment on that call, and issue #211). With that fixed, chunkSize up to 512 has been confirmed reliable on real hardware (memory- and disk-sourced images, up to 16 simultaneous 320px blits/frame, sustained). Still keep it well under 768, though: a merged chunk spanning a large mostly-empty vertical gap (e.g. a floating platform merged with unrelated ground tiles far below it) can still silently fail to render at 768+ inside a real running game even though the identical dimensions render reliably in an isolated stress test - see examples/platformer's Level.bs for the specific confirmed-safe/confirmed-broken measurements and the follow-up (issue #213) tracking this remaining gap.
Parameters
tiles(Array.<BGE.TileMap.TileSpec>)chunkSize(float) — grouping granularity, in world pixels (keep <= 512)
Returns
Array.<BGE.TileMap.BakedChunk>
floorDiv(value: float, divisor: float): integer
Floor division (not BrightScript's truncate-toward-zero Int()/\) - a tile at a negative world position must bucket into the chunk below/left of the origin, not get pulled back toward chunk 0.
Parameters
value(float)divisor(float)
Returns
integer
mergeTileColliderRuns(
cells: Array.<BGE.TileMap.ColliderCell>,
): Array.<BGE.TileMap.MergedColliderRun>
Merges a sparse grid of tagged collision cells into the minimal set of same-tag, contiguous-column runs per row - e.g. a solid platformer floor 20 tiles wide becomes one run instead of 20 separate per-tile colliders. Input order doesn't matter (cells are grouped/sorted internally); a gap in columns or a change in tag always starts a new run. Only merges within a single row - two vertically stacked identical runs are NOT combined into one taller rectangle (that's a further optimization, not implemented here - see bakeTileMapImages() for where the bigger per-frame cost actually lives).
Parameters
cells(Array.<BGE.TileMap.ColliderCell>)
Returns
Array.<BGE.TileMap.MergedColliderRun>
Other
AutoTileOptions
Extends: roAssociativeArray
Options for the auto-tiling functions.
Properties
outOfBoundsMatches(boolean) — When true (the default), a neighbour outside the map counts as the same surface, so a
TileSpec
One occupied tile to bake into a chunked tilemap image - addressed by world position (not row/col) since baking is fundamentally about pixels/chunks, not grid adjacency (compare BGE.TileMap.ColliderCell, which stays in grid space).
Properties
worldX(float)worldY(float)region(roRegion)width(float)height(float)
BakedChunk
One non-empty baked chunk: a single pre-composited bitmap, ready to become one Image drawable (one addImage() call instead of one per tile). worldX/worldY is the chunk's own top-left world corner - worldX the chunk's minimum X, but worldY the chunk's MAXIMUM Y (its top edge, matching TileSpec.worldY's own top-edge convention and Image's top-left-anchor convention, where pixel rows increase downward as world Y decreases) - i.e. where to position the resulting drawable via addImage()'s offset. The bitmap's own width/height are sized to this chunk's actual tile content (see bakeTileMapImages()'s doc comment) - not necessarily the chunkSize passed in.
Properties
bitmap(roBitmap)worldX(float)worldY(float)
ColliderCell
One occupied collision cell in a tile grid, addressed by integer row/column - merging is pure adjacency in grid space, so world position doesn't come into it at all (unlike bakeTileMapImages(), which bakes pixels and therefore does need world position/chunking).
Properties
row(integer)col(integer)tag(string)
MergedColliderRun
One merged run of contiguous same-tag cells within a single row.
Properties
row(integer)startCol(integer)endCol(integer)tag(string) — inclusive