2D Pathfinding

So I was looking into having pathfinding in my Unity game. I’m new to Unity, but I tried searching for it, but there’s a lot of different answers out there that really doesn’t match my conditions.

I’ve created a map using the Tilemap in Unity, where I’ve placed walls in their own Tilemap and each tile with a Box Collider. I found the NavMeshComponents, but I can’t get them to work with my game. Researched into it further and it seems that it doesn’t support 2D games.

I also found the A* Pathfinding Project on the store, but the pro version costs 100$ and in the free version you have to create your own NavMeshes manually. That seems like it would take a lot of time.

What type of pathfinding solutions do you guys use? :slight_smile:

1 Like

@MayoNinjaGames
I have Aron Granberg’s A* Pathfinding Project, and while it is quite useful, I found it to be overkill for my own project. It took more effort to learn how to use his version than write my own. The following is my own implementation of the A* algorithm. Hope this helps. I have documented the resources I used to write it.

Pathfinding

using System;
using System.Collections.Generic;
using System.Linq;
using Extensions;
using UnityEngine;

namespace Systems
{
    public static class Pathfinding
    {   //The following resources were used to write this class:
        //SEE https://www.raywenderlich.com/3016-introduction-to-a-pathfinding
        //AND https://medium.com/@nicholas.w.swift/easy-a-star-pathfinding-7e6689c7f7b2

        private class MapSquare : IComparable<MapSquare>
        {
            public readonly Vector2Int Position;

            private int _g;

            public int F => _g + H;

            public int G
            {
                get { return _g; }
                set { _g = value; }
            }

            public int H { get; set; }

            public MapSquare Parent;

            public MapSquare(Vector2Int location, int g, int h)
            {
                Position = location;
                _g = g;
                H = h;
            }

            public int CompareTo(MapSquare other)
            {
                if (F > other.F)
                {
                    return -1;
                }

                if (F == other.F)
                {
                    return 0;
                }

                return 1;
            }

            public bool Equals(MapSquare other)
            {
                return Position.x == other.Position.x && Position.y == other.Position.y;
            }

            public override int GetHashCode()
            {
                return 7 * Position.x + 11 * Position.y;
            }
        }

