Skip to content

Tilemap

namespace Griff.Scenes;
public sealed class Tilemap : Component, ISortsInParts, IDrawable

A grid of tiles from one or more tileset images, drawn and (optionally) collided with as a whole. The grid starts at the entity’s position and runs right and down, on an isometric map (see Shape) as diamonds running down to the right and down to the left, and on a hex map as rows of hexagons. Paint it in the Scene view, edit Tiles as text in the inspector, or change it from scripts with SetTile, FillRect and FloodFill, each of which can also lay its tile turned or flipped (see Turns). A tilemap with no Image on an entity inside another tilemap’s is one of its layers (see LayerOf), painted on its own: it uses that map’s tilesets, tile size and tile settings, while its Tiles (and Turns), Tint, Order and collision (Solid, Layer, IgnoreLayers) are its own.

Each row of Tiles is a line of characters, one per cell, top row first: . is empty and 0-9, a-z, A-Z pick tiles 0–61; tiles from 62 on are accented letters, then CJK characters (see CharOf). Rows are saved one per line, so levels diff line by line in git. How each cell’s tile is turned or flipped is a grid of its own, Turns, lined up with it.

This is a component: its saved properties are on its component page.

public string? AnimatedTiles { get; set; }

Tiles that animate while the game runs, as runs of frames side by side in the tileset with an optional speed in frames a second: “16-19 @6” turns tiles 16, 17, 18 and 19 into water that ripples through all four. Separate runs with commas: “16-19 @6, 30-31 @2”. A cell painted with a later frame starts the run from there, so neighbors needn’t move in step.

public float AnimationTime { get; set; }

Seconds its AnimatedTiles have been playing. Advanced by the game.

public Box Area { get; }

The box in the world round the map’s cells, from its first row and column to its last.

public string? AutoTiles { get; set; }

Runs of 16 tiles in the tileset that fit themselves to their neighbors: paint with any tile of a run and each cell shows the piece that joins up with the cells around it painted from the same run. “16-31” (or just “16”), several separated by commas. A run’s tiles go in order of which sides join, counting up 1, right 2, down 4 and left 8: the first is a lone block, the second joins only above, the last joins on every side.

public string? BackgroundTiles { get; set; }

Tiles that never collide, for decoration like grass, windows or signs: tile numbers and ranges, e.g. “10-15, 20”.

public Tilemap BaseMap { get; }

The map whose tilesets and tile settings this one uses: itself, or for a layer the map it’s a layer of (the bottom one, for a layer of a layer).

public float? BeltTravel { get; set; }

How far a Conveyor whose Belt Tiles this map is has moved its belts, in cells (a cell’s worth kept): while it’s set, its AnimatedTiles step with it rather than with time, a run’s frames making up one cell of travel. Set by the conveyor; null plays them by time.

public IReadOnlyDictionary<ValueTuple<int, int>, float>? BeltTravels { get; set; }

How far the belts on cells of their own speed have moved, by cell, as BeltTravel (which the rest step with). Set by the conveyor; null while all its cells go at one speed.

public int Columns { get; set; }

Tiles across the first tileset’s image.

public int Depths { get; }

How many rows DepthOf gives, back to front.

public Tilemap? GridMap { get; }

The map LinesUpWith names, if it’s in the scene (and isn’t this one).

public int Height { get; }

Number of rows.

public string? IgnoreLayers { get; set; }

Layers that pass through the map’s tiles, like IgnoreLayers, e.g. “Ghost”.

public string? Image { get; set; }

The first tileset’s image, relative to Assets: tiles 0 on. Without one, its tiles draw as solid squares of Tint (or of PlaceholderColors, a color for each tile), or inside another tilemap’s entity, it’s a layer of that map (see LayerOf). MoreTilesets adds others.

public string? LadderTiles { get; set; }

Tiles that are ladders: they never collide, and a PlatformerMovement climbs them with up and down. Tile numbers and ranges, e.g. “12, 13”.

public string? Layer { get; set; }

The collision layer of the map’s tiles, like Layer. Empty is “Default”.

public Tilemap? LayerOf { get; }

