# Finding the player node on a 2d isometric grid

I have used a tilemap grid to generate a grid of nodes, thinking it would be an easy way to set up a grid without needing to configure the size of my grid manually for each different scene. I came across an issue that the nodes were offset from the actual tilemap, which I managed to solve on my own, the original thread of that can be found here:

From solving that problem I found that the bottom right tile was not the 0,0 origin, for example it might actually have been at -17,-21. This leads me to my current problem, as part of the reason I wanted to create a grid of nodes was for pathfinding. I need to get the node that the player character is on, which I canâ€™t currently work out how to do.

I was following this tutorial and have adapted it to work with 2d isometric tilemap:

I also found this from someone else who is using isometric perspective but hasnâ€™t used tilemaps from looking at their code:

This is the code for generating the nodes:

``````public class Grid : MonoBehaviour
{
public Tilemap tilemap; // Reference to the isometric tilemap
public Grid gridBase;
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 45,47 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
Debug.Log(tilemapOrigin.ToString());

Vector3 tilemapSize = tilemap.size;
Debug.Log(tilemapSize.ToString());

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;
}
}
}

public Node GetNodeFromWorldPoint(Vector3 worldPosition)
{
return null;
}

// Draw Gizmos in the Scene view
void OnDrawGizmos()
{
if (grid != null)
{
Node playerNode = GetNodeFromWorldPoint(playerPosition.position);
// 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;
if(playerNode == node)
{
Gizmos.color = Color.blue;
}
Gizmos.DrawCube(node.position, Vector3.one * 0.1f);

}

}
}

}
``````

Plus these are the various bits of code I have tried in the GetNodeFromWorldPoint() method:

``````public Node GetNodeFromWorldPoint(Vector3 worldPosition)
{
Vector3Int cellPosition = tilemap.WorldToCell(worldPosition);
Vector3 cellCentreWorld = tilemap.GetCellCenterWorld(cellPosition);

float percentX = (worldPosition.x - 2 * worldPosition.y - cellCentreWorld.x - 2 * cellCentreWorld.y) / 4;
float percentY = (worldPosition.x + 2 * worldPosition.y - cellCentreWorld.x - 2 * cellCentreWorld.y) / 4;
percentX = Mathf.Clamp01(percentX);
percentY = Mathf.Clamp01(percentY);

int x = Mathf.RoundToInt((numTilesX - 1) * percentX);
int y = Mathf.RoundToInt((numTilesY - 1) * percentY);

return grid[x, y];
}

public Node NodeFromWorldPoint(Vector3 worldPosition)
{
// Convert the world position to a cell position within the tilemap
Vector3Int cellPosition = tilemap.WorldToCell(worldPosition);

// Adjust for the isometric grid layout
if (cellPosition.y % 2 != 0)
{
cellPosition.x -= 1; // Apply the offset for every other row
}

// Check if the cell position is within the grid bounds
if (cellPosition.x >= 0 && cellPosition.x < numTilesX && cellPosition.y >= 0 && cellPosition.y < numTilesY)
{
// Return the corresponding node from the grid
return grid[cellPosition.x, cellPosition.y];
}

// Return null if the world position is outside the grid
return null;
}
public Node GetNodeFromWorldPoint(Vector3 worldPosition)
{
//Calculate the percentage of the position within the grid's bounds
float percentX = (worldPosition.x + gridBase.transform.position.x) / (tilemap.size.x * 2);
float percentY = (worldPosition.y + gridBase.transform.position.y) / (tilemap.size.y * 2);

// Clamp the percentages to ensure they are within [0, 1]
percentX = Mathf.Clamp01(percentX);
percentY = Mathf.Clamp01(percentY);

// Calculate the grid indices based on percentages
int x = Mathf.RoundToInt((numTilesX - 1) * percentX);
int y = Mathf.RoundToInt((numTilesY - 1) * percentY);

// Adjust x and y based on the isometric layout
if (y % 2 != 0)
{
x = Mathf.RoundToInt((numTilesX - 1) * percentX - 0.5f);
}

// Make sure x and y are within valid grid bounds
x = Mathf.Clamp(x, 0, numTilesX - 1);
y = Mathf.Clamp(y, 0, numTilesY - 1);

return grid[x, y];
}
``````

One of the thoughts I had about this was from the Tilemap API, Unity - Scripting API: Tilemap and using one of the public methods CellToWorld, WorldToCell, WorldToLocal etc. Any help would be greatly appreciated as I canâ€™t seem to find anyone who has had a similar issue.

In my method of A* pathfinding, I use the classes(Tile.cs) as the things to check, which may be similar to your nodes? And looks like this:

``````public static List<Tile> FindAnyPath(Tile startNode, Tile targetNode)
{
List<Tile> toSearch = new List<Tile>() { startNode };
List<Tile> processed = new List<Tile>();
while (toSearch.Count > 0)
{
Tile current = toSearch[0];
foreach (Tile t in toSearch)
if (t.F < current.F || t.F == current.F && t.H < current.H) current = t;
toSearch.Remove(current);
if (current == targetNode)
{
Tile currentPathTile = targetNode;
List<Tile> path = new List<Tile>();

while (currentPathTile != startNode)
{
currentPathTile = currentPathTile.cameFrom;
}
path.Reverse();
return path;
}
{
{
{
if (!inSearch)
{
}
}
}
}
}
return null;
}
``````

With the key â€śmeat and potatoesâ€ť being the Tile.adjacentTiles, which are set after map creation, and can be modified for certain scenarios like thin walls between 2 tiles.

But for sure, if youâ€™re mixing several tutorials together? Youâ€™re just gonna produce headaches, unless you fully grasp what each tutorial is doing, in what way, and why. Not saying you shouldnâ€™t experiment, as you most definitely should!

I know that the Tilemap inherits from GridLayout and Tile inherits from Tilemap. If I understand your code, youâ€™re assigning a cost to move from a starting to get to the end node. What I have got is in this image:

The white square being the player, small green dots are the node for each tile, and the blue dot is the node at which it thinks the player is currently at. This link for the tutorial Pathfinding/Episode 02 - grid/Assets/Grid.cs at master Â· SebLague/Pathfinding Â· GitHub was for a top down perspective, and they created a grid manually as they didnâ€™t use tilemaps, but the basic principles are the same of creating a grid of nodes, with difference being Iâ€™m applying it to an isometric tilemap. The other link Get node from position isometric was someone else who looks like they followed that other tutorial, but unlike me hasnâ€™t used a tilemap, and hasnâ€™t had the same offset issue I have had because of this.

For the understanding of that tutorial, I get how the grid is formed. The part Iâ€™m a bit less certain on is understanding the math being applied to find the node that the player is on. So they are adding the position of the player x and y to the size of the grid x and y and dividing by 2, and then once again dividing that by the grid size x and y. Which gives a percentage that theyâ€™re then clamping between 0 and 1. To get x and y for the grid then they are taking the grid size in x and y -1 and multiplying that by the percentage x and y value they got to give the correct grid position.

As I have had to offset the nodes to the correct tile due to how the tilemaps seem to work. How Iâ€™m thinking about it logically, I need to offset the player position to the grid node position or vice versa, if that makes sense? So that in the above image the tile either to the right or above, depending on which one the player is closest to should be blue instead of green.

correct, cost is basically just distance. So it checks all the adjacent tiles from the start tile, and chooses the one that is closer to the goal, and then makes that the current tile, and continues to repeat the process. Saving the best way from where it came from in each tile as you see â€ścameFromâ€ť. Until the last â€ścurrent tileâ€ť finally gets to the end tile, then it back tracks to the start tile with the most efficient way, reverses then returns that list as the path.

In the way I do things, each tile(node) would not only have itâ€™s â€śgridPosâ€ť but also itâ€™s â€śworldPosâ€ť. So I just counter check within the distance of a tile, and correlate that with the players world position, and get the tile very easily. This is why itâ€™s good to make your node(tile) a actual class, so it can save variables within it like gridPos[6,9] and worldPos(11.7,0,18.2), among other useful information.

As far as calculating the grids dimensions, that sounds like an awful lot of beating around the bush to me. If anything the grid should be set to whole values, like 1 or 10 units wide/long, so the world positions line up with easy calculations.

Currently Iâ€™m making a hex style map, so gridPos doesnâ€™t line up with worldPos at all, so I have each hex have itâ€™s own update method, and checks a static Vector3 I make for checking where the mouse clicks. Which is of course rounded, so the checks donâ€™t run constantly in each of the many hexes. And once the rounded position matches up with that hex, that hex calls out and says â€śyou want meâ€ť(technically).

Another way, that would be far easier, is collision checks, either via raycast, or a sort of CheckSphere() type physics call. But thatâ€™s assuming you have any colliders, which you might not in this case.

1 Like

I have the nodes as class here:

``````public class Node
{
public Vector3 position { get; }
public bool isWalkable { get; }

public Node(bool _isWalkable, Vector3 _position)
{
position = _position;
isWalkable = _isWalkable;
}
}
``````

So you would store both gridPosition and worldPosition to this class?

The way I have it setup, the grid dimensions are from the Tilemap class, and they come back as say 4,10 etc, where as the world position for the player might be 12,16.

I will setup a tilemap collider so the character canâ€™t go out of bounds, and also a composite collider. I thought of maybe doing something with a layer mask as well for the player, which would be similar for checking collisions or likewise I could check for collisions. I will have a think about it over the next few days. Thanks for the help.

I just make a function that the player uses to not go out of bounds, right after movement:

``````public static void StayInBounds(Transform trans)
{
float bounds = (Global.gridWidth * 5) - 5; // will be different same as -10 lower
float X = trans.position.x;
float Y = trans.position.y;
float Z = trans.position.z;
if (X < -bounds - 10) { trans.position = new Vector3(-bounds - 10, Y, Z); }
if (Z < -bounds - 10) { trans.position = new Vector3(X, Y, -bounds - 10); }
if (X > bounds) { trans.position = new Vector3(bounds, Y, Z); }
if (Z > bounds) { trans.position = new Vector3(X, Y, bounds); }
}
``````

Youâ€™d have to play around with the above method, as those values were for my particular instance, so basically delete the â€ś10â€ť parts, then see if you need offsets for more correct math.

Well my tiles inherit from monobehaviour, as they technically are gameObjects. But yes you can still store any values in them:

``````public class Node : Monobehaviour
{
public Vector3 worldPos;
public Vector2 gridPos;
public bool isWalkable;
public Node cameFrom;

void Awake()
{
// Add to list here, or in node maker part
worldPos = transform.position;
}
}

public class NodeManager : MonoBehaviour // place in empty object in scene
{
public static List<Node> allNodes = new List<Node>();

{
int nodesCount = allNodes.Count;
float smallestDistance = 100f;
int index = 0;
for (int i = 0; i < nodesCount; i++)
{
float check = Vector3.Distance(allnodes[i].worldPos, position);
if (check < smallestDistance)
{
smallestDistance = check;
index = i;
}
}
return allNodes[i];
}
// or
public static Node GetNodeAtPositionByRounding(Vector3 position)
{
float nodeWH = 10; // easy value?
int playerX = (int)(Mathf.Round(position.x / nodeWH) * nodeWH);
int playerZ = (int)(Mathf.Round(position.z / nodeWH) * nodeWH);
for (int i = 0; i < nodesCount; i++)
{
if (allNodes[i].worldPos.x == playerX && allNodes[i].worldPos.z == playerZ)
return allNodes[i]; // if successful
}
print("No node found by rounding"); // if failed
return null; // if failed
}
}
``````

Just as 2 examples ^, you might have to play around with the variances to match up with what you got. But thatâ€™s the technical idea behind that way.

But as I mentioned, nothing will beat a single raycast or collision check(if node has collider on it). As iterating through a ton of (even)simple references can cause a bit of a stutter in frames.

Just keep playing around with the idea, or even stick with the way they gave as examples. You personally would have to test, by benchmarking each way, to see which oneâ€™s best. But thereâ€™s always multiple ways of doing the same thing.

2 Likes

You shouldnâ€™t do this. Here you call the native get_position method 3 times. Each time it would calculate the worldspace position and return a Vector3 struct which is temporary stored on the stack. You then just grab one of the components in each case. This would make more sense:

``````    public static void StayInBounds(Transform trans)
{
float bounds = Global.halfGridWidth;
Vector3 pos = trans.position;
pos.x = Mathf.Clamp(pos.x, -bounds, bounds);
pos.z = Mathf.Clamp(pos.z, -bounds, bounds);
trans.position = pos;
}
``````

Though note that in many cases it makes more sense to have the player a child of the grid parent and use localPosition. This makes things much more flexible especially when you need to rotate or offset (floating origin?) the grid itself. Reading / writing the localPosition is also cheaper, especially when the object is a child. Though of course it doesnâ€™t make always sense to use the localPosition.

Anyways, you should avoid using magic hard-coded numbers like your 5 / 10.

1 Like

Well each tile in that case was a distance of 10, but grid width counted them as 1, and had a issue if the map generation was a even or odd number of tiles. So after much testing that was what was needed for the objects to stay in bounds properly. But it does look odd, so Iâ€™ll probably have to go back over it at some point.

``````float X = trans.position.x;
float Y = trans.position.y;
float Z = trans.position.z;
``````

And this was to just simplify the bottom reading, as each of the four checks called for those many times, so 3 was better in that case. The negative 10, was because the tiles root positions were bottom left, so had to add that extra 10 in 2 directions. Looking back at it, probably wouldâ€™ve been best to make their center there root positions, lolâ€¦

But true, looking back on a lot of my old code, I gotta redo an awful lot!

I have not been able to work on this for a while, but have come back today and finally solved my problem. Thank you for the help. I got my original way to work in the end, storing the node into a dictionary and then comparing the players position to the nodes position.

If anyone has a similar issue in the future and finds this thread here is what I have come up with

``````void GenerateNodes()
{
grid = new Node[numTilesX, numTilesY];
nodes = new Dictionary<Vector2, Node>();

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.2f, 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;

nodes[new Vector2Int(x, y)] = node;

}
}
}

public Node GetNodeFromWorldPoint(Vector3 worldPosition)
{
Node closestNode = null;
float closestDistance = float.MaxValue;

foreach (Node node in nodes.Values)
{
// Calculate the distance between the node's world position and the player's world position
float distance = Vector3.Distance(node.position, worldPosition);

// If this node is closer to the player, update the closest node
if (distance < closestDistance)
{
closestDistance = distance;
closestNode = node;
}
}

return closestNode;
}
``````