How to connect to neighbouring nodes using Astar Pathfinding?

In Unity 2D, I decided to use the astar pathfinding package to create a pathfinding AI.

I initially wanted to use a grid graph, but I want to avoid using layers to assign obstacles. So, I wrote some code (see AstarGraphCreator.cs) that created a point graph, and assigned nodes using the children of a root gameobject. The child objects have another script attached to them that destroys them depending on the value of an int. (see NodeShimmier.cs).

The code places the nodes in the correct positions, and destroys the ones I would like to destroy. But the issue is when I scan the graph, All of the nodes connect to every single other node. I would like to adjust my code so every node only connects to it’s surrounding nodes.

AstarGraphCreator.cs

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

public class AstarGraphCreator : MonoBehaviour
{
    public FloorIdentifier[] floorIdentifiers;
    public int NumberOfGraphsToInstantiate;
    public int width = 5;
    public int height = 5;
    public float spacing = 1.0f;
    public GameObject node;

    [HideInInspector]
    public int iInThisContext;
    public List<GameObject> allRoots = new List<GameObject>();

    GameObject currentNode;
    // Start is called before the first frame update
    void Start()
    {
        floorIdentifiers = FindObjectsOfType<FloorIdentifier>();
        for(int i = 0; i < floorIdentifiers.Length; i++)
        {
            if(floorIdentifiers[i].floor > NumberOfGraphsToInstantiate && floorIdentifiers[i].floor != 999)
            {
                NumberOfGraphsToInstantiate = floorIdentifiers[i].floor;
            }
        }

        NumberOfGraphsToInstantiate += 1;

       
        for (int i = 0; i < NumberOfGraphsToInstantiate; i++)
        {
            iInThisContext = i;
            //Make a point graph
            PointGraph graph = AstarPath.active.data.AddGraph(typeof(PointGraph)) as PointGraph;
            graph.name = "Floor " + i;

            //Make a empty object
            GameObject rootObj = new GameObject();
            rootObj.name = "floor " + i + " nodes";
            allRoots.Add(rootObj);

            //Set the point graph's root to the empty object
            graph.root = rootObj.transform;

            //Make a node shimmier object
            Vector3 positionToInstantiate = new Vector3(transform.position.x - (width / 2) - 0.5f, transform.position.y - (height / 2) + 0.5f, 0);

           for (int j = 0; j < height; j++)
           {
               for (int k = 0; k < width; k++)
               {
                    currentNode = Instantiate(node, positionToInstantiate, Quaternion.identity);
                    currentNode.transform.parent = rootObj.transform;
                    currentNode.GetComponent<NodeShimmier>().maxFloor = iInThisContext;
                    positionToInstantiate = new Vector3(positionToInstantiate.x + spacing, positionToInstantiate.y, 0);
               }

                positionToInstantiate = new Vector3(transform.position.x - (width / 2) - 0.5f, currentNode.transform.position.y + spacing, 0);
            }
        }
        StartCoroutine(disableAllRootObjs());
    }

    IEnumerator disableAllRootObjs()
    {
        yield return new WaitForEndOfFrame();
        for(int i = 0; i < allRoots.Count; i++)
        {
            //allRoots[i].SetActive(false);
        }
    }
   
}

NodeShimmier.cs

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

public class NodeShimmier : MonoBehaviour
{
    public int maxFloor;
    bool destroy;

    GraphNode currentNode;
    AstarPath astarPath;

    public List<GameObject> nearbyGameObjects = new List<GameObject>();


    // Start is called before the first frame update
    void Start()
    {
        astarPath = AstarPath.active;

        // Get the current node based on the node's position
        currentNode = AstarPath.active.GetNearest(transform.position).node;

        // Call a function to set up connections
        SetConnections();

        Invoke("keepAfterASecond", 0.5f);
        Invoke("destroyAfterASecond", 1);
        Invoke("SetConnections", 0.2f);
    }