The tilemap this one is a layer of, or null when it isn’t one. A tilemap with no Image of its own on an entity inside another tilemap’s entity is a layer of that map: it’s drawn with that map’s tilesets, TileSize and tile settings (Background, One Way, Ladder, Slope, Animated and Auto Tiles), while its Tiles, Tint, Order and collision (Solid, Layer, IgnoreLayers) are its own. Giving it an Image makes it a map of its own again. Art > Add Layer to Tilemap adds one.

public string? LinesUpWith { get; set; }

Another tilemap, by its entity’s name, whose cells this one’s line up with, e.g. “Level”: it takes that map’s TileSize and Shape, and its top left stays on a corner of that map’s cells (moved to the nearest as it’s set, as it’s dragged in the Scene view, and while the game runs), so a match-3 or falling-blocks board in a platformer level sits on the level’s squares. Its own tiles stay its own: with Solid ticked, a board’s settled pieces are walls and floors the level’s bodies stand on. Empty lines up with nothing.

public IReadOnlyList<string> MoreTilesets { get; set; }

Tilesets after the first (Image), one per line: an image and its tiles across and down, like “Tiles/props.png 4x4”. Each one’s tiles are numbered on from the next hundred after the tiles before it: with a first tileset of 16 tiles, the second’s are 100 on and a third’s 200 on, so a tileset that grows (up to the next hundred) doesn’t renumber the ones after it. Tile number fields (Background Tiles, Auto Tiles and the rest) and GetTile use these numbers. Art > Add Tileset to Tilemap adds one.

public string? OneWayTiles { get; set; }

Tiles that only block bodies landing on top, like OneWay: tile numbers and ranges, e.g. “4, 5”.

public int Order { get; set; }

Higher draws on top, like Order.

public IReadOnlyList<string> PlaceholderColors { get; set; }

For a map with no Image: a color for each tile number, in order, to block a level out before there’s any art. Tile 0 draws as a solid shape of the first color, tile 1 of the second and so on (times Tint), so a floor can be checkered and wall tops told from wall faces; a number past the end goes round the list again. The palette in the Scene view offers a tile for each color to paint with. Empty (the default), every tile is a solid shape of the tint. A layer uses its map’s (see LayerOf), so every layer’s tile 2 is the same color.

public bool PlaceholderGrid { get; set; }

For a map with no Image: draws a thin line round each tile, a little darker than it (lighter on a dark one), so the cells show on a floor of one color. Off by default. A layer uses its map’s (see LayerOf).

public int Rows { get; set; }

Tiles down the first tileset’s image.

public TileShape Shape { get; set; }

How the cells are laid out: Square in rows and columns, or Isometric as diamonds twice as wide as they’re tall, the grid’s columns running down to the right from the entity’s position (the top corner of cell 0, 0) and its rows down to the left. Tiles, painting, CellAt and CellCenter all follow it, and so do collisions: on an isometric map a body meets a solid tile’s diamond with the diamond under its collider (see Body), sliding along the walls, and grid movers step from diamond to diamond. Hex makes the cells hexagons pointing up and down, a tile wide and a tile tall, in rows three quarters of a tile apart with every odd row (the second, fourth…) shifted half a tile to the right, the entity’s position at the top left of cell 0, 0’s picture; grid movers step from hexagon to hexagon on them (and Tactics units walk and fight over them), and a body’s collider meets a solid hexagon’s six sides and slides along them. A layer’s is its map’s (see LayerOf).

public string? SlopeDownTiles { get; set; }

Tiles that are slopes falling to the right, like “\”: solid below the line from their top-left corner to their bottom-right one.

public string? SlopeUpTiles { get; set; }

Tiles that are slopes rising to the right, like “/”: solid below the line from their bottom-left corner to their top-right one, so bodies walk up and down them. Tile numbers and ranges, e.g. “20”.

public bool Solid { get; set; }

When set, tiles are walls and floors that bodies collide with (except BackgroundTiles).

public bool SortByY { get; set; }

Draws each row of tiles (each diagonal row, on an isometric map) in depth among the sprites, as the camera’s SortByY does for things that move: whatever stands lower down draws in front, so a character walks behind a wall and in front of it. Needs the camera’s Sort By Y on too. Off, the map draws as one, behind what’s lower in the Hierarchy at its Order: right for floors, so put walls and props on a layer of their own (see LayerOf) and turn this on there. The tiles always draw back to front among themselves.

public float TileHeight { get; set; }