        public static bool GetPathTo(Vector2Int from, Vector2Int to, out List<Vector2Int> path, bool includeEndpoints = false)
        {
            //Get map maximum values
            Vector2Int mapMaxBounds = MapDataAccess.Service.mapMaxBounds;

            //Declare openList
            List<MapSquare> openList = new List<MapSquare>();

            //Declare closed list
            List<MapSquare> closedList = new List<MapSquare>();

            //The first square will contain the "from" location
            MapSquare firstSquare = new MapSquare(
                from, 0, from.GetDeltaTo(to)
            );

            //Add the original square to the open list
            openList.Add(firstSquare);

            bool pathFound = false;

            do
            {
                //Sort open list
                openList = openList.OrderBy(l => l.F).ToList();

                //Set current square equal to square with the lowest "F" = G + H
                MapSquare currentPathSquare = openList.First();

                //Add current square to the closed list
                closedList.Add(currentPathSquare);

                //Remove current square from open list
                openList.Remove(currentPathSquare);

                //Create a list of neighbor blocks
                List<MapSquare> neighborSquares = new List<MapSquare>();

                //Add open neighboring squares
                for (int delta = -1; delta < 2; delta += 2)
                {
                    Vector2Int dxPosition = new Vector2Int(currentPathSquare.Position.x + delta, currentPathSquare.Position.y);
                    Vector2Int dyPosition = new Vector2Int(currentPathSquare.Position.x, currentPathSquare.Position.y + delta);

                    if (dxPosition.x > 0 && dxPosition.x < mapMaxBounds.x && !MapDataAccess.Service.IsCellBlocked(dxPosition))
                    {
                        MapSquare mapSquare = new MapSquare(dxPosition, currentPathSquare.G + 1, dxPosition.GetDeltaTo(to));
                        neighborSquares.Add(mapSquare);
                    }

                    if (dyPosition.y > 0 && dyPosition.y < mapMaxBounds.y && !MapDataAccess.Service.IsCellBlocked(dyPosition))
                    {
                        MapSquare mapSquare = new MapSquare(dyPosition, currentPathSquare.G + 1, dyPosition.GetDeltaTo(to));
                        neighborSquares.Add(mapSquare);
                    }
                }

                //If any neighbor squares are the target, don't even worry about adding anything to the open list
                if (neighborSquares.Any(s => s.Position.Equals(to)))
                {
                    MapSquare targetSquare = neighborSquares.Single(s => s.Position.Equals(to));
                    targetSquare.Parent = currentPathSquare;
                    openList.Add(targetSquare);
                    pathFound = true;
                    break;
                }

                //Add/update neighbors in the open list or ignore them if already in the closed list.
                foreach (MapSquare square in neighborSquares)
                {
                    //Ignore any square that is in the closed list
                    if (closedList.Any(s => s.Equals(square)))
                    {
                        continue;
                    }

                    //Try to find matching square in open list
                    MapSquare openMatch = openList.SingleOrDefault(m => m.Equals(square));

                    //Add square that is not in the open list to the open list
                    if (openMatch == null)
                    {
                        //set parent square to current
                        square.Parent = currentPathSquare;
                        openList.Add(square);
                    }
                    else
                    {
                        //Neighbor is already in the open list. Update it's G score and parent if it is
                        int nextPathG = currentPathSquare.G + 1;
                        if (nextPathG < openMatch.G)
                        {
                            int matchIndex = openList.IndexOf(openMatch);
                            openList[matchIndex].G = nextPathG;
                            openList[matchIndex].Parent = currentPathSquare;
                        }
                    }

                }

            } while (openList.Count != 0); //If there is nothing in the open list, then the path was not found


            //Closed list vis
            foreach (var n in closedList)
            {
                var offSet = 1f;
                Debug.DrawLine(new Vector3(n.Position.x, n.Position.y, 0f), new Vector3(n.Position.x + offSet, n.Position.y + offSet, 0f), Color.red, 1f);
                Debug.DrawLine(new Vector3(n.Position.x, n.Position.y + offSet, 0f), new Vector3(n.Position.x + offSet, n.Position.y, 0f), Color.red, 1f);
            }

            path = new List<Vector2Int>();

            if (pathFound)
            {
                MapSquare lastNode = openList.Last();

                while (lastNode != null)
                {
                    path.Add(lastNode.Position);

                    //Open list vis
                    //Debug.DrawLine(new Vector3(lastNode.Position.x, lastNode.Position.y, 0f), new Vector3(lastNode.Position.x + 1f, lastNode.Position.y + 1f, 0f), Color.cyan, 1f);
                    //Debug.DrawLine(new Vector3(lastNode.Position.x, lastNode.Position.y + 1f, 0f), new Vector3(lastNode.Position.x + 1f, lastNode.Position.y, 0f), Color.cyan, 1f);

                    lastNode = lastNode.Parent;
                }

                if (!includeEndpoints)
                {
                    //Remove the "from" node
                    path.RemoveAt(path.Count - 1);

                    //Remove the "to" node
                    path.RemoveAt(0);
                }

                //Because path will be "reversed", reverse it
                path.Reverse();

                foreach (var pnode in path)
                {
                    Debug.DrawLine(new Vector3(pnode.x, pnode.y, 0f), new Vector3(pnode.x + 1f, pnode.y + 1f, 0f), Color.cyan, 1f);
                    Debug.DrawLine(new Vector3(pnode.x, pnode.y + 1f, 0f), new Vector3(pnode.x + 1f, pnode.y, 0f), Color.cyan, 1f);
                }

                ////Remove
                //path = path.Except(new List<Vector2Int>{path.First()}).ToList();
            }

            return pathFound;
        }
    }
}

Vector2IntExtensions

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

namespace Extensions
{
    public static class Vector2IntPathfindingExtensions
    {
        public static int GetDeltaTo(this Vector2Int from, Vector2Int to)
        {
            //Manhattan Dist
            return Math.Abs(from.x - to.x) + Math.Abs(from.y - to.y);
        }

        public static bool FindPathTo(this Vector2Int origin, Vector2Int destination, out List<Vector2Int> path)
        {
            return Systems.Pathfinding.GetPathTo(origin, destination, out path);
        }

        public static bool IsNeighborOf(this Vector2Int origin, Vector2Int cell)
        {
            return (Math.Abs(cell.x - origin.x) == 1 && cell.y - origin.y == 0 || Math.Abs(cell.y - origin.y) == 1 && cell.x - origin.x == 0);
        }
    }
}

@MayoNinjaGames
Unity does not support NavMesh 2d natively. But NavMeshComponents can be used for 2d to some extent
Check out this proof of concept GitHub - h8man/NavMeshPlus: Unity NavMesh 2D Pathfinding
Feel free to ask question or leave suggestions

This is a very helpful tutorial for beginners. It describes A* pathfinding, but also some simpler algorithms that will work just as well for many games.

Hiya everyone! This is very useful feedback for us in the 2D team at Unity. Please tell us more about what you need for 2D pathing and pathfinding in your projects. How do you intend to use it?

@spryx
In what way was it overkill? What would be a good basic set of functionality for 2D pathfinding?

5 Likes

Hi @rustum ,

I think we need Pathfinding in all kinds of tilemap top down 2d games, so that NPC can walk around TilemapColliders2D, or Player can follow mouse click avoiding obstacles. Also it would be great to use NamMeshModifier on Tilemap, like Jump area or areas with difficult terrain,

Current NavMesh Pathfinding suites fine, the problem is to generate NamMesh from Tiles.

I had implemented it in NavMeshSurface2d (GitHub - h8man/NavMeshPlus: Unity NavMesh 2D Pathfinding), but support is very limited, I threats all tiles as Box sources.
It would be great if NavMeshBuilder.CollectSources will generate effective meshes from tilemap.

Also, if game is not tile-based, it would be great to generate NamMesh from art geometry.

1 Like

https://github.com/rakkarage/PathFinder
here is a 2d astar path finder that works with new unity tilemaps, using just points and TileBase not colliders and physics

1 Like

Do you have any estimated timeframe on when this is done? I’m new to Unity, but it’s really awesome seeing you guys being so commited to it!

I should point out it is overkill for my project - the package itself is quite useful for many others and offers multiple types of pathfinding. I wanted a simple implementation of A* as my project is 2d, grid-based, and doesn’t use colliders.

The following code was required to initialize A* PP

public void Generate()
    {
        //Generate Karcero Map
        var map = _builder.Now();

        //Create a virtualmap from karcero generation
        KarceroVirtualMap karceroVirtualMap = new KarceroVirtualMap(map);

        //Using a map translator, visualize the map with the tilemap system
        karceroVirtualMap.VisualizeMap(GetComponent<MapVisualTranslator>());

        //Game can now get information about the map
        MapDataAccess.Service.Initialize();

        //Prepare FOV
        FieldOfView.System.InitVis();

        //Prepare Pathfinding
        AstarData ad = AstarPath.active.data;

        GridGraph pfGraph = ad.AddGraph(typeof(GridGraph)) as GridGraph;

        // Setup a grid graph with some values
        int width = map.Width;
        int depth = map.Height;
        float nodeSize = 1;

        //movement in NSEW
        pfGraph.neighbours = NumNeighbours.Four;

        //Set graph center to map center plus the depth of a single node/2
        pfGraph.center = new Vector3(map.Width / 2 + nodeSize / 2, map.Height / 2 + nodeSize / 2);

        //Set 2d
        pfGraph.rotation = new Vector3(-90f, 0f, 0f);

        //Update
        pfGraph.UpdateTransform();

        //Set dimension
        pfGraph.SetDimensions(width, depth, nodeSize);

        AstarPath.active.AddWorkItem(new AstarWorkItem(ctx =>
        {
            for (int y = 0; y < pfGraph.depth; y++)
            {
                for (int x = 0; x < pfGraph.width; x++)
                {
                    var node = pfGraph.GetNode(x, y);
                    node.Walkable = !MapDataAccess.Service.IsCellBlocked(x, y);
                }
            }

            // Recalculate all grid connections
            // This is required because we have updated the walkability of some nodes
            pfGraph.GetNodes(node => pfGraph.CalculateConnections((GridNodeBase)node));

            // Update the 'area' information in the graph.
            // This is required because we have updated the connectivity of the graph
            ctx.QueueFloodFill();

        }));

        AstarPath.active.Scan();

        SamplePlayerBehavior.Player.Initialize();


        //DebugDraw.DualMapToConsole(map, kWrapper.ReturnMap());

        //VisualizeMap(kWrapper.ReturnMap());
        //BuildMap(map);
    }

I can also call my own pathfinding in a single step:

transform.Fixed2DPosition().FindPathTo(playerPosition, out playerPath)

Finally, I don’t have to update the walkability of nodes in my own solution.

I also found the documentation for A* PP to be all over the place…while I did get it working, it was rather confusing as to what was happening. All of this said, there are benefits to A* PP vs my own - namely, A*PP does not generate garbage as my own solution does.

I assume the direction of this conversation is such that Unity may be planning its own 2d pathfinding solutions. The A* algorithm would be ideal for grid-based 2d games. My problem with A* PP is mostly documentation - it is very big, difficult to use, but gives great results.

While I have the ear of the gods, unrelated, but I would love a way to “move” gameobjects between tile instances AKA: assign the tile GO an existing one. I have tiles in my project merely for vis purposes in the editor, but the gameobjects tied to the have my version of “colliders”. Unfortunately, to “move” tiles I have to delete an old tile and create it again in a new spot. A method such as TileMap.MoveTile(); would solve this if the tile associated data is not destroyed.

I need pathfinding to work with side-scrolling 2D game that uses physics. So that navmesh agents could use navmesh while obeying gravity that is not perpendicular to the navmesh plane.

2 Likes

Curious has there been any development on a 2d pathfinding system for unity. The post below here describes exactly what I am looking for so I thought I would ask.

Hi, @unity_B1HQdTCx5M06Uw

NavMeshPlus now works with tiled and non-tiled worlds. It can generate mesh from tilemap squares, or tile’s sprite shapes, or collider2d (thanks to Mel).

More you can find on wiki page HOW TO · h8man/NavMeshPlus Wiki · GitHub
and readme https://github.com/h8man/NavMeshPlus