stuck in while loop if class is changed to struct

I’m not really sure anyone can help, but I didn’t want to abandon this pursuit before asking, so… here goes nothing I suppose.

I’m going to attempt to convert sebastian lague’s astar tutorial to jobs. I almost have it figured out except the Node class is a class and not a struct. If I want to be able to pass it through jobs, it has to be a struct.

Here is the main issue: I have a while loop that checks neighbors to see/set the nodes fcost, gcost, and hcosts. Whenever I switch from class Nodes to struct Nodes, Unity hangs in this while loop.

I think the problem stems from structs having to pass by value and the code must not be consistent with that somewhere. I tried to debug log values from the nodes, suspecting they aren’t actually being set, but they seem to print the right values and stuff. But still, the path just never finishes.

I’m stumped. Does anybody out there see anything obvious that would go hoorribly wrong with my code when switching the Nodes from classes to structs?

Node:

using UnityEngine;

public class Node {
    public bool _walkable;
    public Vector3 _worldPosition;
    public float _gCost, _hCost;
    public int _gridX, _gridY, _gridZ;
    public int _parent;
    public int _movementPenalty;
    public int _index;

    public float fCost {
        get { return _gCost + _hCost; }
    }

    public int CompareTo(Node nodeToCompare) {
        int compare = fCost.CompareTo(nodeToCompare.fCost);
        if (compare == 0) {
            compare = _hCost.CompareTo(nodeToCompare._hCost);
        }
        return -compare;
    }
    public static bool operator ==(Node x, Node y) {
        return x._index == y._index;
    }
    public static bool operator !=(Node x, Node y) {
        return !(x._index == y._index);
    }
}

Pathfinding (where Unity is freezing):

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Unity.Mathematics;
using UnityEngine;
public class Pathfinding : MonoBehaviour {
    public bool debugPath;
    PathRequestManager requestManager;
    Grid grid;
    Stopwatch sw = new Stopwatch();
    List<Node> openSet;
    List<int> neighbors;
    HashSet<Node> closedSet;
    Vector3[] waypoints;

    void Awake() {
        requestManager = GetComponent<PathRequestManager>();
        grid = GetComponent<Grid>();
        neighbors = new List<int>();
        openSet = new List<Node>();
        closedSet = new HashSet<Node>();
    }

