3D Grid System

In this Entry I decided to make a Grid System. I’ve been working on a Personal Project of mine where I need to use a 3D Grid system and I decided instead of dealing with countless bugs and errors I just wanted to make the system in a clean Unity Project. So here it is 🙂

So What does this Grid System do, well for what it is it can do a lot and can be altered largely. This is what I mean – This system allows Unity using it’s built in Gizmos() Function to create a Grid with X amount of Cells that we can set. The System works with Building GameObjects. These GameObjects can take up different sizes on the grid and occupy different cells. We can also detect building stacking too. Nothing is stopping the building stacking but the logic is all here. The functions being used can be called in different places like through UI. 

If you use UI to place a building down it can be prevented with how we detect where buildings are on the grid. Take a look at the script and it’ll be clearer.

Building.cs

using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

public class Building : MonoBehaviour
{
    [Tooltip("Direct Reference to the Grid Manager Class")]
    private Grid_Manager gridManager;   // Reference to our Grid Manager CS file
    [Header("Building Grid Size")]
    [Tooltip("Cell Building Scale On X Axis")]
    public int buildingSizeX = 2; // Set your desired size
    [Tooltip("Cell Building Scale On Z Axis")]
    public int buildingSizeZ = 2; // Set your desired size

    private int lastOccupiedX = -1; // Last position in the Grid On X
    private int lastOccupiedZ = -1; // Last Position in the Grid On Z

    private int currentlyOccupiedX = -1;    // Current Position in Grid on X
    private int currentlyOccupiedZ = -1;    // Current Position in Grid on Z

    private string occupyingBuildingName = "";  // Current Building GameObject String name inside a cell
    private bool hasOccupiedCell = false;   // Bool to check if a cell is occupied 

    private bool isBeingDragged = false;    // Boolean controlling if mouse is down and dragging a building

    // New variables to keep track of the last occupied cell for each building
    private Dictionary<string, Vector2Int> buildingLastOccupiedCells = new Dictionary<string, Vector2Int>();

    void Awake()
    {
        gridManager = FindObjectOfType<Grid_Manager>(); // Find the Grid Manager inside the scene
        if (gridManager == null)    // if we do not find the grid manager
        {
            Debug.LogError("Grid Manager not found. Make sure it is present in the scene.");    // Log Error since nothing works without it 
        }

        if (buildingSizeX >= 5 || buildingSizeZ >= 5)
            Debug.LogWarning("This Script Does not support larger buildings well"
                + " Please Reduce the size if you see any abnormalities");

    }

    void Start()
    {
        // when grid manager is aquired
        if (gridManager != null)
        {
            PlaceBuilding();    // Run PLace Building Function
        }
    }

    void Update()
    {
        // When true
        if (isBeingDragged)
        {
            // Update the building position during drag
            UpdateGridOccupancy();
        }
    }


    void PlaceBuilding()
    {
        UpdateGridOccupancy(); // Initial update
    }

    void SnapToGrid(int x, int z)
    {
        // Calculate the center position of the grid cells
        float centerX = (x + 0.5f * (buildingSizeX - 1)) * gridManager.cellSize;
        float centerZ = (z + 0.5f * (buildingSizeZ - 1)) * gridManager.cellSize;

        // Set the new position while maintaining the Y position
        transform.position = new Vector3(centerX, transform.position.y, centerZ);
    }

    private void OnMouseDrag()
    {
        // Get the mouse position in world coordinates
        Vector3 mousePosition = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.transform.position.y));

        // Calculate the half size of the building on the X and Z axes
        float halfSizeX = buildingSizeX * 0.5f * gridManager.cellSize;
        float halfSizeZ = buildingSizeZ * 0.5f * gridManager.cellSize;

        // Calculate the snapped position based on the closest grid cell
        float snappedX = Mathf.Floor((mousePosition.x - halfSizeX) / gridManager.cellSize) * gridManager.cellSize + halfSizeX;
        float snappedZ = Mathf.Floor((mousePosition.z - halfSizeZ) / gridManager.cellSize) * gridManager.cellSize + halfSizeZ;

        // Set the new position while maintaining the Y position
        transform.position = new Vector3(snappedX, transform.position.y, snappedZ);

        // Debug logs for diagnosis
        //Debug.Log($"Mouse Position: {mousePosition}, Snapped Position: {snappedX}, {snappedZ}, Actual Position: {transform.position}");
    }

    void OnMouseDown()
    {
        isBeingDragged = true;
    }

    void OnMouseUp()
    {
        isBeingDragged = false;
    }

    void UpdateGridOccupancy()
    {
        // grid cell index on the Axis where the center of the building is located.
        int x = Mathf.FloorToInt((transform.position.x - gridManager.transform.position.x) / gridManager.cellSize);
        int z = Mathf.FloorToInt((transform.position.z - gridManager.transform.position.z) / gridManager.cellSize);

        // Debug log for actual position
        Debug.Log($"Actual Position: {transform.position}");

        // Check if the building is within the grid
        if (x >= 0 && x < gridManager.gridSize - buildingSizeX + 1 &&
            z >= 0 && z < gridManager.gridSize - buildingSizeZ + 1)
        {

            SnapToGrid(x, z);
            // Check if the cell is already occupied by another building
            if (gridManager.IsCellOccupiedByOther(x, z, buildingSizeX, buildingSizeZ, gameObject.name))
            {
                // Cell is already occupied by another building, handle accordingly (e.g., print a message)
                Debug.LogWarning($"Cannot place {gameObject.name} in occupied cell ({x}, {z}). Already occupied by another building.");
                // When a Building moves towards an occupied Cell it refreshes the grid since we are no longer in the old cell
                gridManager.ReleaseCells(lastOccupiedX, lastOccupiedZ, buildingSizeX, buildingSizeZ, gameObject.name);

                return; // Skip updating last occupied values if there is a conflict
            }

            // Rest of the logic for handling placement inside the grid
            if (!hasOccupiedCell)
            {
                occupyingBuildingName = gameObject.name;
                Debug.Log($"Cell ({x}, {z}) is now occupied by {occupyingBuildingName}.");
                hasOccupiedCell = true;
            }
            else if (occupyingBuildingName == gameObject.name)
            {
                Debug.Log($"Cell ({x}, {z}) is still occupied by {gameObject.name}.");
            }
            else
            {
                // Check if the cell was previously occupied by another building with a different name
                if (lastOccupiedX != -1 && lastOccupiedZ != -1 &&
                    (lastOccupiedX == x && lastOccupiedZ == z))
                {
                    // Cell is already occupied by another building, handle accordingly (e.g., print a message)
                    Debug.LogWarning($"Cannot place {gameObject.name} in occupied cell ({x}, {z}). Already occupied by another building.");
                    return; // Skip updating last occupied values if there is a conflict
                }
            }

            // If the building moved to a new cell, update the currently occupied cell for debugging
            currentlyOccupiedX = x;
            currentlyOccupiedZ = z;

            // If the building moved to a new cell, revert the color of the last occupied cells to green
            if (lastOccupiedX != -1 && lastOccupiedZ != -1 && (lastOccupiedX != x || lastOccupiedZ != z))
            {
                gridManager.ReleaseCells(lastOccupiedX, lastOccupiedZ, buildingSizeX, buildingSizeZ, gameObject.name);
            }

            // Update the grid to occupy the current cells
            gridManager.OccupyCells(x, z, buildingSizeX, buildingSizeZ, gameObject.name);

            // Update the last occupied cell for this building
            buildingLastOccupiedCells[gameObject.name] = new Vector2Int(x, z);

            lastOccupiedX = x;
            lastOccupiedZ = z;
        }
        else
        {
            // If the building is outside the grid, revert the color of the last occupied cells to green
            if (lastOccupiedX != -1 && lastOccupiedZ != -1)
            {
                gridManager.ReleaseCells(lastOccupiedX, lastOccupiedZ, buildingSizeX, buildingSizeZ, gameObject.name);
                currentlyOccupiedX = -1;
                currentlyOccupiedZ = -1;
                // If the building is outside the grid, clear its occupancy from the grid
                gridManager.ClearBuildingOccupancy(gameObject.name);
            }

            lastOccupiedX = -1;
            lastOccupiedZ = -1;
        }
    }
}

Grid Manager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Grid_Manager : MonoBehaviour
{
    // Size of the Grid for X and Z axis (10x10)
    public int gridSize = 10;
    public float cellSize = 1f; // Size of each cell inside the grid 
    // States used to manage how a cell can be occupied
    public enum CellState { Empty, Occupied, Reserved }
    // New array to store building names
    public CellState[,] cellStates;
    private string[,] cellBuildingNames; 
    // Positions where each cell is on the grid / in world space 
    private List<Vector3> cellPositions = new List<Vector3>();

    void OnDrawGizmos()
    {
        if (cellStates == null)  // Add null check prevents runtime error
        {
            // CellStates array is not initialized yet, return without drawing Gizmos
            return;
        }
        // Grid Starts Blue
        Gizmos.color = Color.blue;
        // For each grid values on x 
        for (int i = 0; i < gridSize; i++)
        {
            // for each grid value on Z
            for (int j = 0; j < gridSize; j++)
            {
                // Position of each cell in the grid we need to see how big eah cell is since different sized cells take up more space
                Vector3 cellPosition = new Vector3(i * cellSize, 0f, j * cellSize);
                Gizmos.DrawWireCube(transform.position + cellPosition, new Vector3(cellSize, 0.1f, cellSize));  // Make the grid
                // when the cell is occupied
                if (cellStates[i, j] == CellState.Occupied)
                {
                    Gizmos.color = Color.red;   // make grid cells red
                    Gizmos.DrawCube(transform.position + cellPosition, new Vector3(cellSize, 0.1f, cellSize));  // draw cube inside grid which is red
                }

                // Draw a small radius at each cell position
                Gizmos.color = Color.blue;
                float radius = 0.1f;
                Gizmos.DrawWireSphere(cellPosition, radius);
            }
        }
    }

    void CreateGrid()
    {
        cellStates = new CellState[gridSize, gridSize]; // Make new state with cells
        cellBuildingNames = new string[gridSize, gridSize]; // Initialize the new array

        // For each Cell on X
        for (int i = 0; i < gridSize; i++)
        {
            // For each cell on Z
            for (int j = 0; j < gridSize; j++)
            {
                cellStates[i, j] = CellState.Empty; // Initialize the cell state
                cellBuildingNames[i, j] = ""; // Initialize the building name
                cellPositions.Add(new Vector3(i * cellSize, 0f, j * cellSize)); // add cell psotions to List
            }
        }
    }

    void Start()
    {
        CreateGrid();   // create grid new on Start
    }

    /// <summary>
    /// Function to say if we can place a building down 
    /// Can be used to place a building using UI - returns a bool 0(false) or 1(true)
    /// </summary>
    /// <param name="x"></param>
    /// <param name="z"></param>
    /// <param name="sizeX"></param>
    /// <param name="sizeZ"></param>
    /// <param name="buildingName"></param>
    /// <returns></returns>
    public bool CanPlaceBuilding(int x, int z, int sizeX, int sizeZ, string buildingName)
    {
        // Check if any of the cells are already taken by the specified building
        return !IsCellOccupiedByOther(x, z, sizeX, sizeZ, buildingName);
    }

    /// <summary>
    /// Function used to check if the current cell a building could access is occupied. Use Enum to check Cell enum state 
    /// Check to that we do not over stack a cell area (Buildings are not on top of eachother) 
    /// </summary>
    /// <param name="x"></param>
    /// <param name="z"></param>
    /// <param name="sizeX"></param>
    /// <param name="sizeZ"></param>
    /// <param name="buildingName"></param>
    /// <returns></returns>
    public bool IsCellOccupiedByOther(int x, int z, int sizeX, int sizeZ, string buildingName)
    {
        // For each cell on X
        for (int i = x; i < x + sizeX; i++)
        {
            // For Each Cell on Z
            for (int j = z; j < z + sizeZ; j++)
            {
                // When grid is made 
                if (i >= 0 && i < gridSize && j >= 0 && j < gridSize)
                {
                    // Check if the cell is occupied by another building with a different name
                    if (cellStates[i, j] == CellState.Occupied && cellBuildingNames[i, j] != buildingName)
                    {
                        return true; // Cell is occupied by a different building
                    }
                }
                else
                {
                    return true; // Assuming cells outside the grid are taken
                }
            }
        }
        return false; // Cell is not occupied by a different building
    }

    /// <summary>
    /// Function that helps Occupy a Cell that is not taken by a building
    /// </summary>
    /// <param name="x"></param>
    /// <param name="z"></param>
    /// <param name="sizeX"></param>
    /// <param name="sizeZ"></param>
    /// <param name="buildingName"></param>
    public void OccupyCells(int x, int z, int sizeX, int sizeZ, string buildingName)
    {
        // Get X values for cells
        for (int i = x; i < x + sizeX; i++)
        {
            // Get Z values for Cells
            for (int j = z; j < z + sizeZ; j++)
            {
                if (i >= 0 && i < gridSize && j >= 0 && j < gridSize)
                {
                    cellStates[i, j] = CellState.Occupied;  // Cell is now occupied 
                    cellBuildingNames[i, j] = buildingName; // store string of building name occupying cells 
                }
            }
        }
    }

    /// <summary>
    /// Function allows Cells that are currently taken to be put back into an empty state as building moved away
    /// </summary>
    /// <param name="x"></param>
    /// <param name="z"></param>
    /// <param name="sizeX"></param>
    /// <param name="sizeZ"></param>
    /// <param name="buildingName"></param>
    public void ReleaseCells(int x, int z, int sizeX, int sizeZ, string buildingName)
    {
        // Get X values of cells
        for (int i = x; i < x + sizeX; i++)
        {
            // Get Z values of cells
            for (int j = z; j < z + sizeZ; j++)
            {
                // If grid is made
                if (i >= 0 && i < gridSize && j >= 0 && j < gridSize)
                {
                    cellStates[i, j] = CellState.Empty; // Grid that was occupied is now empty 
                    cellBuildingNames[i, j] = "";   // building string name reset for nothing is occupying
                }
            }
        }
    }

    public void ClearBuildingOccupancy(string buildingName)
    {
        for (int i = 0; i < gridSize; i++)
        {
            for (int j = 0; j < gridSize; j++)
            {
                if (cellBuildingNames[i, j] == buildingName)
                {
                    cellStates[i, j] = CellState.Empty;
                    cellBuildingNames[i, j] = "";
                }
            }
        }
    }
}

Check it out for yourself

I’ve added my GitHub so the UnityPackage can be installed. I’ve also added the Version of unity I’m using – Enjoy 🙂