advanced AI & Pathfinding

A* Grid Pathfinding

Grid-based A* pathfinding without NavMesh. Supports diagonal movement, weighted nodes, and debug visualization.

Unity 2022.3+ · 4.2 KB · AStarPathfinding.cs

How to Use

1

Attach AStarPathfinding to a manager object

2

Set grid size, cell size, and grid origin position

3

Assign obstacle layer to detect unwalkable cells

4

Call GenerateGrid() to build the walkability map

5

Find path: var path = AStarPathfinding.Instance.FindPath(start, end)

6

Path returns a list of Vector3 waypoints to follow

7

Enable debug to visualize walkable (green) and blocked (red) cells

8

Call GenerateGrid() again if the environment changes

Features

  • Grid-based A* pathfinding without relying on Unity NavMesh
  • Support for diagonal movement with proper heuristic calculation
  • Weighted nodes for terrain cost variation
  • LayerMask-based obstacle detection using Physics.CheckBox
  • Editor gizmo visualization showing walkable and blocked cells
  • Regenerate grid at runtime when the environment changes

When to Use This

Ideal for 2D top-down games, tile-based RPGs, RTS games, and tower defense titles where NavMesh isn't suitable. Use when you need grid-aligned movement or when obstacles are placed dynamically. Pair with Waypoint Patrol AI or Enemy Chase AI for enemy navigation.

Common Mistakes

The gridOrigin must match where your level starts in world space — if it's offset, the grid won't align with your tiles. The obstacleLayer must include all layers with blocking colliders; missing layers will leave walls marked as walkable. For large grids (100x100+), pathfinding can be slow per frame — consider caching paths or spreading over multiple frames.

Source Code

AStarPathfinding.cs
C#
using UnityEngine;
using System.Collections.Generic;

/// <summary>
/// Grid-based A* pathfinding. Works without Unity NavMesh.
/// Ideal for 2D top-down games and tile-based environments.
/// </summary>
public class AStarPathfinding : MonoBehaviour
{
    public static AStarPathfinding Instance { get; private set; }

    [Header("Grid")]
    [SerializeField] private int gridWidth = 50;
    [SerializeField] private int gridHeight = 50;
    [SerializeField] private float cellSize = 1f;
    [SerializeField] private Vector3 gridOrigin = Vector3.zero;
    [SerializeField] private LayerMask obstacleLayer;

    [Header("Settings")]
    [SerializeField] private bool allowDiagonal = true;
    [SerializeField] private bool showDebug = true;

    private Node[,] grid;

    private class Node
    {
        public int x, y;
        public bool walkable;
        public float weight;
        public float gCost, hCost;
        public Node parent;

        public float fCost => gCost + hCost;
        public Vector3 worldPosition;

        public Node(int x, int y, bool walkable, Vector3 pos, float weight = 1f)
        {
            this.x = x;
            this.y = y;
            this.walkable = walkable;
            this.worldPosition = pos;
            this.weight = weight;
        }
    }

    private void Awake()
    {
        if (Instance == null)
            Instance = this;
        else
        {
            Destroy(gameObject);
            return;
        }

        GenerateGrid();
    }

    /// <summary>
    /// Regenerate the walkability grid (call after terrain changes).
    /// </summary>
    public void GenerateGrid()
    {
        grid = new Node[gridWidth, gridHeight];

        for (int x = 0; x < gridWidth; x++)
        {
            for (int y = 0; y < gridHeight; y++)
            {
                Vector3 worldPos = gridOrigin + new Vector3(
                    x * cellSize + cellSize * 0.5f,
                    0f,
                    y * cellSize + cellSize * 0.5f
                );

                bool walkable = !Physics.CheckBox(
                    worldPos,
                    Vector3.one * cellSize * 0.4f,
                    Quaternion.identity,
                    obstacleLayer
                );

                grid[x, y] = new Node(x, y, walkable, worldPos);
            }
        }
    }