    public void FindPath(Vector3 startPos, Vector3 targetPos) {
        if (debugPath) {
            sw = new Stopwatch();
            sw.Start();
        }
        bool pathSuccess = false;
        int startNodeId = grid.NodeFromWorldPoint(startPos);
        int endNodeId = grid.NodeFromWorldPoint(targetPos);
        Node startNode = grid.grid[startNodeId];
        Node targetNode = grid.grid[endNodeId];
        openSet.Clear();
        closedSet.Clear();
        neighbors.Clear();
        openSet.Add(startNode);
        while (openSet.Count > 0) {
            Node currentNode = openSet[0];
            for (int i = 0; i < openSet.Count; i++) {
                if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i]._hCost < currentNode._hCost) {
                    currentNode = openSet[i];
                }
            }
            openSet.Remove(currentNode);
            closedSet.Add(currentNode);
            if (currentNode == targetNode) {
                if (debugPath) {
                    sw.Stop();
                    print("path found: " + sw.ElapsedMilliseconds + " ms");
                }
                pathSuccess = true;
                break;
            }
            neighbors = grid.GetNeighbours(currentNode);
            ProcessNeighbors(currentNode, targetNode);
        }
        if (pathSuccess) {
            waypoints = RetracePath(startNode, targetNode);
        }
        requestManager.FinishedProcessingPath(waypoints, pathSuccess);
    }

    public void ProcessNeighbors(Node currentNode, Node targetNode) {
        for (int i = 0; i < neighbors.Count; i++) {
            Node neighbor = grid.grid[neighbors[i]];
            if (!neighbor._walkable || closedSet.Contains(neighbor)) {
                continue;
            }
            float newMovementCostToNeighbour = currentNode._gCost + GetDistance(currentNode, neighbor) + neighbor._movementPenalty;
            if (newMovementCostToNeighbour < neighbor._gCost || !openSet.Contains(neighbor)) {
                neighbor._gCost = newMovementCostToNeighbour;
                neighbor._hCost = GetDistance(neighbor, targetNode);
                neighbor._parent = currentNode._index;
                if (!openSet.Contains(neighbor)) {
                    openSet.Add(neighbor);
                }
            }
        }
    }

    Vector3[] RetracePath(Node startNode, Node endNode) {
        List<Node> path = new List<Node>();
        Node currentNode = endNode;
        while (currentNode != startNode) {
            path.Add(currentNode);
            int nodeid = currentNode._parent;
            currentNode = grid.grid[nodeid];
        }
        Vector3[] waypoints = SimplifyPath(path);
        Array.Reverse(waypoints);
        return waypoints;
    }

    Vector3[] SimplifyPath(List<Node> path) {
        List<Vector3> waypoints = new List<Vector3>();
        Vector3 directionOld = Vector3.zero;
        for (int i = 1; i < path.Count; i++) {
            float x = path[i - 1]._gridX - path[i]._gridX;
            float y = path[i - 1]._gridY - path[i]._gridY;
            float z = path[i - 1]._gridZ - path[i]._gridZ;
            Vector3 directionNew = new Vector3(x, y, z);
            if (directionNew != directionOld) {
                waypoints.Add(path[i]._worldPosition);
            }
            directionOld = directionNew;
        }
        return waypoints.ToArray();
    }

    float GetDistance(Node nodeA, Node nodeB) {
        return math.distance(nodeA._worldPosition, nodeB._worldPosition);
    }
}

Grid:

using System.Collections.Generic;
using UnityEngine;

public class Grid : MonoBehaviour {
    public Vector3 gridWorldSize;
    public float nodeRadius;
    public TerrainType[] walkableRegions;
    public LayerMask unwalkableMask;
    public bool displayGridGizmos, onlyDisplayWalkable, onlyDisplayUnwalkable;
    public Node[] grid;
    float nodeDiameter;
    int gridSizeX, gridSizeY, gridSizeZ;
    LayerMask walkableMask;
    Dictionary<int, int> walkableRegionsDictionary = new Dictionary<int, int>();
    public float refreshRate;
    float refreshTimer;
    List<int> neighbours;
    public int MaxSize {
        get {
            return gridSizeX * gridSizeY * gridSizeZ;
        }
    }
    void Awake() {
        neighbours = new List<int>();
        nodeDiameter = nodeRadius * 2f;
        gridSizeX = Mathf.RoundToInt(gridWorldSize.x / nodeDiameter);
        gridSizeY = Mathf.RoundToInt(gridWorldSize.y / nodeDiameter);
        gridSizeZ = Mathf.RoundToInt(gridWorldSize.z / nodeDiameter);
        foreach (TerrainType region in walkableRegions) {
            walkableMask.value |= region.terrainMask.value;
            walkableRegionsDictionary.Add((int)Mathf.Log(region.terrainMask.value, 2), region.terrainPenalty);
        }
        CreateGrid();
    }