    void SetConnections()
    {

        // Get direct neighbors
        GraphNode[] neighbors = GetDirectNeighbors();

        // Connect to neighbors
        foreach (GraphNode neighbor in neighbors)
        {
            // Add connection
            currentNode.AddConnection(neighbor, (uint)CalculateInt3Distance(currentNode.position, neighbor.position));
        }
    }

    private float CalculateInt3Distance(Int3 position1, Int3 position2)
    {
        float dx = position1.x - position2.x;
        float dy = position1.y - position2.y;
        float dz = position1.z - position2.z;

        return Mathf.Sqrt(dx * dx + dy * dy + dz * dz);
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.GetComponent<FloorColliderIdentifier>() == true)
        {
            if (other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor <= maxFloor || other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor == maxFloor + 1)
            {
                Debug.Log("Keeping Because representingfloor <= max floor");
                Destroy(GetComponent<Rigidbody2D>());
                Destroy(GetComponent<BoxCollider2D>());
                //Destroy(this);
                destroy = false;
            }
            else
            {
                Debug.Log("Deleting");
                destroy = true;
            }
        }
        else
        {
            Debug.Log("Keeping Because no collision");
        }


    }

    void keepAfterASecond()
    {
        if (!destroy)
        {
            Destroy(GetComponent<Rigidbody2D>());
            Destroy(GetComponent<BoxCollider2D>());
            //Destroy(this);
        }

    }

    void destroyAfterASecond()
    {
        if (destroy)
        {
            Destroy(gameObject);
        }
    }

    private GraphNode[] GetDirectNeighbors()
    {
        Vector2 currentNodePosition = new Vector2(currentNode.position.x, currentNode.position.y);
        return new GraphNode[]
        {
            GetNodeAtPosition(currentNodePosition + Vector2.up),
            GetNodeAtPosition(currentNodePosition + Vector2.down),
            GetNodeAtPosition(currentNodePosition + Vector2.left),
            GetNodeAtPosition(currentNodePosition + Vector2.right),
        };
    }

    private GraphNode GetNodeAtPosition(Vector3 position)
    {
        // Use A* Pathfinding Project's utility function to get the nearest node to a position
        return AstarPath.active.GetNearest(position).node;
    }

    void GetDaBois()
    {
        NodeShimmier[] allComponentsOfType = FindObjectsOfType<NodeShimmier>();

        List<GameObject> allGameObjectsOfType = new List<GameObject>();

        foreach (NodeShimmier component in allComponentsOfType)
        {
            if(component.maxFloor == maxFloor)
            {
                allGameObjectsOfType.Add(component.gameObject);
            }
            
        }

        foreach (GameObject obj in allGameObjectsOfType)
        {
            if (obj != this.gameObject)
            {
                float distance = Vector2.Distance(transform.position, obj.transform.position);

                if (distance <= 1)
                {
                    nearbyGameObjects.Add(obj);
                }
            }

        }
    }
}

Hi

Are you creating the nodes in a grid pattern?

Yes, I have placed them in a 2D grid pattern.

In that case, I would strongly recommend that you use a grid graph, instead.

You can adjust the walkability of all nodes manually, if you want. See Graph Updates during Runtime - A* Pathfinding Project

So, I swapped to a grid graph and rewrote my code. But all of the nodes in the graph still register as walkable, and none are set to unwalkable.

AstarGraphCreator.cs

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

public class AstarGraphCreator : MonoBehaviour
{
    public FloorIdentifier[] floorIdentifiers;
    public int NumberOfGraphsToInstantiate;
    public int width = 5;
    public int height = 5;
    public GameObject nodeShimmier;

    [HideInInspector]
    public int iInThisContext;

