How to deal with the new tiles type (unsafe span)? Removing nodes marked as unwalkable

In versions previous to 5.0 I have used the following code to remove nodes tagged as unwalkable. This no longer works as it appears tiles have been changed to an unmanaged memory type.

// Destroy nodes tagged not walkable
                foreach (NavmeshTile tile in recastGraphs[r].GetTiles()) {
                    // Filter the node and triangle arrays and keep only the nodes that you want to keep
                    var newNodes = new List<TriangleMeshNode>();
                    var newTris = new List<int>();
                    for (int i = tile.nodes.Length - 1; i >= 0; i--) {
                        if (!tile.nodes[i].Walkable) {
                            tile.nodes[i].Destroy();
                        }
                        else {
                            // Keep this node
                            newNodes.Add(tile.nodes[i]);
                            newTris.Add(tile.tris[i * 3 + 0]);
                            newTris.Add(tile.tris[i * 3 + 1]);
                            newTris.Add(tile.tris[i * 3 + 2]);
                        }
                    }
                    tile.nodes = newNodes.ToArray();
                    tile.tris = newTris.ToArray();
                    tile.bbTree.RebuildFrom(tile);
                }

So the line “tile.tris = newTris,ToArray();” fails and “bbTree.RebuildFrom” does not appear to exist any more. Not sure how to do the managed / unmanaged conversion and other implications.

Hi

I’d recommend that you do something like this:

recastGraph.StartBatchTileUpdate();
foreach (var tile in recastGraph.GetTiles()) {
    var trianglesToKeep = new List<int>();
    for (int i = 0; i < tile.nodes.Length; i++) {
        var node = tile.nodes[i];
        if (node.Walkable) {
            trianglesToKeep.Add(tile.tris[i*3+0]);
            trianglesToKeep.Add(tile.tris[i*3+1]);
            trianglesToKeep.Add(tile.tris[i*3+2]);
       }
    }
    recastGraph.ReplaceTile(tile.x, tile.z, tile.verts, trianglesToKeep.ToArray());
}
recastGraph.EndBatchTileUpdate();

Note: I haven’t tested this code, but I think something like it should work.

ReplaceTile is expecting Int3[] for the verts so I tried this to convert:

Int3[] newVerts = new Int3[tile.verts.Length];
for (int i = 0; i < tile.verts.Length; i++) {
     newVerts[i]= tile.verts[i];
}
recastGraphs[r].ReplaceTile(tile.x, tile.z, newVerts, trianglesToKeep.ToArray());

I think it is removing the unwalkable nodes ok but the results are odd. Original graph, unwalkable nodes in grey:

Graph after running this code:

Right. I think you’ll need to do:

var offset = (Int3) new Vector3((tile.x * graph.TileWorldSizeX), 0, (tile.z * graph.TileWorldSizeZ));
Int3[] newVerts = new Int3[tile.vertsInGraphSpace.Length];
for (int i = 0; i < tile.vertsInGraphSpace.Length; i++) {
     newVerts[i]= tile.vertsInGraphSpace[i] - offset;
}

That seems to work great. Thanks!

1 Like

In case this is useful to anyone else here is the full code. This is a helper for removing unwanted areas from tiled recast graphs.

using System.Collections.Generic;
using UnityEngine;

namespace Pathfinding
{
    public class PathRemoveUnconnected : GraphModifier
    {
        // Remove unconnected areas in tiled recast graphs that are not relevant
        // After graphs are scanned, this will mark all nodes that are not relevant as not walkable and then remove them
        // Desired functionality is similar to relevantGraphSurface but without having to place objects in each tile
        // Usage: place an empty GameObject inside areas you want to keep and add these to the areaMarkers array

        public Transform[] areaMarkers;
        Vector3[] connectedPoints;
        HashSet<GraphNode> connectedNodes = new HashSet<GraphNode>();
        List<GraphNode> toSearch = new List<GraphNode>();
        GraphNode nearestNode;
        GraphNode searchNode;
        int removedNodeCounter;

        public override void OnPostScan()
        {
            Debug.Log("Graphs-->OnPostScan");
            // Get connected points from area markers
            connectedPoints = new Vector3[areaMarkers.Length];
            for (int x = 0; x < areaMarkers.Length; x++) {
                connectedPoints[x] = areaMarkers[x].transform.position;
            }
            // Create a list of the recast graphs in scene
            List<RecastGraph> recastGraphs = new List<RecastGraph>();
            foreach (RecastGraph r in AstarPath.active.data.FindGraphsOfType(typeof(RecastGraph))) {
                recastGraphs.Add(r);
            }
            if (recastGraphs.Count == 0) return;
            // Process each graph
            toSearch.Clear();
            connectedNodes.Clear();
            for (int r = 0; r < recastGraphs.Count; r++) {
                // Get the nearest node to each area marker and add to the search
                for (int x = 0; x < connectedPoints.Length; x++) {
                    nearestNode = recastGraphs[r].GetNearest(connectedPoints[x], NNConstraint.Walkable).node;
                    if (nearestNode != null) {
                        connectedNodes.Add(nearestNode);
                        toSearch.Add(nearestNode);
                    }
                }
                // Go through all connected nodes and add them to the connected list
                while (toSearch.Count > 0) {
                    searchNode = toSearch[toSearch.Count - 1];
                    toSearch.RemoveAt(toSearch.Count - 1);
                    searchNode.GetConnections(n =>
                    {
                        if (n.Walkable && !connectedNodes.Contains(n)) {
                            toSearch.Add(n);
                            connectedNodes.Add(n);
                        }
                    });
                }
                // Mark unconnected nodes as not walkable
                recastGraphs[r].GetNodes(node =>
                {
                    if (!connectedNodes.Contains(node)) {
                        node.Walkable = false;
                        node.ClearConnections(true);
                    }
                });
                // Update each tile and discard non walkable nodes
                removedNodeCounter = 0;
                recastGraphs[r].StartBatchTileUpdate();
                foreach (var tile in recastGraphs[r].GetTiles()) {
                    var trianglesToKeep = new List<int>();
                    for (int i = 0; i < tile.nodes.Length; i++) {
                        var node = tile.nodes[i];
                        if (node.Walkable) {
                            trianglesToKeep.Add(tile.tris[i * 3 + 0]);
                            trianglesToKeep.Add(tile.tris[i * 3 + 1]);
                            trianglesToKeep.Add(tile.tris[i * 3 + 2]);
                        }
                        else {
                            removedNodeCounter++;
                        }
                    }
                    var offset = (Int3)new Vector3((tile.x * recastGraphs[r].TileWorldSizeX), 0, (tile.z * recastGraphs[r].TileWorldSizeZ));
                    Int3[] newVerts = new Int3[tile.verts.Length];
                    for (int i = 0; i < tile.verts.Length; i++) {
                        newVerts[i] = tile.vertsInGraphSpace[i] - offset;
                    }
                    recastGraphs[r].ReplaceTile(tile.x, tile.z, newVerts, trianglesToKeep.ToArray());
                }
                recastGraphs[r].EndBatchTileUpdate();
                // Info
                Debug.Log("Graph #" + r + ", Tile count: " + recastGraphs[r].GetTiles().Length + ", Nodes removed: " + removedNodeCounter);
            }
        }
    }
}
3 Likes

Note for the above: Newer versions of Astar now apply navmesh cuts in the editor. The info from the cuts will be missing when PathRemoveUnconnected() is run OnPostScan(). My simple fix for this is to disable any navmesh cuts in OnPreScan(), and then revert them back in OnPostScan() after the pruning. Another option may be to analyze the pre cut data.

1 Like