Tilemaps
A TileLayer draws a whole grid of tiles as one batch. Use it instead of one Sprite per cell: a 200x200 map is 40,000 cells, and as sprites that is 40,000 nodes.
A tile layer
ts
import { Texture, TextureRegion, TileLayer } from "willow";
class TileDemo {
static main(): void {
const tiles: Texture = Texture.load("assets/tileset.png");
const regions: Array<TextureRegion> = TextureRegion.grid(tiles, 16, 16);
const COLS: int = 50;
const ROWS: int = 38;
const data: Array<int> = [];
for (let row: int = 0; row < ROWS; row++) {
for (let col: int = 0; col < COLS; col++) {
const edge: boolean = row === 0 || col === 0 || row === ROWS - 1 || col === COLS - 1;
data.push(edge ? 2 : 1);
}
}
const ground: TileLayer = new TileLayer({
tileWidth: 16, tileHeight: 16, columns: COLS, rows: ROWS,
tileset: tiles, regions: regions, data: data,
});
engine.root2d.add(ground);
ground.setTile(10, 10, TileLayer.encodeGid(3, true));
}
}GIDs. Each cell holds a number. 0 is empty; N draws regions[N - 1]. The top bits carry the horizontal, vertical and diagonal flip flags used by the Tiled editor. Use TileLayer.encodeGid(index, flipH, flipV, flipD) and TileLayer.decodeGid(gid) rather than packing bits yourself.
| Method | Does |
|---|---|
setTile(col, row, gid) | Changes one cell. |
getTile(col, row) | Reads one cell, with any tile animation applied. |
setData(array) | Replaces the whole grid. The length must be columns * rows. |
setAnimation(gid, frames, fps) | Animates every cell showing gid through a list of GIDs, like water or torches. |
query() | A read-only TileQuery for collision and pathfinding. |
Out-of-range cells raise an error rather than being ignored.
Culling big maps
A bare TileLayer draws its whole grid. Put it under a TilemapViewport and only the tiles inside the camera view (plus a margin) are drawn each frame, so a huge map stays cheap:
ts
import { Texture, TextureRegion, TileLayer, TilemapViewport } from "willow";
class BigMap {
static main(): void {
const tiles: Texture = Texture.load("assets/tileset.png");
const data: Array<int> = [];
for (let i: int = 0; i < 512 * 512; i++) {
data.push(1 + (i % 7));
}
const view: TilemapViewport = new TilemapViewport({ padding: 64 });
engine.root2d.add(view);
const ground: TileLayer = new TileLayer({
tileWidth: 16, tileHeight: 16, columns: 512, rows: 512,
tileset: tiles, regions: TextureRegion.grid(tiles, 16, 16), data: data,
});
view.add(ground);
}
}padding is the extra margin, in world pixels, drawn beyond the edge of the view.
Querying tiles
ts
import { Texture, TextureRegion, TileLayer, TileQuery, Sprite } from "willow";
class Walls {
static main(): void {
const tiles: Texture = Texture.load("assets/tileset.png");
const data: Array<int> = [];
for (let i: int = 0; i < 20 * 15; i++) {
data.push(i % 20 === 0 ? 2 : 1);
}
const ground: TileLayer = new TileLayer({
tileWidth: 32, tileHeight: 32, columns: 20, rows: 15,
tileset: tiles, regions: TextureRegion.grid(tiles, 32, 32), data: data,
});
engine.root2d.add(ground);
const map: TileQuery = ground.query();
const hero: Sprite = new Sprite("assets/hero.png");
hero.setPosition(100, 100);
engine.root2d.add(hero);
App.onFrame((dt: float): void => {
let nx: float = hero.x;
if (Keyboard.isDown(Key.LEFT)) {
nx -= 150 * dt;
}
if (map.tileAtWorld(nx, hero.y) !== 2) {
hero.x = nx;
}
});
}
}TileQuery also has tileAt(col, row), rawAt(col, row) (ignoring animation), worldToTile(x, y) and tileToWorld(col, row). Out-of-range reads return 0.
Pair a tilemap with the camera's scrollToTile(col, row, seconds) to move the view to a cell.