    GameObject currentNode;
    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(doTheCoolThing());
    }

    IEnumerator doTheCoolThing()
    {
        floorIdentifiers = FindObjectsOfType<FloorIdentifier>();
        for (int i = 0; i < floorIdentifiers.Length; i++)
        {
            if (floorIdentifiers[i].floor > NumberOfGraphsToInstantiate && floorIdentifiers[i].floor != 999)
            {
                NumberOfGraphsToInstantiate = floorIdentifiers[i].floor;
            }
        }

        NumberOfGraphsToInstantiate += 1;


        for (int i = 0; i < NumberOfGraphsToInstantiate; i++)
        {
            iInThisContext = i;

            GridGraph graph = AstarPath.active.data.AddGraph(typeof(GridGraph)) as GridGraph;
            graph.is2D = true;
            graph.collision.use2D = true;
            graph.name = "Floor " + i;

            graph.SetDimensions(width, height, 1);
            graph.center = new Vector3(0.5f, 0.5f, 0);
            AstarData.active.Scan();
            GameObject Shimmier = Instantiate(nodeShimmier, transform.position, Quaternion.identity);
            Shimmier.GetComponent<NodeShimmier>().maxFloor = iInThisContext;
            yield return new WaitForEndOfFrame();

            for (int y = -(graph.depth / 2) + 1; y < graph.depth / 2 + 1; y++)
            {
                for (int x = -(graph.width / 2) + 1; x < graph.width / 2 + 1; x++)
                {
                    Shimmier.transform.position = new Vector3(x, y, 0);
                    //Debug.Log("x = " + x + ". Y = " + y);

                    var node = graph.GetNearest(Shimmier.transform.position).node;
                    //Debug.Log(Shimmier.transform.position);

                    if (Shimmier.GetComponent<NodeShimmier>().NotCollidingWithBad)
                    {
                        node.Walkable = true;
                        Debug.Log("Node is walkable");
                    }
                    else
                    {
                        node.Walkable = false;
                        Debug.Log("Node isn't walkable");
                    }
                }
            }
            AstarData.active.Scan();
            graph.GetNodes(node => graph.CalculateConnections((GridNodeBase)node));
        }

    }
}

NodeShimmier.cs

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

public class NodeShimmier : MonoBehaviour
{
    public int maxFloor;
    public bool NotCollidingWithBad;

    public List<GameObject> nearbyGameObjects = new List<GameObject>();


    // Start is called before the first frame update
    void Start()
    {

    }

    private void Update()
    {
        //Debug.Log(NotCollidingWithBad);
    }
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.GetComponent<FloorColliderIdentifier>() == true)
        {
            if (other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor <= maxFloor || other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor == maxFloor + 1)
            {
                NotCollidingWithBad = true;
            }
            else
            {
                NotCollidingWithBad = false;
            }
        }
        
    }
}

This second call will completely re-create the graph from scratch, removing all changes you just made.

I removed the call, but now it says that the node variable is null.

Did you remove both calls, or just the second one?

Both, but if I remove just the second one, all of the nodes are registered as walkable.

You’ll need the first one to create the graph. But not the second one, as that will overwrite all your changes.

I’d try to verify that you are setting the walkability of the right nodes.

fyi. You can also access the nodes using graph.GetNode(x, y) (where x and y go from (0,0) to (width-1, depth-1). This is faster.

How would I recommend I verify that?

I often use Debug.DrawLine for this purpose.

Oh, and by the way. You should create all your graphs first, then call AstarPath.active.Scan, and then update your nodes. Currently you call Scan inside the loop, which will remove any changes you made to earlier graphs.

1 - How would I use Debug.DrawLine?
2 - Would that look something like this?

IEnumerator doTheCoolThing()
    {
        floorIdentifiers = FindObjectsOfType<FloorIdentifier>();
        for (int i = 0; i < floorIdentifiers.Length; i++)
        {
            if (floorIdentifiers[i].floor > NumberOfGraphsToInstantiate && floorIdentifiers[i].floor != 999)
            {
                NumberOfGraphsToInstantiate = floorIdentifiers[i].floor;
            }
        }

        NumberOfGraphsToInstantiate += 1;


        for (int i = 0; i < NumberOfGraphsToInstantiate; i++)
        {
            iInThisContext = i;

            GridGraph graph = AstarPath.active.data.AddGraph(typeof(GridGraph)) as GridGraph;
            gridGraphs.Add(graph);
            graph.is2D = true;
            graph.collision.use2D = true;
            graph.name = "Floor " + i;

            graph.SetDimensions(width, height, 1);
            graph.center = new Vector3(0.5f, 0.5f, 0);
            AstarData.active.Scan();
            GameObject Shimmier = Instantiate(nodeShimmier, transform.position, Quaternion.identity);
            Shimmier.GetComponent<NodeShimmier>().maxFloor = iInThisContext;
            yield return new WaitForEndOfFrame();

            for (int y = -(graph.depth / 2) + 1; y < graph.depth / 2 + 1; y++)
            {
                for (int x = -(graph.width / 2) + 1; x < graph.width / 2 + 1; x++)
                {
                    Shimmier.transform.position = new Vector3(x, y, 0);
                    //Debug.Log("x = " + x + ". Y = " + y);

                    var node = graph.GetNearest(Shimmier.transform.position).node;
                    //Debug.Log(Shimmier.transform.position);

                    if (Shimmier.GetComponent<NodeShimmier>().NotCollidingWithBad)
                    {
                        node.Walkable = true;
                        Debug.Log("Node is walkable");
                    }
                    else
                    {
                        node.Walkable = false;
                        Debug.Log("Node isn't walkable");
                    }
                }
            }


        }
        AstarData.active.Scan();

        foreach(GridGraph graph in gridGraphs)
        {
            graph.GetNodes(node => graph.CalculateConnections((GridNodeBase)node));
        }
    }
  1. E.g. Debug.DrawRay((Vector3)node.position, Vector3.up, node.Walkable ? Color.green : Color.red, 2f);

  2. No. You need to

    1. Create all graphs
    2. Scan all graphs
    3. Set the node walkability and call CalculateConnections

So, I have changed the scripts and have tried to incorporate everything mentioned. But I am still having trouble getting the nodes registered as unwalkable.

AstarGraphCreator.cs

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

public class AstarGraphCreator : MonoBehaviour
{
    public FloorIdentifier[] floorIdentifiers;
    public int NumberOfGraphsToInstantiate;
    public int width = 5;
    public int height = 5;
    public GameObject nodeShimmier;

    // Start is called before the first frame update
    void Start()
    {
        doTheCoolThing();
    }

    void doTheCoolThing()
    {
        floorIdentifiers = FindObjectsOfType<FloorIdentifier>();
        for (int i = 0; i < floorIdentifiers.Length; i++)
        {
            if (floorIdentifiers[i].floor > NumberOfGraphsToInstantiate && floorIdentifiers[i].floor != 999)
            {
                NumberOfGraphsToInstantiate = floorIdentifiers[i].floor;
            }
        }

        NumberOfGraphsToInstantiate += 1;


        for (int i = 0; i < NumberOfGraphsToInstantiate; i++)
        {

            GridGraph graph = AstarPath.active.data.AddGraph(typeof(GridGraph)) as GridGraph;
            graph.is2D = true;
            graph.collision.use2D = true;
            graph.name = "Floor " + i;

            graph.SetDimensions(width, height, 1);
            graph.Scan();

            coolThing2(graph, i);

        }
        AstarData.active.Scan();

    }

    void coolThing2(GridGraph graph, int maxFloor)
    {
        GameObject Shimmier = Instantiate(nodeShimmier, transform.position, Quaternion.identity);
        Shimmier.GetComponent<NodeShimmier>().maxFloor = maxFloor;
        for (int y = 0; y < graph.depth - 1; y++)
        {
            for (int x = 0; x < graph.width - 1; x++)
            {
                var node = graph.GetNode(x, y);
                Shimmier.transform.position = new Vector3(x - graph.width / 2 + 0.5f, y - graph.depth / 2 + 0.5f, 0);

                if (Shimmier.GetComponent<NodeShimmier>().NotCollidingWithBad)
                {
                    node.Walkable = true;
                }
                else
                {
                    node.Walkable = false;
                }

                graph.GetNodes(node => graph.CalculateConnections((GridNodeBase)node));
            }
        }

    }
}

NodeShimmier.cs

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

public class NodeShimmier : MonoBehaviour
{
    public int maxFloor;
    public bool NotCollidingWithBad;

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.GetComponent<FloorColliderIdentifier>() == true)
        {
            if (other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor <= maxFloor || other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor == maxFloor + 1)
            {
                NotCollidingWithBad = true;

            }
            else
            {
                NotCollidingWithBad = false;
            }
        }
    }
}