    void Update() {
        if (refreshRate != 0) {
            refreshTimer += Time.deltaTime;
            if (refreshTimer >= Mathf.Abs(refreshRate)) {
                refreshTimer = 0;
                CreateGrid();
            }
        }
    }

 
    void CreateGrid() {
        grid = new Node[gridSizeX * gridSizeY * gridSizeZ];
        Vector3 leftEdge = (Vector3.left * gridWorldSize.x / 2);
        Vector3 bottomEdge = (Vector3.down * gridWorldSize.y / 2);
        Vector3 backEdge = (Vector3.back * gridWorldSize.z / 2);
        Vector3 worldBottomLeft = transform.position + leftEdge + bottomEdge + backEdge;
        for (int i = 0; i < grid.Length; i++) {
            int idx = i;
            int z = idx / (gridSizeX * gridSizeY);
            idx -= (z * gridSizeX * gridSizeY);
            int y = idx / gridSizeX;
            int x = idx % gridSizeX;
            Vector3 worldPoint = worldBottomLeft +
            Vector3.right * (x * nodeDiameter + nodeRadius) +
            Vector3.up * (y * nodeDiameter + nodeRadius) +
            Vector3.forward * (z * nodeDiameter + nodeRadius);
            bool walkable = !(Physics.CheckSphere(worldPoint, nodeRadius, unwalkableMask));
            int movementPenalty = 0;
            //raycast
            if (walkable) {
                Ray ray = new Ray(worldPoint + Vector3.up * 50, Vector3.down);
                RaycastHit hit;
                if (Physics.Raycast(ray, out hit, 100, walkableMask)) {
                    walkableRegionsDictionary.TryGetValue(hit.collider.gameObject.layer, out movementPenalty);
                }
            }
            grid[i] = new Node() {

            };
            grid[i]._walkable = walkable;
            grid[i]._worldPosition = worldPoint;
            grid[i]._gridX = x;
            grid[i]._gridY = y;
            grid[i]._gridZ = z;
            grid[i]._movementPenalty = movementPenalty;
            grid[i]._index = i;
        }
    }

    public int GetIdFromCoord(int x, int y, int z) {
        return (z * gridSizeX * gridSizeY) + (y * gridSizeX) + x;
    }

    public int[] to3D(int idx) {
        int z = idx / (gridSizeX * gridSizeY);
        idx -= (z * gridSizeX * gridSizeY);
        int y = idx / gridSizeX;
        int x = idx % gridSizeX;
        return new int[] { x, y, z };
    }

    public List<int> GetNeighbours(Node node) {
        neighbours.Clear();
        for (int x = -1; x <= 1; x++) {
            for (int y = -1; y <= 1; y++) {
                for (int z = -1; z <= 1; z++) {
                    if (x == 0 && y == 0 && z == 0) {
                        continue;
                    }
                    int checkX = node._gridX + x;
                    int checkY = node._gridY + y;
                    int checkZ = node._gridZ + z;
                   if (checkX >= 0 && checkX < gridSizeX && checkY >= 0 && checkY < gridSizeY && checkZ >= 0 && checkZ < gridSizeZ) {
                        int id = GetIdFromCoord(checkX, checkY, checkZ);
                        neighbours.Add(id);
                    }
                }
            }
        }
        return neighbours;
    }


    public int NodeFromWorldPoint(Vector3 worldPosition) {
        float percentX = (worldPosition.x + gridWorldSize.x / 2) / gridWorldSize.x;
        float percentY = (worldPosition.y + gridWorldSize.y / 2) / gridWorldSize.y;
        float percentZ = (worldPosition.z + gridWorldSize.z / 2) / gridWorldSize.z;
        percentX = Mathf.Clamp01(percentX);
        percentY = Mathf.Clamp01(percentY);
        percentZ = Mathf.Clamp01(percentZ);
        int x = Mathf.RoundToInt((gridSizeX - 1) * percentX);
        int y = Mathf.RoundToInt((gridSizeY - 1) * percentY);
        int z = Mathf.RoundToInt((gridSizeZ - 1) * percentZ);
        int id = GetIdFromCoord(x, y, z);
        return grid[id]._index;
    }
    void OnDrawGizmos() {
        Gizmos.DrawWireCube(transform.position, gridWorldSize);
        if (grid != null && displayGridGizmos) {
            foreach (Node n in grid) {
                bool walkable = n._walkable;
                //only do this if both settings arent turned on. if they are, ignore it.
                if (!(onlyDisplayUnwalkable && onlyDisplayWalkable)) {
                    if ((walkable && onlyDisplayUnwalkable) || (!walkable && onlyDisplayWalkable)) {
                        continue;
                    }
                }

                Gizmos.color = walkable ? Color.white : Color.red;
                Gizmos.DrawWireCube(n._worldPosition, Vector3.one * (nodeDiameter - 0.1f));
            }
        }
    }
    [System.Serializable]
    public class TerrainType {
        public LayerMask terrainMask;
        public int terrainPenalty;
    }
}

