When building tile-based games, placing the right tile variant at every position can be tedious. Autotiling algorithms solve this by automatically selecting appropriate tiles based on their neighbors. Let's explore how these algorithms work and implement them interactively.
Imagine you're building a tile-based game world. You have terrain like grass, water, or walls. When you place tiles, the edges need to connect smoothly with their neighbors. A grass tile surrounded by other grass tiles looks different from a grass tile at the edge next to water.
Click on the grid to place or remove tiles. Watch how autotiling automatically selects the right tile variant:
The most common autotiling technique is bitmasking. We examine each tile's neighbors and encode their presence as bits in a number. This number then maps to the appropriate tile variant.
The simplest form checks only the four cardinal directions: North, East, South, and West.
function calculate4BitMask(x, y, grid) {
let mask = 0;
if (grid[y - 1]?.[x]) mask |= 1; // North
if (grid[y]?.[x + 1]) mask |= 2; // East
if (grid[y + 1]?.[x]) mask |= 4; // South
if (grid[y]?.[x - 1]) mask |= 8; // West
return mask;
}
This gives us 16 possible tile configurations (2^4), which is manageable but doesn't handle corner pieces well.
For better visual quality, we also check diagonal neighbors:
function calculate8BitMask(x, y, grid) {
let mask = 0;
// Cardinal directions
const n = grid[y - 1]?.[x] ? 1 : 0;
const e = grid[y]?.[x + 1] ? 1 : 0;
const s = grid[y + 1]?.[x] ? 1 : 0;
const w = grid[y]?.[x - 1] ? 1 : 0;
mask |= n << 0; // North
mask |= e << 2; // East
mask |= s << 4; // South
mask |= w << 6; // West
// Diagonal neighbors (only if adjacent cardinals exist)
if (n && e && grid[y - 1]?.[x + 1]) mask |= 1 << 1; // NE
if (s && e && grid[y + 1]?.[x + 1]) mask |= 1 << 3; // SE
if (s && w && grid[y + 1]?.[x - 1]) mask |= 1 << 5; // SW
if (n && w && grid[y - 1]?.[x - 1]) mask |= 1 << 7; // NW
return mask;
}
While 8-bit bitmasking can produce 256 different values, many of these are visually identical when rotated or mirrored. The popular "47-tile system" (used in RPG Maker) reduces this to just 47 unique tiles.
Each tile in the template serves a specific purpose:
The key insight is that we can break each logical tile into four quarter-tiles, then determine each quarter independently based on only three neighbors:
function getTileQuarters(mask) {
// For each quarter, check relevant neighbors
const quarters = {
topLeft: getQuarterType(mask, 'N', 'W', 'NW'),
topRight: getQuarterType(mask, 'N', 'E', 'NE'),
bottomLeft: getQuarterType(mask, 'S', 'W', 'SW'),
bottomRight: getQuarterType(mask, 'S', 'E', 'SE')
};
return quarters;
}
function getQuarterType(mask, vert, horiz, corner) {
const v = (mask >> directionBit[vert]) & 1;
const h = (mask >> directionBit[horiz]) & 1;
const c = (mask >> directionBit[corner]) & 1;
if (!v && !h) return 'outer-corner';
if (v && !h) return 'vertical-edge';
if (!v && h) return 'horizontal-edge';
if (v && h && !c) return 'inner-corner';
if (v && h && c) return 'solid';
}
Click cells to toggle them and see how the bitmask value changes:
Technique | Tile Count | Visual Quality | Complexity | Use Case |
---|---|---|---|---|
4-bit (Cardinal) | 16 tiles | Basic | Simple | Minimalist games, prototypes |
8-bit (Full) | 256 tiles* | Excellent | Moderate | Requires optimization |
47-tile (RPG Maker) | 47 tiles | Very Good | Moderate | 2D RPGs, top-down games |
Wang Tiles | Varies | Excellent | Complex | Procedural textures |
* Most 8-bit configurations can be reduced through rotation and mirroring
Wang tiles take a different approach. Instead of checking neighbors, each tile edge has a "color" or type, and tiles can only connect if their edges match.
Click tiles to rotate them. Adjacent edges must have matching colors:
class WangTile {
constructor(north, east, south, west) {
this.edges = { north, east, south, west };
}
canConnect(other, direction) {
const opposite = {
north: 'south', south: 'north',
east: 'west', west: 'east'
};
return this.edges[direction] === other.edges[opposite[direction]];
}
rotate() {
this.edges = {
north: this.edges.west,
east: this.edges.north,
south: this.edges.east,
west: this.edges.south
};
}
}
When implementing autotiling in a game, consider these optimizations:
class AutotiledMap {
constructor(width, height) {
this.tiles = Array(height).fill().map(() => Array(width).fill(0));
this.cache = new Map();
this.dirty = new Set();
}
setTile(x, y, value) {
this.tiles[y][x] = value;
// Mark this tile and neighbors as dirty
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const key = `${x + dx},${y + dy}`;
this.dirty.add(key);
this.cache.delete(key);
}
}
}
getTileMask(x, y) {
const key = `${x},${y}`;
if (!this.cache.has(key)) {
this.cache.set(key, this.calculateMask(x, y));
}
return this.cache.get(key);
}
}
Autotiling isn't just for terrain. Here are creative uses: