Topic 11 of 15 13 min intermediate

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 LayoutUsed forNote
RectanglePlatformers, top-down, most thingsThe default.
IsometricDiamond-shaped tilesNeeds sort order set on the Tilemap Renderer.
Isometric Z as YIsometric with heightZ drives both height and sorting.
HexagonalStrategy boardsFlat-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.

GridPlacer.cs
C#
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.

Grid2D.cs
C#
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)
        );
    }
}
Note 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.

C#
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;
    }
}
Once you have neighbours and bounds, A* is a short step away. The A* Grid Pathfinding script builds directly on this shape.

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.

C#
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 Tilemap batches 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.
  • TilemapCollider2D plus CompositeCollider2D merges 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, never RoundToInt, 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.