Grids & Tilemaps
Snapping a world to a lattice
Grids turn up everywhere: tile-based levels, building placement, inventories, tactics movement, match-3 boards. Unity ships a Grid component, but the useful skill is converting between world space and cell coordinates — and that is the same maths whether or not you use Unity's version.
Unity's Grid and Tilemap
A Grid component defines cell size and layout. A Tilemap child stores which tile sits in each cell, and a TilemapRenderer draws them in one batch, which is why tilemaps stay cheap even at thousands of tiles.
| Cell Layout | Used for | Note |
|---|---|---|
Rectangle | Platformers, top-down, most things | The default. |
Isometric | Diamond-shaped tiles | Needs sort order set on the Tilemap Renderer. |
Isometric Z as Y | Isometric with height | Z drives both height and sorting. |
Hexagonal | Strategy boards | Flat-top and point-top variants. |
World to cell, and back
This pair of conversions is most of what you need. WorldToCell tells you which cell a point falls in; GetCellCenterWorld gives you the exact position to place something so it sits centred.
using UnityEngine;
using UnityEngine.Tilemaps;
public class GridPlacer : MonoBehaviour
{
[SerializeField] private Grid grid;
[SerializeField] private Tilemap tilemap;
[SerializeField] private GameObject buildingPrefab;
private void Update()
{
if (!Input.GetMouseButtonDown(0)) return;
Vector3 mouse = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector3Int cell = grid.WorldToCell(mouse);
// Only build on a tile that actually exists
if (!tilemap.HasTile(cell)) return;
// Snap to the centre of the cell, not the mouse position
Vector3 snapped = grid.GetCellCenterWorld(cell);
Instantiate(buildingPrefab, snapped, Quaternion.identity);
}
}ScreenToWorldPoint with a perspective camera needs a Z distance or everything lands on the camera plane. For 2D use an orthographic camera, or raycast onto a ground plane in 3D.Rolling your own grid
For inventories, tactics maps, and puzzle boards you usually want a plain data grid rather than a Tilemap. A 2D array plus two conversion methods gets you there, and it stays testable because none of it touches the scene.
public class Grid2D<T>
{
private readonly T[,] cells;
private readonly float cellSize;
private readonly Vector3 origin;
public int Width { get; }
public int Height { get; }
public Grid2D(int width, int height, float cellSize, Vector3 origin)
{
Width = width; Height = height;
this.cellSize = cellSize;
this.origin = origin;
cells = new T[width, height];
}
public bool InBounds(int x, int y) =>
x >= 0 && y >= 0 && x < Width && y < Height;
public T Get(int x, int y) => InBounds(x, y) ? cells[x, y] : default;
public void Set(int x, int y, T value)
{
if (InBounds(x, y)) cells[x, y] = value;
}
public Vector3 CellToWorld(int x, int y) =>
origin + new Vector3(x * cellSize, y * cellSize) + Vector3.one * (cellSize * 0.5f);
public Vector2Int WorldToCell(Vector3 world)
{
Vector3 local = world - origin;
return new Vector2Int(
Mathf.FloorToInt(local.x / cellSize),
Mathf.FloorToInt(local.y / cellSize)
);
}
}FloorToInt, not RoundToInt. Rounding puts the cell boundary in the middle of the cell, which produces an off-by-one that only shows up on one side of the grid — a genuinely annoying bug to chase.Bounds checking is not optional
Every grid bug eventually traces back to a missing bounds check. Put it in one place, as above, and call it from everywhere rather than scattering if (x >= 0 && ...) through your code.
Neighbours
Pathfinding, flood fill, and match detection all need neighbours. Decide early whether diagonals count, because it changes level design more than it changes code.
private static readonly Vector2Int[] Cardinal = {
new(0, 1), new(1, 0), new(0, -1), new(-1, 0)
};
public IEnumerable<Vector2Int> Neighbours(Vector2Int c)
{
foreach (var d in Cardinal)
{
Vector2Int n = c + d;
if (InBounds(n.x, n.y)) yield return n;
}
}Seeing the grid while you work
A grid you cannot see is a grid you cannot debug. OnDrawGizmos costs nothing in a build and saves hours.
private void OnDrawGizmos()
{
Gizmos.color = new Color(1f, 1f, 1f, 0.2f);
for (int x = 0; x <= width; x++)
Gizmos.DrawLine(CellToCorner(x, 0), CellToCorner(x, height));
for (int y = 0; y <= height; y++)
Gizmos.DrawLine(CellToCorner(0, y), CellToCorner(width, y));
}Performance
- A
Tilemapbatches its tiles — thousands of tiles cost roughly one draw call. - One GameObject per cell does not scale. A 100×100 grid of objects is 10,000 transforms.
- Store data in arrays, and only create GameObjects for cells that need to be interactive.
TilemapCollider2DplusCompositeCollider2Dmerges tile colliders into one shape, which is far cheaper.
What to take away
- The core skill is converting between world position and cell coordinates.
- Use
FloorToInt, neverRoundToInt, for that conversion. - Keep bounds checking in one method.
- Use a Tilemap for drawn levels, a plain array for game logic.
- Draw gizmos so you can see what the maths is doing.
- Composite tile colliders instead of one collider per tile.