Use Tilemaps to get a 2D isometric grid

I thought a good way to implement a grid of nodes would be to use an existing tilemap. I have designed a 2D isometric tile, that is 128x64 pixels, and a block which is 128x128 pixels. The grid displays but is offset to where it should be, I’m not sure if I have something wrong in the settings for tilemaps or if I have made a mistake with the script.

public class Grid : MonoBehaviour
{
   
    public Tilemap tilemap; // Reference to the isometric tilemap
    public LayerMask walkableLayerMask; // For obstacle detection

    Node[,] grid;

    int gridSizeX, gridSizeY;

    void Start()
    {
        gridSizeX = tilemap.cellBounds.size.x; // x-axis grid size from tilemap
        gridSizeY = tilemap.cellBounds.size.y; // y-axis grid size from tilemap
        CreateGrid();
    }

    void CreateGrid()
    {
        grid = new Node[gridSizeX, gridSizeY];

        BoundsInt bounds = tilemap.cellBounds; // boundaries of tilemap in cell co-ordinates
        TileBase[] allTiles = tilemap.GetTilesBlock(bounds); // Retrieves all tiles in the given bounds

        for (int x = 0; x < gridSizeX; x++) // For x-axis tiles
        {
            for (int y = 0; y < gridSizeY; y++) // For y-axis tiles
            {
                TileBase tile = allTiles[x + y * bounds.size.x];

                Vector3 cellWorldPos = tilemap.GetCellCenterWorld(new Vector3Int(x, y, 0)); // Gets tilemap centre
                bool walkable = !(Physics2D.OverlapCircle(cellWorldPos, 0.1f, walkableLayerMask));
                grid[x, y] = new Node(walkable, cellWorldPos); // Create a Node instance and store it
            }
        }
    }

    public Node NodeFromWorldPoint(Vector3 worldPosition)
    {
        // Calculate the percentage of the x and y co-ordinates within the tilemap
        float percentX = Mathf.Clamp01((worldPosition.x + tilemap.cellSize.x * 0.5f - tilemap.origin.x) / tilemap.size.x);
        float percentY = Mathf.Clamp01((worldPosition.y + tilemap.cellSize.y * 0.5f - tilemap.origin.y) / tilemap.size.y);

        // Calculate the array indices based on the percentages
        int x = Mathf.RoundToInt((gridSizeX - 1) * percentX);
        int y = Mathf.RoundToInt((gridSizeY - 1) * percentY);
        return grid[x, y];
    }

    void OnDrawGizmos()
    {
        Gizmos.DrawWireCube(transform.position, new Vector3(gridSizeX, gridSizeY, 1));

        if (grid != null)
        {
            foreach (Node n in grid)
            {
                Gizmos.color = n.isWalkable ? Color.white : Color.red;
                Gizmos.DrawCube(n.position, Vector3.one * (Mathf.Min(tilemap.cellSize.x, tilemap.cellSize.y) - 0.1f));
            }
        }
    }
}

I think the grid is of by about 31,22, from quickly counting it.

Start digging into the scene when it’s running, see what all the positions of Transforms and Grids are. Anything that isn’t at (0,0,0) with identity rotation and scaling would cause offset.

The position of the tilemap grid is at 0,0,0 the scale is 1,1,1. The cell size is 1,0.5,1 the cell gap is 0,0,0 it is set to Isometric and Cell Swizzle XYZ. The transform for the ground, child of the grid is at 0,0,0, there is no rotation and scale is 1,1,1. The Tilemap component has Tile Anchor of 0,0,0 and orientation of XY, which were the default when setting up the tilemap. For the tile, its set to 128 pixels per unit, the pivot is set to bottom.

I have placed the Grid script to be component of the Grid, I have set the layer mask for the unwalkable parts and the tilemap to the ground tilemap.

I have found that the problem is with my code. I have tried to rewrite it and used some Debug.Log to find what values I’m getting tilemap.origin is at (-17,-21,0), I would have expected it to be at (0,0,0) but tilemap.transform.position gives this instead. Is there a way to get local origin, like you can with local position of a transform.

void GenerateNodes()
    {
        grid = new Node[numTilesX, numTilesY];

        Vector3 tilemapOrigin = tilemap.origin;
        Debug.Log(tilemapOrigin.ToString());
        //Prints the origin as (-17,-21,0) for the current grid 40,38

        Debug.Log(tilemap.transform.localPosition.ToString());
        Vector3 actualOrigin = tilemap.transform.position;
        //Prints at (0,0,0)

        Vector3 cellSize = tilemap.cellSize;
        Debug.Log(cellSize.ToString());

        Vector3 originOffset = actualOrigin - tilemapOrigin;
        Debug.Log("The originOffset xyz is, " + originOffset.ToString());

        for (int x = 0; x < numTilesX; x++)
        {
            for (int y = 0; y < numTilesY; y++)
            {
                Vector3Int cellPosition = new Vector3Int(x, y, 0);

                Vector3 cellCentreWorld = tilemap.GetCellCenterWorld(cellPosition); 

                bool walkable = !(Physics2D.OverlapCircle(cellCentreWorld, 0.1f, walkableLayerMask));
                Node node = new Node(walkable, cellCentreWorld);
                grid[x, y] = node;
            }
        }
    }