Other scripts

PathRequestManager:

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

public class PathRequestManager : MonoBehaviour {

    Queue<PathRequest> pathRequestQueue = new Queue<PathRequest>();
    PathRequest currentPathRequest;

    static PathRequestManager instance;
    Pathfinding pathfinding;

    bool isProcessingPath;
    Coroutine pathingCoroutine;

    void Awake() {
        instance = this;
        pathfinding = GetComponent<Pathfinding>();
    }

    public static void RequestPath(Vector3 pathStart, Vector3 pathEnd, Action<Vector3[], bool> callback) {
        PathRequest newRequest = new PathRequest(pathStart, pathEnd, callback);
        instance.pathRequestQueue.Enqueue(newRequest);
        instance.TryProcessNext();
    }

    void TryProcessNext() {
        if (!isProcessingPath && pathRequestQueue.Count > 0) {
            currentPathRequest = pathRequestQueue.Dequeue();
            isProcessingPath = true;
            // if (pathingCoroutine != null) {
            //     StopCoroutine(pathingCoroutine);
            // }
            pathfinding.FindPath(currentPathRequest.pathStart, currentPathRequest.pathEnd);
        }
    }

    public void FinishedProcessingPath(Vector3[] path, bool success) {
        currentPathRequest.callback(path, success);
        isProcessingPath = false;
        TryProcessNext();
    }

    struct PathRequest {
        public Vector3 pathStart;
        public Vector3 pathEnd;
        public Action<Vector3[], bool> callback;

        public PathRequest(Vector3 _start, Vector3 _end, Action<Vector3[], bool> _callback) {
            pathStart = _start;
            pathEnd = _end;
            callback = _callback;
        }

    }
}

Unit:

using UnityEngine;
using System.Collections;

public class Unit : MonoBehaviour {
    public Transform target;
    public float acceleration = 1, maxSpeed = 10f;
    public bool rescan, lookAtTarget, lookAtMoveDir;
    [Range(0, 1)] public float rotationSmoothing = 0.5f;
    public float rescanFrequency = 2f;
    public bool pathOnStart, useRigidBody;
    public float waypointRadius = 0.2f, finalWaypointRadius = 0.15f;
    public float groundCheckDistance = 1f, groundCheckRadius = 0.5f;
    Vector3[] path;
    int targetIndex;
    float frequencyTimer;
    Vector3 currentWaypoint;
    public bool debugPath;
    Coroutine co;
    Grid grid;
    Quaternion endRotation;
    Vector3 velocity;
    Rigidbody rb;
    void Awake() {
        grid = FindObjectOfType<Grid>();
        rb = GetComponent<Rigidbody>();
    }

    void Start() {
        if (pathOnStart) {
            PathRequestManager.RequestPath(transform.position, target.position, OnPathFound);
        }
    }
    public void OnPathFound(Vector3[] newPath, bool pathSuccessful) {
        if (pathSuccessful && gameObject.activeInHierarchy) {
            path = newPath;
            targetIndex = 0;
            if (co != null) {
                StopCoroutine(co);
            }
            co = StartCoroutine(FollowPath());
        }
    }

    void FixedUpdate() {
        // if (Input.GetKeyDown(KeyCode.P)){
        //     PathRequestManager.RequestPath(transform.position, target.position, OnPathFound);
        // }

        if (target && target.gameObject.activeInHierarchy && rescan) {
            frequencyTimer += Time.fixedDeltaTime;
            if (frequencyTimer >= rescanFrequency) {
                frequencyTimer = 0;
                PathRequestManager.RequestPath(transform.position, target.position, OnPathFound);

            }
            if (!target) {
                print("No target.");
            }
            Vector3 dir = Vector3.zero;
            if (lookAtTarget) {
                dir = (target.position - transform.position).normalized;
            }
            if (lookAtMoveDir) {
                dir = rb.velocity.normalized;
            }
            endRotation = dir != Vector3.zero ? Quaternion.LookRotation(dir) : Quaternion.identity;
        }
        //velocity = Vector3.ClampMagnitude(velocity, maxSpeed);
        transform.rotation = Quaternion.Slerp(transform.rotation, endRotation, rotationSmoothing * Time.fixedDeltaTime);

        if (useRigidBody) {
            if (rb && rb.velocity.sqrMagnitude < maxSpeed) {
                rb.AddForce(velocity);
            }
        } else {
            transform.position += velocity * Time.fixedDeltaTime;
        }

    }

    IEnumerator FollowPath() {
        if (path.Length > 0) {
            currentWaypoint = path[0];
            bool endLoop = false;
            while (endLoop == false) {
                float dst = waypointRadius;
                if (currentWaypoint == path[path.Length - 1]) {
                    dst = finalWaypointRadius;
                }
                if (Vector3.Distance(transform.position, currentWaypoint) < dst) {
                    targetIndex++;
                    if (targetIndex >= path.Length) {
                        velocity = Vector3.zero;
                        endLoop = true;
                        yield break;
                    }
                    currentWaypoint = path[targetIndex];
                }
                Vector3 dir = (currentWaypoint - transform.position).normalized;
                float mass = 1f;
                if (rb) {
                    mass = rb.mass;
                }
                velocity = dir * acceleration * mass;
                yield return null;

            }
        }
    }
    public void OnDrawGizmos() {
        if (path != null && grid && debugPath) {
            for (int i = targetIndex; i < path.Length; i++) {
                Gizmos.color = Color.black;
                Gizmos.DrawWireCube(path[i], Vector3.one * grid.nodeRadius);

                if (i == targetIndex) {
                    Gizmos.DrawLine(transform.position, path[i]);
                } else {
                    Gizmos.DrawLine(path[i - 1], path[i]);
                }
            }
        }
    }
}

As a side note, i’m also curious if anyone has tried to convert sebastian lague’s tutorial to jobs before already…?

https://www.gamedev.net/forums/topic/703851-doing-astar-pathing-finding-in-threads/ Came across this thread. Gonna try to not change the nodes in the grid, and instead give each unit their own copy to work with. hopefully that’ll help it not freeze?

I haven’t looked through all your code as I generally know how AStar works (I’ve implemented it myself from scratch several times in different languages). Though one key point of AStar is that it needs to update the state of a cell / node as it closes the nodes. Structs are value types and you currently pass them around as if they are reference types (since that’s what the code originally did). Though you now work on temporary copies of those nodes on the stack. So none of the algorithm would work. Furthermore the way the open and closed lists are implemented also rely on the fact that you can identify a node based on its reference. Comparing two reference types means you compare their reference value. So in some sense you look if the two references point to the same node instance. When you have value types this does not work anymore. When you compare structs, you actually compare all their content. That has several problems in this case. If you actually modifed one of the copies of a certain node, this modified copy would no longer be equal to the other copies. So seaching for a modified node in the open or closed list would fail.

In short this does not work at all ^^. Nothing in the AStar algorithm can actually be parallelized as it’s a sequential algorithm. Of course AStar can be executed on a seperate thread so the main thread could do something else in the meantime. Though other than that there’s not much you can do about that.

If you only ever have a single AStar search at a time, using classes like that is not an issue at all as long as you communicate correctly which thread is allowed to access the data at which time. Usually you have the main thread setup a search and then hand the data over to the background thread which does its thing. During that process no other thread is allowed to access any of the used data. Once finished you signal the completion and the main thread can take over again.

If you need / want to do several searches at the same time, you need to have several seperate copies of the data. Again, AStar’s main feature is that it updates the G and F cost of a node as it iterates through the nodes. So if you do 2 seaches at the same time, you need a complete copy of all the nodes for each thread.

4 Likes