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