As the docs state, this is the cell position at its origin, not the world position.

There are lots of conversion methods from local<->cell<->world available to you. For cell to world it’s: Unity - Scripting API: GridLayout.CellToWorld

2 Likes

Thank you for the help. Although I have still gone wrong somewhere. Where I was using this line of code Vector3 cellCentreWorld = tilemap.GetCellCenterWorld(cellPosition); I was getting this first screenshot. 9262041--1295928--upload_2023-8-30_22-29-39.jpg

Using CellToWorld(cellPosition) I’m getting the nodes being placed at the corner of each cell on the grid. Its still not in the correct cell, it should be placed in that very bottom cell.
9262041--1295931--upload_2023-8-30_22-31-27.jpg

This is the grid layout so I’m not sure what exactly you mean by “bottom cell”. Do you mean the cell lowest to the bottom of the image? If so, that cell is likely a negative cell position.

The very bottom cell isn’t always (0,0,0) as that would mean all the cell positions change as you paint. If you paint a cell at (0,0,0) and everything positive then it will be but it’s totally up to where you paint. Is this what is confusing perhaps?

I managed to figure it out. I orginally thought that Vector3 tilemapOrigin = tilemap.origin; would be 0,0,0 but it wasn’t. My nested for loop started at 0,0,0 which is the world position, not the local position of that cell. So it was that, that needed to be offset.

If anyone else has this issue here is my code:

public class Grid : MonoBehaviour
{
    public Tilemap tilemap; // Reference to the isometric tilemap
    public Grid gridBase;
    public LayerMask walkableLayerMask; // For obstacle detection
    public Transform playerPosition;

    Node[,] grid;

    private int numTilesX; // Number of tiles in the X axis
    private int numTilesY; // Number of tiles in the Y axis

    void Start()
    {
        numTilesX = CalculateNumTilesX(tilemap.cellBounds);
        numTilesY = CalculateNumTilesY(tilemap.cellBounds);

        Debug.Log("Number of tiles on the x-axis = " + numTilesX.ToString() + ", on the y-axis = " + numTilesY.ToString());
        //Prints 40,38 which are the number of tiles in scene view.

        GenerateNodes();
    }

    int CalculateNumTilesX(BoundsInt bounds)
    {
        return bounds.size.x;
    }

    int CalculateNumTilesY(BoundsInt bounds)
    {
        return bounds.size.y;
    }

    void GenerateNodes()
    {
        grid = new Node[numTilesX, numTilesY];

        Vector3 tilemapOrigin = tilemap.origin; //Origin of the tilemap
       
        Vector3 actualOrigin = tilemap.transform.position; //Actual position of the tilemap
       
        Vector3 originOffset = actualOrigin - tilemapOrigin; //Calculation to offset the node to the correct tile

        for (int x = 0; x < numTilesX; x++)
        {
            for (int y = 0; y < numTilesY; y++)
            {
                Vector3Int cellPosition = new Vector3Int(x, y, 0);

                cellPosition -= Vector3Int.FloorToInt(originOffset); //Places the nodes on the correct tile

                Vector3 cellCentreWorld = tilemap.GetCellCenterWorld(cellPosition);

                // Check if a tile exists in the current cell
                bool hasTile = tilemap.HasTile(cellPosition);

                if (!hasTile)
                {
                    grid[x, y] = null; // Skip this cell
                    continue;
                }

                bool walkable = !(Physics2D.OverlapCircle(cellCentreWorld, 0.1f, walkableLayerMask)); //Check to see if tile is able to walked on
                Node node = new Node(walkable, cellCentreWorld); //For the Node, gives information for if the tile is walkable
                grid[x, y] = node;


            }
        }
    }



    // Draw Gizmos in the Scene view
    void OnDrawGizmos()
    {
        if (grid != null)
        {
            // Visualize nodes using Gizmos
            foreach (Node node in grid)
            {
                // Check for null node
                if (node == null)
                    continue;

                Gizmos.color = node.isWalkable ? Color.green : Color.red;
                Gizmos.DrawCube(node.position, Vector3.one * 0.1f);
            }

            // Draw player's position
            Gizmos.color = Color.blue;
            Gizmos.DrawSphere(playerPosition.position, 0.1f);
        }
    }

}

And here are the gizmos in each cell to determine if there is an obstacle
9264348--1296378--upload_2023-8-31_17-27-52.png

Thank you for the help.

1 Like

Good stuff. Good luck with your project.