|
| 1 | +--- |
| 2 | +sidebar_position: 1000 |
| 3 | +title: Tilemaps |
| 4 | +--- |
| 5 | + |
1 | 6 | # Tilemaps |
2 | 7 |
|
3 | | -Tilemaps allow you to quickly create maps using a predefined tileset. |
| 8 | +Tilemaps let you paint 2D levels quickly using a tileset (a grid of sprites). |
| 9 | +A **Tilemap** entity is a regular entity, so you can move, scale, parent, |
| 10 | +and duplicate it like anything else. |
4 | 11 |
|
5 | | -## Creating New Tilemap And Assigning Pallete |
6 | 12 | <video src="/img/liltilemapguide.mov" style={{ maxWidth: "100%" }} autoPlay controls></video> |
| 13 | + |
| 14 | +## Quick start |
| 15 | + |
| 16 | +1. **Create → Tilemap** in the scene graph. |
| 17 | +2. Drag a spritesheet PNG onto **Atlas** in the Properties Panel. |
| 18 | + The editor auto-slices it into `resolution x resolution` squares |
| 19 | + (64 x 64 px by default). |
| 20 | +3. Open the Tilemap tab on the bottom panel when the Tilemap entity is selected. |
| 21 | +4. Pick a tile and start painting. |
| 22 | + |
| 23 | +> **Tip — scale filtering** |
| 24 | +> Set **Scale filter mode** to “nearest” for crisp pixel-art, |
| 25 | +> or “linear” for smooth scaling. |
| 26 | +
|
| 27 | +--- |
| 28 | + |
| 29 | +## Scripting a tilemap |
| 30 | + |
| 31 | +Need procedural generation or an in-game brush? |
| 32 | +Just ask the entity for its `Tilemap` component and call the helpers. |
| 33 | + |
| 34 | +```ts |
| 35 | +import { Behavior, Rng, syncedValue, Tilemap } from "@dreamlab/engine"; |
| 36 | + |
| 37 | +/** Pick a random element from the array with the supplied PRNG. */ |
| 38 | +function sampleArray<T>(array: readonly T[], prng: () => number): T { |
| 39 | + const idx = Math.floor(Math.abs(prng()) % 1 * array.length); |
| 40 | + return array[idx]!; |
| 41 | +} |
| 42 | + |
| 43 | +export default class Generate extends Behavior { |
| 44 | + #tilemap = this.entity.cast(Tilemap); |
| 45 | + |
| 46 | + @syncedValue() |
| 47 | + seed: number = 0; |
| 48 | + |
| 49 | + /** Grass‑only palette IDs. */ |
| 50 | + static readonly #GRASS = [1,2,3,8,9,10,11,16,17,18,19,24,25,26,27] as const; |
| 51 | + /** Flower‑only palette IDs. */ |
| 52 | + static readonly #FLOWER = [4,5,6,7,12,13,14,20,21,22,23,28,29,30,31] as const; |
| 53 | + |
| 54 | + |
| 55 | + /** Deterministic RNG tied to `seed`. */ |
| 56 | + #prng!: () => number; |
| 57 | + |
| 58 | + #setRng(): void { |
| 59 | + this.#prng = Rng.Seeded(BigInt(this.seed)); |
| 60 | + } |
| 61 | + |
| 62 | + /** Decide which palette to use, then pick a sprite ID from it. */ |
| 63 | + #getTile(): number { |
| 64 | + const roll = this.#prng(); // 0 … 1 |
| 65 | + if (roll < 0.05) return 0; // empty |
| 66 | + if (roll < 0.8) { // grass |
| 67 | + return sampleArray(Generate.#GRASS_TILES, this.#prng); |
| 68 | + } |
| 69 | + return sampleArray(Generate.#FLOWER_TILES, this.#prng); |
| 70 | + } |
| 71 | + |
| 72 | + clearData(): void { |
| 73 | + this.#tilemap.clearTiles(); |
| 74 | + } |
| 75 | + |
| 76 | + generateMap(size = 50): void { |
| 77 | + for (let x = 0; x < size; x++) { |
| 78 | + for (let y = 0; y < size; y++) { |
| 79 | + this.#tilemap.setTile(x, y, this.#getTile()); |
| 80 | + } |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + onInitialize() { |
| 85 | + // Re‑seed whenever the network‑synced seed value changes. |
| 86 | + this.values.get("seed")?.onChanged(() => { |
| 87 | + this.#setRng(); |
| 88 | + if (this.game.isServer()) this.generateMap(); |
| 89 | + }); |
| 90 | + |
| 91 | + this.#setRng(); |
| 92 | + if (this.game.isServer()) this.generateMap(); |
| 93 | + } |
| 94 | + |
| 95 | + onFrame(): void { |
| 96 | + if (!this.game.isClient()) return; |
| 97 | + |
| 98 | + const world = this.inputs.cursor.world; |
| 99 | + if (!world) return; |
| 100 | + |
| 101 | + const left = this.inputs.getKey("MouseLeft"); |
| 102 | + const right = this.inputs.getKey("MouseRight"); |
| 103 | + if (!left && !right) return; |
| 104 | + |
| 105 | + const tile = this.#tilemap.getTileAtPoint(world); |
| 106 | + if (!tile) return; |
| 107 | + |
| 108 | + const id = left ? 33 /* hard‑coded brush */ : this.#getTile(); |
| 109 | + this.#tilemap.setTile(tile.x, tile.y, id); |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +## Core API |
| 117 | + |
| 118 | +| Method | Purpose | |
| 119 | +|------------------------------------------|-------------------------------------------------------------| |
| 120 | +| `getTile(x, y)` | Returns `TileData` \| `undefined` for that coordinate. | |
| 121 | +| `getTilePaletteId(x, y)` | Returns palette index (`number`) or `undefined`. | |
| 122 | +| `setTile(x, y, paletteId?)` | Sets tile; pass `undefined` to erase. | |
| 123 | +| `getTileAtPoint(worldPos)` | Ray-casts the cursor; returns `{ x, y, …tile }` or `undefined`. | |
| 124 | +| `clearTiles()` | Removes every tile from the map. | |
| 125 | +| `tiles()` | Generator that yields `{ x, y, tile }` for each filled cell.| |
| 126 | +| `bounds` *(getter)* | `{ width, height, offset }` in tile units. | |
| 127 | + |
| 128 | +### `TileData` union |
| 129 | + |
| 130 | +```ts |
| 131 | +type TileData = |
| 132 | + | { type: "color"; color: string; alpha?: number } |
| 133 | + | { type: "texture"; texture: string } |
| 134 | + | { type: "spritesheet"; spritesheet: string; frame: number } |
| 135 | + | { type: "texture-slice"; texture: string; x: number; y: number }; |
| 136 | +``` |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +### FAQ |
| 141 | + |
| 142 | +**How big can a tilemap be?** |
| 143 | +Technically unlimited, but performance is tuned for ≤ 10 000 drawn tiles. |
| 144 | + |
| 145 | +**Can I swap atlases at runtime?** |
| 146 | +Yes — just assign a new path to **Atlas**; all visuals update automatically. |
| 147 | + |
| 148 | +**How do I force pixel-perfect textures?** |
| 149 | +Set **Scale filter mode** to “nearest” on the Tilemap *or* on the Camera. |
0 commit comments