On an isometric map, how tall each tile’s picture is drawn, in tile widths: 0.5 (the default) is a flat diamond, and 1 a square picture standing on its diamond, a block or wall as tall again (a tileset of 32 by 32 pixel tiles with a 32 by 16 diamond at the bottom). A picture stands on its cell’s bottom corner. A layer’s is its map’s.

public float TileSize { get; set; }

Width and height of one cell in world units, before the transform’s scale; on an isometric map, a diamond’s width (it’s half as tall), and on a hex map a hexagon’s width and height (its rows are three quarters of it apart). A layer’s is its map’s (see LayerOf).

public IReadOnlyList<string> Tiles { get; set; }

The grid, top row first; see the remarks on Tilemap for the format.

public Color Tint { get; set; }

Multiplies the tiles’ colors: white leaves them as they are, and a lower alpha fades them.

public IReadOnlyList<string> Turns { get; set; }

How each cell’s tile is turned or flipped, a line a row lined up with Tiles: a character per cell, . as drawn, 1, 2 and 3 turned a quarter, a half and three quarters clockwise, and 4 to 7 the same mirrored left to right first (4 just mirrored, 6 upside down; see TileTurn). Animated tiles turn with their cells, and slope tiles collide turned. Set it with SetTile or by turning the brush while painting (Z, X); empty, as it is for a map that turns nothing, draws every tile as it is. Quarter turns suit square maps; flips and half turns suit every shape.

public int Width { get; }

Number of columns: the length of the longest row.

public IEnumerable<ValueTuple<int, int, int>> BackToFront()

Every non-empty cell and its tile in the order they’re drawn: row by row, or on an isometric map diagonal by diagonal from the top corner (see DepthOf), so what’s in front comes after what’s behind.

public ValueTuple<Vector2, Vector2> Bounds()

The box the grid’s tiles cover in the entity’s own space, (min, max): all its cells, and on an isometric map the pictures standing up from them (see TileHeight). One cell for an empty map.

public ValueTuple<int, int> CellAt(Vector2 local)

The cell containing local, a point in the entity’s own space.

public Vector2 CellCenter(int x, int y)

The center of cell (x, y) in the entity’s own space (on a hex map, odd rows’ are half a tile further right).

public IEnumerable<Matrix4x4> CellShape(int x, int y, float scale = 1f)

What covers cell (x, y)’s shape in the entity’s own space, shrunk to scale of it round its middle, as transforms of the unit quad (-0.5 to 0.5 each way): a square, an isometric map’s diamond, or a hex map’s hexagon in three parts that don’t overlap. For lighting cells up, as Tactics shows where a unit can go.

public IEnumerable<ValueTuple<int, int, int>> Cells()

Every non-empty cell and its tile, row by row.

public IEnumerable<ValueTuple<int, int, int>> CellsAtDepth(int depth)

The non-empty cells of one row of BackToFront (see DepthOf), in the order they’re drawn.

public static char CharOf(int tile)

The character for tile; -1 (or anything from MaxTiles on) gives Empty. Tiles 0–61 are 0-9, a-z and A-Z, 62–459 the accented letters from À to ɏ (skipping × and ÷), and 460 on the CJK characters from 一, so a cell is always one character.

public static char CharOf(TileTurn turn)

The character for turn in Turns: . for none, 1 to 7 for the rest.

public IReadOnlyList<ValueTuple<int, int, int, bool>> CollisionRuns()

Horizontal runs of tiles that collide, split where solid tiles meet one-way tiles. Background tiles are left out. Cached until the tiles or tile settings change.

public Vector3 CornerNearest(Vector3 world)

The corner of its cells nearest world, a point in the world: where a map lined up with this one (see LinesUpWith) can start.

public int DepthOf(int x, int y)

Which row cell (x, y) is drawn in, back (0) to front: its row on a square or hex map, its diagonal (x + y) on an isometric one. A map that sorts by Y sorts each of these among the sprites.

public float DepthY(int depth)

The world Y a row of tiles (see DepthOf) sorts by when the map sorts by Y: the middle of its cells, where a character standing among them has its feet.

public void FillRect(int fromX, int fromY, int toX, int toY, int tile)

Sets every cell in the rectangle between cells (fromX, fromY) and (toX, toY), corners included and either way round; -1 clears them. The grid grows to fit, like SetTile.

public void FillRect(int fromX, int fromY, int toX, int toY, int tile, TileTurn turn)

Sets every cell in the rectangle between two corner cells to tile turned by turn, as FillRect does unturned.

public IReadOnlyList<ValueTuple<int, int>> FillRegion(int x, int y)

The cells a paint-bucket fill from cell (x, y) reaches: that cell and every cell joined to it side by side (see Neighbors) through cells holding the same tile (or all empty), within the grid’s Width and Height. Empty when the cell is outside the grid.

public IReadOnlyList<string> Filled(int x, int y, int tile)

The grid with the area a fill from cell (x, y) reaches (see FillRegion) set to tile, leaving this map alone. Unchanged when the cell is outside the grid or already holds that tile.

public ValueTuple<IReadOnlyList<string>, IReadOnlyList<string>> Filled(int x, int y, int tile, TileTurn turn)

The grid and its Turns with the area a fill from cell (x, y) reaches set to tile turned by turn, leaving this map alone, as Filled says. Unchanged when the cell already holds that tile turned that way.

public IReadOnlyList<ValueTuple<int, int>> FindCellPath(ValueTuple<int, int> start, ValueTuple<int, int> goal, bool diagonal = true)

A* over cells: the cells to step through after start, up to goal, keeping only the cells where the direction changes (and the goal). Empty if unreachable; just the goal if already there.

public IReadOnlyList<Vector2> FindPath(Vector2 from, Vector2 to, bool diagonal = true)

The shortest way from from to to (world positions) that doesn’t go through walls, as world positions to head for one after another, ending at the center of to’s cell. Straight stretches are one waypoint. Empty if there’s no way there, or either end is inside a wall. Moves diagonally unless diagonal is false, but never cuts a wall’s corner. The search stays within the map, plus one cell all round so paths can go around its outside.

public IReadOnlyList<Vector2> FindPath(Vector3 from, Vector3 to, bool diagonal = true)

A world position, given as a Vector3; its Z is ignored.

public IReadOnlyList<ValueTuple<int, int>>? FindSteps(ValueTuple<int, int> start, IEnumerable<ValueTuple<int, int>> goals, IEnumerable<ValueTuple<int, int>>? avoid = null, string? blockingLayers = null, bool diagonal = false)

The shortest way in cells from start to the nearest of goals, as a grid mover on this map would walk it (see WalkTo): every cell to step into after the start, ending at the goal it reaches; empty if the start is a goal; null if no goal can be reached. Cells are column then row, row 0 at the top. It stays on the map and goes round solid tiles (of every tilemap in the scene), the cells in avoid (where others stand, say), and the solid colliders on blockingLayers, e.g. “Obstacle” (grid movers on them block the cell they’re in). Four ways, or eight with diagonal, never cutting a corner; six on a hex map. For planning without walking.

public void FloodFill(int x, int y, int tile)

Fills the area around cell (x, y) with tile, like a paint bucket (see FillRegion for what it reaches); -1 clears it.

public void FloodFill(int x, int y, int tile, TileTurn turn)

Fills the area around cell (x, y) with tile turned by turn, as FloodFill does unturned.

public int GetTile(int x, int y)

The tile at column x, row y (0, 0 is top left), or -1 if empty or outside.

public TileTurn GetTurn(int x, int y)

How the tile in cell (x, y) is turned (see Turns): None for one drawn as it is, an empty cell, or one outside the grid.

public Vector2 GridAt(Vector2 local)

How many columns (X) and rows (Y) along the grid local, a point in the entity’s own space, is from its start: the other way from PointAt, so on a square or isometric map cell x, y holds the points from (x, y) to (x + 1, y + 1). On an isometric map that turns the diamonds into squares, where walls run straight along the grid; on a hex map rows are three quarters of a tile apart, and odd rows’ shift isn’t counted.

public string? ImageOf(int tile)

The image tile is cut from (see TilesetOf); null draws it as a square of Tint.

public static bool InSet(bool[] set, int tile)

True if tile is in a set read by ParseTileSet.

public bool IsBackground(int tile)

True if tile is listed in BackgroundTiles.

public bool IsBlocked(int x, int y)

True if cell (x, y) is a wall for FindPath: a solid tile on a Solid map. Empty, background, ladder and one-way tiles can be walked through.

public bool IsLadder(int tile)

True if tile is listed in LadderTiles.