    /// <summary>
    /// Find a path from start to end world positions.
    /// Returns a list of world positions, or null if no path found.
    /// </summary>
    public List<Vector3> FindPath(Vector3 startPos, Vector3 endPos)
    {
        Node startNode = GetNodeFromWorld(startPos);
        Node endNode = GetNodeFromWorld(endPos);

        if (startNode == null || endNode == null || !endNode.walkable)
            return null;

        List<Node> openSet = new List<Node> { startNode };
        HashSet<Node> closedSet = new HashSet<Node>();

        // Reset costs
        for (int x = 0; x < gridWidth; x++)
            for (int y = 0; y < gridHeight; y++)
            {
                grid[x, y].gCost = float.MaxValue;
                grid[x, y].parent = null;
            }

        startNode.gCost = 0;
        startNode.hCost = Heuristic(startNode, endNode);

        while (openSet.Count > 0)
        {
            Node current = GetLowestFCost(openSet);

            if (current == endNode)
                return RetracePath(startNode, endNode);

            openSet.Remove(current);
            closedSet.Add(current);

            foreach (Node neighbor in GetNeighbors(current))
            {
                if (!neighbor.walkable || closedSet.Contains(neighbor))
                    continue;

                float moveCost = current.gCost + Heuristic(current, neighbor) * neighbor.weight;

                if (moveCost < neighbor.gCost)
                {
                    neighbor.gCost = moveCost;
                    neighbor.hCost = Heuristic(neighbor, endNode);
                    neighbor.parent = current;

                    if (!openSet.Contains(neighbor))
                        openSet.Add(neighbor);
                }
            }
        }

        return null; // No path found
    }

    private Node GetLowestFCost(List<Node> nodes)
    {
        Node lowest = nodes[0];
        for (int i = 1; i < nodes.Count; i++)
        {
            if (nodes[i].fCost < lowest.fCost ||
                (nodes[i].fCost == lowest.fCost && nodes[i].hCost < lowest.hCost))
            {
                lowest = nodes[i];
            }
        }
        return lowest;
    }

    private float Heuristic(Node a, Node b)
    {
        int dx = Mathf.Abs(a.x - b.x);
        int dy = Mathf.Abs(a.y - b.y);

        if (allowDiagonal)
            return Mathf.Min(dx, dy) * 1.414f + Mathf.Abs(dx - dy);
        else
            return dx + dy;
    }

    private List<Node> GetNeighbors(Node node)
    {
        List<Node> neighbors = new List<Node>();
        int[] dx = { 0, 1, 0, -1 };
        int[] dy = { 1, 0, -1, 0 };

        for (int i = 0; i < 4; i++)
        {
            int nx = node.x + dx[i];
            int ny = node.y + dy[i];
            if (nx >= 0 && nx < gridWidth && ny >= 0 && ny < gridHeight)
                neighbors.Add(grid[nx, ny]);
        }

        if (allowDiagonal)
        {
            int[] ddx = { 1, 1, -1, -1 };
            int[] ddy = { 1, -1, 1, -1 };
            for (int i = 0; i < 4; i++)
            {
                int nx = node.x + ddx[i];
                int ny = node.y + ddy[i];
                if (nx >= 0 && nx < gridWidth && ny >= 0 && ny < gridHeight)
                    neighbors.Add(grid[nx, ny]);
            }
        }

        return neighbors;
    }

    private List<Vector3> RetracePath(Node start, Node end)
    {
        List<Vector3> path = new List<Vector3>();
        Node current = end;

        while (current != start)
        {
            path.Add(current.worldPosition);
            current = current.parent;
        }

        path.Reverse();
        return path;
    }

    private Node GetNodeFromWorld(Vector3 worldPos)
    {
        Vector3 local = worldPos - gridOrigin;
        int x = Mathf.FloorToInt(local.x / cellSize);
        int y = Mathf.FloorToInt(local.z / cellSize);

        if (x < 0 || x >= gridWidth || y < 0 || y >= gridHeight)
            return null;

        return grid[x, y];
    }

#if UNITY_EDITOR
    private void OnDrawGizmos()
    {
        if (!showDebug || grid == null) return;

        for (int x = 0; x < gridWidth; x++)
        {
            for (int y = 0; y < gridHeight; y++)
            {
                Gizmos.color = grid[x, y].walkable
                    ? new Color(0f, 1f, 0f, 0.1f)
                    : new Color(1f, 0f, 0f, 0.3f);
                Gizmos.DrawCube(grid[x, y].worldPosition, Vector3.one * cellSize * 0.9f);
            }
        }
    }
#endif
}