Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

A* Pathfinding with Unity Tilemaps

Discussion in '2D' started by Tom163, Jul 14, 2021.

  1. Tom163

    Tom163

    Joined:
    Nov 30, 2007
    Posts:
    1,293
    I was looking for a solution to this everywhere, finally found a few, didn't quite like them, found a few more I liked better, but anyway, here is my approach:

    I wanted to have pathfinding on a Unity Tilemap without any extras. Without painting a "walkable" layer as a separate tilemap on top or adding another grid.

    What I did was to extend the RulesTile class like this:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Tilemaps;
    5.  
    6.  
    7. namespace Lemuria {
    8.  
    9.     [CreateAssetMenu(fileName = "New TileData", menuName = "Ghost/TileData")]
    10.  
    11.     public class TileData : RuleTile {
    12.      
    13.         public bool walkable;
    14.      
    15.     }
    16. }
    17.  
    That gives me a nice checkbox in my tile definitions. The idea is that a wall tile would be not walkable, a floor tile would be walkable, etc.

    Obviously, this could trivially be expanded to have a cost in addition to the simple flag. And someone may contribute here in this thread how to extend it to cover things such as doors or dynamic obstacles which can be sometimes walkable and sometimes not.


    I took the A* implementation from https://bitbucket.org/Sniffle6/tilemaps-with-astar/ - @sniffle63 posted a video tutorial on his approach here: https://forum.unity.com/threads/tutorial-implement-astar-with-the-tilemap-system.635833/ and it came closest to what I wanted, but it still uses seperate tilemaps.

    I then followed his example to write a GridManager class:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Tilemaps;
    5.  
    6.  
    7.  
    8. namespace Lemuria {
    9.  
    10.     public class GridManager : MonoBehaviour {
    11.      
    12.         public Tilemap tilemap;
    13.         BoundsInt bounds;
    14.         public Vector3Int[,] spots;
    15.         Astar astar;
    16.         public int maxSteps = 1000;
    17.  
    18.  
    19.         void Start() {
    20.             tilemap.CompressBounds();
    21.             bounds = tilemap.cellBounds;
    22.             CreateGrid();
    23.             astar = new Astar(spots, bounds.size.x, bounds.size.y);
    24.         }
    25.  
    26.  
    27.  
    28.         void CreateGrid() {
    29.             spots = new Vector3Int[bounds.size.x, bounds.size.y];
    30.             for (int x = bounds.xMin, i = 0; i < (bounds.size.x); x++, i++) {
    31.                 for (int y = bounds.yMin, j = 0; j < (bounds.size.y); y++, j++) {
    32.                     Vector3Int myGridPos = new Vector3Int(x, y, 0);
    33.                     TileBase myTile = tilemap.GetTile(myGridPos);
    34.                     if (myTile && myTile is TileData && ((TileData)myTile).walkable == true) {
    35.                         spots[i, j] = new Vector3Int(x, y, 0);
    36.                     } else {
    37.                         spots[i, j] = new Vector3Int(x, y, 1);                      
    38.                     }
    39.                 }
    40.             }
    41.          
    42.         }
    43.      
    44.         public List<Spot> CreatePath(Vector2Int startCell, Vector2Int endCell) {
    45.             return astar.CreatePath(spots, startCell, endCell, maxSteps);
    46.         }
    47.  
    48.  
    49.     }
    50. }
    51.  

    And a simple Pathfinder class that I can add to my AI GameObjects:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5.  
    6. namespace Lemuria {
    7.  
    8.     public class Pathfinder : MonoBehaviour {
    9.      
    10.         GridManager GM;
    11.         List<Spot> path = new List<Spot>();
    12.  
    13.         void Start() {
    14.             GM = FindObjectOfType<GridManager>();
    15.         }
    16.  
    17.  
    18.         // pathfinds and returns the next position we want to move to
    19.         Vector2Int FindNextPosition(Vector2Int destination) {
    20.             Vector3Int gridPos = GM.tilemap.WorldToCell(transform.position);
    21.             Vector2Int myPosition = new Vector2Int(gridPos.x, gridPos.y);
    22.  
    23.             path = GM.CreatePath(myPosition, destination);
    24.             if (path.Count < 2) return Vector2Int.zero; // we don't have a proper path
    25.             return new Vector2Int(path[path.Count-2].X, path[path.Count-2].Y);
    26.         }
    27.      
    28.  
    29.  
    30.         public Color markerColor = Color.yellow;
    31.         public float markerSize = 0.2f;
    32.      
    33.         void OnDrawGizmosSelected() {
    34.             if (GM) {
    35.                 Gizmos.color = markerColor;
    36.                 Grid grid = GM.tilemap.layoutGrid;
    37.  
    38.                 foreach (Spot s in path) {
    39.                     Vector3 worldPosition = grid.GetCellCenterWorld(new Vector3Int(s.X, s.Y, 0));
    40.                     Gizmos.DrawSphere(worldPosition, markerSize);
    41.                 }
    42.             }
    43.         }
    44.  
    45.     }
    46. }
    47.  
    Note that since I'm working on a turn-based game, I return only the next grid position I want to walk to, not the entire path. I also call this every turn, just in case my target has moved (in case I move towards an enemy, not a fixed position). Obviously, if performance matters, you may want a separate function for moving to fixed positions, but I have a low number of enemies on screen and don't worry about performance just yet (no premature optimization).

    In a realtime game you probably want to return the whole path, store it, follow it for a set period of time and then re-path if the player moved.



    I'll be happy to hear feedback or possible improvements to the code.
     
    Last edited: Jul 14, 2021
  2. sniffle63

    sniffle63

    Joined:
    Aug 31, 2013
    Posts:
    365
    Great work!

    Was going to try for an approach like this at first but ended up taking the easy route!
     
    Tom163 likes this.
  3. Tom163

    Tom163

    Joined:
    Nov 30, 2007
    Posts:
    1,293
    Thanks.

    Here's a small update that will not error out when it can't find a route:

    Code (CSharp):
    1.         // pathfinds and returns the next position we want to move to
    2.         Vector2Int? FindNextPosition(Vector2Int destination) {
    3.             Vector3Int gridPos = GM.tilemap.WorldToCell(transform.position);
    4.             Vector2Int myPosition = new Vector2Int(gridPos.x, gridPos.y);
    5.  
    6.             if (myPosition == destination) {
    7.                 return null;
    8.             }
    9.             path = GM.CreatePath(myPosition, destination);
    10.             if (path.Count < 2) return null;
    11.             return new Vector2Int(path[path.Count-2].X, path[path.Count-2].Y);
    12.         }
    13.  

    Note that you need to check the return value for .HasValue and access the value through the .Value property, as for all nullables.
     
    sniffle63 likes this.