I have found a solution! I was trying to create all of the graphs before runtime. So, I decided to leave a few milliseconds between each node update. It will take a few seconds to actually finish generation, but nothing a basic loading screen can’t fix. Thank you for all of your help. If you have any ideas for optimization, here are my scripts.

AstarGraphCreator.cs

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

public class AstarGraphCreator : MonoBehaviour
{
    public FloorIdentifier[] floorIdentifiers;
    public int NumberOfGraphsToInstantiate;
    public int width = 5;
    public int height = 5;
    public GameObject nodeShimmier;

    // Start is called before the first frame update
    void Start()
    {
        doTheCoolThing();
    }

    void doTheCoolThing()
    {
        floorIdentifiers = FindObjectsOfType<FloorIdentifier>();
        for (int i = 0; i < floorIdentifiers.Length; i++)
        {
            if (floorIdentifiers[i].floor > NumberOfGraphsToInstantiate && floorIdentifiers[i].floor != 999)
            {
                NumberOfGraphsToInstantiate = floorIdentifiers[i].floor;
            }
        }

        NumberOfGraphsToInstantiate += 1;


        for (int i = 0; i < NumberOfGraphsToInstantiate; i++)
        {

            GridGraph graph = AstarPath.active.data.AddGraph(typeof(GridGraph)) as GridGraph;
            graph.is2D = true;
            graph.collision.use2D = true;
            graph.name = "Floor " + i;

            graph.SetDimensions(width, height, 1);
            graph.Scan();

            StartCoroutine(coolThing2(graph, i));

        }
        AstarData.active.Scan();

    }

    IEnumerator coolThing2(GridGraph graph, int maxFloor)
    {
        GameObject Shimmier = Instantiate(nodeShimmier, transform.position, Quaternion.identity);
        Shimmier.GetComponent<NodeShimmier>().maxFloor = maxFloor;
        for (int y = 0; y < graph.depth; y++)
        {
            for (int x = 0; x < graph.width; x++)
            {
                yield return new WaitForSeconds(0.25f);
                var node = graph.GetNode(x, y);
                Shimmier.transform.position = new Vector3(x - graph.width / 2 + 0.5f, y - graph.depth / 2 + 0.5f, 0);

                yield return new WaitForSeconds(0.25f);
                if (Shimmier.GetComponent<NodeShimmier>().NotCollidingWithBad)
                {
                    node.Walkable = true;
                }
                else
                {
                    node.Walkable = false;
                }

                graph.GetNodes(node => graph.CalculateConnections((GridNodeBase)node));
            }
        }

    }
}

NodeShimmier.cs

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

public class NodeShimmier : MonoBehaviour
{
    public int maxFloor;
    public bool NotCollidingWithBad;

    private void OnTriggerStay2D(Collider2D other)
    {
        if (other.gameObject.GetComponent<FloorColliderIdentifier>() == true)
        {
            if (other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor <= maxFloor || other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor == maxFloor + 1)
            {
                NotCollidingWithBad = true;
            }
            else
            {
                NotCollidingWithBad = false;
            }
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.gameObject.GetComponent<FloorColliderIdentifier>() == true)
        {
            if (other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor <= maxFloor || other.gameObject.GetComponent<FloorColliderIdentifier>().representingFloor == maxFloor + 1)
            {
                NotCollidingWithBad = false;
            }
        }
    }
}