Unity Grid Building System: Snap, Preview and Place Objects

Illustration: a glowing grid with a translucent ghost building snapping into place

City builders, tower defense, farming games and base builders all share one mechanic: pick a building, see a ghost preview snap to a grid under the mouse, turn red where it can’t go, and place it with a click. This guide builds a Unity grid building and placement system from scratch: a grid data structure, mouse-to-cell conversion, multi-cell buildings with rotation, a valid/invalid preview and removal, all in clean code you can extend.

How the System Fits Together

  • BuildGrid stores which cells are occupied, and converts between world positions and cell coordinates.
  • BuildingData (ScriptableObject) describes a building: prefab, footprint size and cost.
  • BuildingPlacer handles input, the preview ghost, validity checks and placing.

The grid is data, not a GameObject per cell. A 100×100 grid as GameObjects is 10,000 objects; as an array it’s nothing.

The Grid Data

using UnityEngine;

public class BuildGrid : MonoBehaviour
{
    [SerializeField] int width = 50;
    [SerializeField] int height = 50;
    [SerializeField] float cellSize = 1f;

    GameObject[,] cells;

    public float CellSize => cellSize;

    void Awake() => cells = new GameObject[width, height];

    public Vector2Int WorldToCell(Vector3 world)
    {
        Vector3 local = world - transform.position;
        return new Vector2Int(Mathf.FloorToInt(local.x / cellSize), Mathf.FloorToInt(local.z / cellSize));
    }

    public Vector3 CellToWorld(Vector2Int cell)
    {
        return transform.position + new Vector3(cell.x * cellSize, 0f, cell.y * cellSize);
    }

    public bool InBounds(Vector2Int c) => c.x >= 0 && c.y >= 0 && c.x < width && c.y < height;

    public bool IsAreaFree(Vector2Int origin, Vector2Int size)
    {
        for (int x = 0; x < size.x; x++)
        for (int y = 0; y < size.y; y++)
        {
            var c = new Vector2Int(origin.x + x, origin.y + y);
            if (!InBounds(c) || cells[c.x, c.y] != null) return false;
        }
        return true;
    }

    public void Occupy(Vector2Int origin, Vector2Int size, GameObject building)
    {
        for (int x = 0; x < size.x; x++)
        for (int y = 0; y < size.y; y++)
            cells[origin.x + x, origin.y + y] = building;
    }

    public GameObject GetAt(Vector2Int c) => InBounds(c) ? cells[c.x, c.y] : null;

    public void Clear(GameObject building)
    {
        for (int x = 0; x < width; x++)
        for (int y = 0; y < height; y++)
            if (cells[x, y] == building) cells[x, y] = null;
    }

    void OnDrawGizmos()
    {
        Gizmos.color = new Color(1f, 1f, 1f, 0.15f);
        for (int x = 0; x <= width; x++)
            Gizmos.DrawLine(transform.position + new Vector3(x * cellSize, 0, 0),
                            transform.position + new Vector3(x * cellSize, 0, height * cellSize));
        for (int y = 0; y <= height; y++)
            Gizmos.DrawLine(transform.position + new Vector3(0, 0, y * cellSize),
                            transform.position + new Vector3(width * cellSize, 0, y * cellSize));
    }
}

The grid lies on the XZ plane starting at the object’s position. Each cell stores a reference to the building that occupies it, so multi-cell buildings fill several cells with the same reference and removal can clear them all. The gizmo draws the grid in the Scene view while you set it up.

Building Definitions

using UnityEngine;

[CreateAssetMenu(menuName = "Building/Building Data")]
public class BuildingData : ScriptableObject
{
    public string displayName;
    public GameObject prefab;
    public Vector2Int size = Vector2Int.one;
    public int cost = 10;
}

Create one asset per building type. Build each prefab with its pivot at the corner of its footprint (the minimum X and Z), and its visuals extending into positive X and Z, so it lines up with the cell origin. If your models are centered, put them under an empty parent with the pivot at the corner. The ScriptableObjects guide covers this data pattern.

Mouse to Grid Cell

Raycast from the mouse onto the ground. A mathematical plane is simplest and doesn’t need colliders:

bool TryGetMouseCell(out Vector2Int cell)
{
    cell = default;
    Ray ray = cam.ScreenPointToRay(Mouse.current.position.ReadValue());
    var plane = new Plane(Vector3.up, grid.transform.position);
    if (!plane.Raycast(ray, out float dist)) return false;
    cell = grid.WorldToCell(ray.GetPoint(dist));
    return true;
}

For uneven terrain, use Physics.Raycast against a ground layer instead. The raycast tutorial explains layer masks.

The Placement Controller

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;

public class BuildingPlacer : MonoBehaviour
{
    [SerializeField] BuildGrid grid;
    [SerializeField] Camera cam;
    [SerializeField] Material validMat, invalidMat;

    BuildingData current;
    GameObject ghost;
    int rotation; // 0..3, quarter turns

    public void Select(BuildingData data)
    {
        Cancel();
        current = data;
        ghost = Instantiate(data.prefab);
        foreach (var c in ghost.GetComponentsInChildren<Collider>()) c.enabled = false;
    }