public bool IsOneWay(int tile)

True if tile is listed in OneWayTiles (and not as a background tile).

public float? LadderAt(Vector2 world)

The ladder cell containing world, a world position, as the world X of its middle; null if there’s no ladder tile there.

public IReadOnlyList<Tilemap> Layers()

The layers this map belongs to: its BaseMap first, then every layer of it (and layers of those), in the order of the Hierarchy. Just this map when it has no layers.

public bool LineUp()

Lines its cells up with GridMap’s: takes its tile size and shape, and moves its entity to the nearest place whose top left is a corner of that map’s cells. Returns whether anything changed. Allocates nothing.

public static void LineUpAll(Scene scene)

Lines up every tilemap in scene that lines up with another (see LineUp).

public static void LineUpWithLevel(Entity entity, Scene scene, Vector3 at)

Lines a new board’s map (on entity, not yet in scene) up with the scene’s level: its first square tilemap that’s no layer and no board, if it has one (boards draw square cells). Its tile size becomes the level’s, and at, the world point it would start at, moves to the nearest corner of the level’s cells.

public Vector3 LinedUp(Vector3 position)

The position (in its entity’s parent’s space) nearest position that puts its top left on a corner of GridMap’s cells; position itself when it lines up with nothing.

public IEnumerable<ValueTuple<int, int>> Neighbors(int x, int y)

The cells that share a side with cell (x, y), inside the grid or not: the four above, right, below and left of it, or on a hex map the six round it, clockwise from the top left (the two in the row above, which for an odd row are its own column and the one to its right and for an even row the one to its left and its own; the one to its right; the two below, the same way; the one to its left).

public Vector2[] OutlineOf(int left, int top, int right, int bottom)

The outline of the cells from (left, top) to (right, bottom), corners included, in the entity’s own space, from the grid’s start (top left, or top on an isometric map) round the way the columns run. A square on a square map, a diamond on an isometric one, and on a hex map the hexagons’ edges all the way round: a single cell’s six corners from its top left one.

public ValueTuple<IReadOnlyList<string>, int, int> Painted(int x, int y, int tile)

The grid with one cell changed, leaving this map alone. Cells left of or above the grid are allowed: the grid grows that way and ShiftX/ShiftY say how many columns and rows were added before the old ones, so the caller can move the entity back by that many cells to keep existing tiles in place.

public ValueTuple<IReadOnlyList<string>, int, int> PaintedRect(int fromX, int fromY, int toX, int toY, int tile)

The grid with every cell in the rectangle between two corner cells (either way round) set to tile, leaving this map alone. Like Painted, the grid grows left and up to take in a rectangle reaching past those edges (a hex map up an even number of rows, so its odd rows stay the shifted ones); clearing (-1) only reaches the cells already in the grid.

public ValueTuple<IReadOnlyList<string>, IReadOnlyList<string>, int, int> PaintedRect(int fromX, int fromY, int toX, int toY, int tile, TileTurn turn)

The grid and its Turns with every cell in the rectangle between two corner cells set to tile turned by turn (or cleared, with -1), leaving this map alone. The grid grows left and up as PaintedRect says, and the turns move with it.

public static IReadOnlyList<TileAnimation> ParseAnimations(string? text)

Reads AnimatedTiles: “first-last @fps” runs separated by commas. Anything else is skipped.

public static IReadOnlyList<int> ParseAutoTiles(string? text)

Reads AutoTiles: the first tile of each run of 16, as “16-31” or “16”, separated by commas. Runs that aren’t 16 long, or run past the highest tile number, are skipped.

public static bool[] ParseTileSet(string? text)

Reads a list of tile numbers and ranges like “3, 7-9”, as flags by tile number up to the highest one listed (look tiles up with InSet). Anything that isn’t a number or range is ignored, as are tiles outside 0 to MaxTiles - 1.

public ValueTuple<Vector2, Vector2> PictureOf(int x, int y)

Where cell (x, y)’s picture is drawn in the entity’s own space: its middle and size. On a square map that’s the cell, and on a hex map a tile-sized square round the hexagon; on an isometric one it’s a tile wide and TileHeight tall, standing on the cell’s bottom corner.

public Color PlaceholderColorOf(int tile)

The color tile tile draws in on a map with no image, before Tint: its color of the BaseMap’s PlaceholderColors (going round them), or white when there are none or the map has an image.

public Vector2 PointAt(float x, float y)

The point x columns and y rows along the grid from its start (the entity’s position), in the entity’s own space: whole numbers are the corners between cells, so (x + 0.5, y + 0.5) is cell x, y’s center. On a square map that’s right and down; on an isometric one, down to the right and down to the left. On a hex map it’s right and down by rows three quarters of a tile apart, not counting odd rows’ shift (use CellCenter for a hexagon’s middle).

public IReadOnlyList<ValueTuple<int, int, int>> Runs()

Horizontal runs of non-empty cells, for collision. Cached until the tiles change.

public void SetTile(int x, int y, int tile)

Sets the tile at column x, row y, as drawn (not turned: see SetTile); -1 clears it. The grid grows to fit, and empty space at the right and bottom is trimmed.

public void SetTile(int x, int y, int tile, TileTurn turn)

Sets the tile at column x, row y turned or flipped by turn, e.g. SetTile(4, 2, 7, TileTurn.Turn90) for a belt drawn pointing right that should point down; -1 clears it. The grid grows to fit, like SetTile.

public void SetTurn(int x, int y, TileTurn turn)

Turns the tile already in cell (x, y) to turn, keeping the tile: SetTurn(x, y, GetTurn(x, y).Turned()) turns it a quarter further clockwise. An empty cell stays empty.

public override string? SetupNote()

On a layer, which map it’s a layer of; and a map sorting by Y in a scene whose camera doesn’t.

public int ShownTile(int tile)

The tile to draw for a cell painted with tile at AnimationTime: itself, or the frame its AnimatedTiles run has reached.

public int ShownTile(int x, int y, int tile)

The tile to draw for cell (x, y) painted with tile: the piece of its AutoTiles run that joins its neighbors, then animated by ShownTile.

public int SlopeAt(int x, int y)

The slope cell (x, y) makes, its Turns taken in: 1 rising to the right, -1 falling, 0 for none. A slope tile flipped left to right falls where it rose, and one turned a quarter can be the other slope; one turned so its solid corner is at the top is a solid block instead.

public IReadOnlyList<ValueTuple<int, int, bool>> SlopeCells()

The cells painted with slope tiles and which way each rises. Cached until the tiles or tile settings change.

public int SlopeOf(int tile)

1 for a tile that’s a slope rising to the right, -1 for one falling to the right, 0 for anything else.

public IEnumerable<ValueTuple<int, float>> SortParts()

With SortByY, each row of tiles (see DepthOf) and the height it sorts at.

public IEnumerable<SpriteDraw> TileDraws(Box? cull = default)

One quad per tile (in the world), back to front, of those in cull (and a cell around it) or all of them. Without an image, tiles are solid shapes of the tint (times their PlaceholderColors): squares, diamonds or hexagons (three quads each), each over a slightly bigger one of its grid line’s color with PlaceholderGrid.

public static int TileOf(char c)

The tile a character stands for, or -1 for empty (and anything unrecognized); see CharOf.

public Vector4 TileRect(int tile)

The part of its tileset’s image showing tile (see TilesetOf): (u, v, width, height) in 0–1 image space.

public Tileset TilesetOf(int tile)

The tileset tile comes from: the last one starting at or before it. A number past a tileset’s own tiles (and before the next one’s) wraps round them, as numbers past the end of a single tileset always have.

public IReadOnlyList<Tileset> Tilesets()

The map’s tilesets in order: the first (Image, Columns and Rows; with no image and PlaceholderColors, a tile for each color in one row) from tile 0, then each line of MoreTilesets from the next hundred on. Blank lines are skipped. Cached until those change.

public Color TintDrawn(StepBlend blend)

Its Tint in a frame drawn as blend says: part way through a fade between steps (for a component drawing its tiles itself).

public static TileTurn TurnOf(char c)

The turn a character of Turns stands for: 1 to 7, anything else as drawn.

public override bool Uses(string property)

On a layer, the tileset settings it takes from its map aren’t its own, so the Inspector hides them; TileHeight only means something on an isometric map, and the placeholder settings only on a map with no Image.

public const char Empty

The character for an empty cell.

public const int MaxTiles

One more than the highest tile number a cell can hold.

public const int TilesetStep

Each tileset after the first starts at the next multiple of this after the tiles before it.