    public void Cancel()
    {
        if (ghost) Destroy(ghost);
        current = null;
        rotation = 0;
    }

    Vector2Int RotatedSize => rotation % 2 == 0 ? current.size : new Vector2Int(current.size.y, current.size.x);

    void Update()
    {
        if (current == null)
        {
            if (Mouse.current.rightButton.wasPressedThisFrame) TryRemove();
            return;
        }

        if (Keyboard.current.rKey.wasPressedThisFrame) rotation = (rotation + 1) % 4;
        if (Keyboard.current.escapeKey.wasPressedThisFrame || Mouse.current.rightButton.wasPressedThisFrame) { Cancel(); return; }

        if (!TryGetMouseCell(out var cell)) { ghost.SetActive(false); return; }
        ghost.SetActive(true);

        Vector2Int size = RotatedSize;
        bool valid = grid.IsAreaFree(cell, size);

        PositionGhost(ghost.transform, cell, size);
        SetGhostMaterial(valid ? validMat : invalidMat);

        bool overUI = EventSystem.current != null && EventSystem.current.IsPointerOverGameObject();
        if (valid && !overUI && Mouse.current.leftButton.wasPressedThisFrame)
        {
            var placed = Instantiate(current.prefab);
            PositionGhost(placed.transform, cell, size);
            grid.Occupy(cell, size, placed);
        }
    }

    void PositionGhost(Transform t, Vector2Int cell, Vector2Int size)
    {
        // rotate around the footprint center so the building stays within its cells
        float cs = grid.CellSize;
        Vector3 center = grid.CellToWorld(cell) + new Vector3(size.x * cs, 0f, size.y * cs) * 0.5f;
        Quaternion rot = Quaternion.Euler(0f, rotation * 90f, 0f);
        Vector3 halfOriginal = new Vector3(current.size.x * cs, 0f, current.size.y * cs) * 0.5f;
        t.SetPositionAndRotation(center - rot * halfOriginal, rot);
    }

    void SetGhostMaterial(Material m)
    {
        foreach (var r in ghost.GetComponentsInChildren<Renderer>())
        {
            var mats = r.sharedMaterials;
            for (int i = 0; i < mats.Length; i++) mats[i] = m;
            r.sharedMaterials = mats;
        }
    }

    void TryRemove()
    {
        if (!TryGetMouseCell(out var cell)) return;
        var building = grid.GetAt(cell);
        if (building == null) return;
        grid.Clear(building);
        Destroy(building);
    }

    bool TryGetMouseCell(out Vector2Int cell)
    {
        cell = default;
        Ray ray = cam.ScreenPointToRay(Mouse.current.position.ReadValue());
        var plane = new Plane(Vector3.up, grid.transform.position);
        if (!plane.Raycast(ray, out float dist)) return false;
        cell = grid.WorldToCell(ray.GetPoint(dist));
        return true;
    }
}

Rotation swaps the footprint’s width and depth on odd quarter turns. PositionGhost rotates the building around the center of its footprint and offsets it so the corner-pivot model still covers exactly the right cells. The UI check stops clicks on your build menu from also placing a building; the UI click-through guide explains it.

Ghost Preview Materials

Create two transparent materials: green at about 40% alpha for valid, red for invalid. In URP, use the Lit or Unlit shader with Surface Type set to Transparent. Swapping every renderer’s materials is simple and works with any prefab. Disable the ghost’s colliders (done in Select) so the ghost never blocks raycasts or triggers anything, and disable scripts on it if the prefab has gameplay behaviour.

Extending It

  • Cost: check and deduct current.cost from your currency before placing.
  • Terrain rules: add a second array of blocked cells (water, cliffs) that IsAreaFree also checks.
  • Drag to place: on mouse held, place along the line between the press cell and the current cell, for walls and roads.
  • NavMesh: add a NavMeshObstacle with Carve to buildings so units path around them.
  • Saving: save a list of (building ID, cell, rotation) and rebuild on load. The save system guide shows the JSON approach.

🔧 Building kits

Modular art packs and grid tools speed up building games. See our recommended Asset Store tools.

Troubleshooting

Buildings are offset by half a cell

The prefab’s pivot is centered. Move the model under a parent so the pivot is at the footprint corner.

Placement happens when clicking a UI button

Add the EventSystem.IsPointerOverGameObject check before placing.

Ghost blocks the mouse raycast

The ghost’s colliders are enabled. Disable them when creating it, or raycast against a plane.

Rotated buildings overlap neighbours

The footprint size isn’t swapped on 90 and 270 degrees. Use the rotated size for checks and occupation.

Ghost changes the real prefab’s materials

You edited the prefab asset instead of the instance. Only change materials on the instantiated ghost.

FAQ

Should I use Unity’s Grid component?

Grid and Tilemap are great for 2D tile games. For 3D building, a simple array is easier to control.

How big can the grid be?

A 2D array handles hundreds of cells per side easily. Only the placed buildings are GameObjects.

Can buildings have non-rectangular footprints?

Yes. Store a list of cell offsets instead of a size, and rotate each offset.

Does this work in 2D?

Yes. Use X and Y instead of X and Z and raycast onto the XY plane.

How do I show the grid in game?

Use a tiling grid texture on a ground plane, or a shader that draws lines based on world position.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *