Does MultiTargetPath handle nonplanar graphs correctly?

using A*path 5.3.3 Unity 6.0.35f1
I am using a pointNode and a very simple test case. I want to find the shortest longest path on a graph. Meaning check all possible pairs of nodes and find the pair with the longest shortest path between them. My graphs are regular rectangular grids. I have tested 2D graphs with various nodes marked walkable false with no problems. With the case of a 2x2x2 3d graph, MultitargetPath does not find the correct shortest path, but passing MultiTargets with a single target does work.

The 2x2x2 graph is the equivalent of the corners on a cube, with edges being connections.
There are 8 nodes. The longest path should be from a corner to the diagonally opposite corner. and should have 4 nodes.
In the broken case, I make 6 MultiTargets. In the first start = node[0] and the targets are nodes 1 to 7. The next multiTarget starts at node[1] and the targets are 2 to 7. The last MultiTarget starts at node[6] and the list is node[7]. This produces a longest path with 5 nodes but the path is not a shortest path.
If I make MultiTargets with one target each, so first start= Node[0] target node[1] next start node[0] and target = Node[2] etc. It produces the correct result of path with 4 nodes.

The nodes are named for their coordinates in 3D in each dimension goes from 0~1
A correct longest path found is 4 :: (0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 1)
The incorrect path found is 5 :: , (0, 1, 1), (0, 0, 1), (0, 0, 0), (1, 0, 0), (1, 1, 0)
iI does not use the connections (0,1,1) → (1,1,1) and (1,1,1) => (1,1,0)
I restart the code each time just changing the list of multiTargets checked.

I can make a test project and send a zip or link with pointers to where the A*path code is called.

Hi

Can you post the code you are using for creating the graph?

Also. Make sure path.pathsForAll is set to the appropriate value for what you want to do with the multi target paths: MultiTargetPath - A* Pathfinding Project

Code to create nodes and connections size is (2,2,2) for testing

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

[Pathfinding.Util.Preserve]
public class PathGraph2D : PointGraph { 

    [NonSerialized]
    public  PointNode[,,] nodeArray;
    [JsonMember]
    public  GraphTransform transform;
    [JsonMember]
    public  Vector3Int size3;
    public void Init(Vector3Int aSize,Vector3 pos,float scale) {
        size3 = aSize;
        nodeArray = new PointNode[size3.x, size3.y,size3.z];
        transform = new GraphTransform(Matrix4x4.TRS(pos, Quaternion.identity, Vector3.one*scale));
        Scan();
    }

    class PathGraph2DScanPromise : IGraphUpdatePromise {
        public PathGraph2D graph;
        public  PointNode[,,] nodeArray;
        public  GraphTransform transform;
        public  Vector3Int size3;

        // In this method you may run async calculations required for updating the graph.
        public IEnumerator<Unity.Jobs.JobHandle> Prepare() => null;

        public void Apply(IGraphUpdateContext ctx) {
            // Destroy previous nodes (if any)
            graph.DestroyAllNodes();
            List<PointNode> allNodes = new();
            
            for (int x = 0; x < size3.x; x++) {
                for (int y = 0; y < size3.y; y++) {
                    for (int z = 0; z < size3.z; z++) {
                        var pos = (Int3) CalculateNodePosition(x, y, z, transform);
                        var node = graph.AddNode(new PointNode(AstarPath.active), pos);
                        nodeArray[x, y, z] = node;
                        allNodes.Add(node);
                    }
                }
            }
            ConnectNodes(size3);
            graph.nodes = allNodes.ToArray();
        }
        public void ConnectNodes(Vector3 size3) {
            //Debug.Log($"ConnectNodes size {size3} node " +
                     // $"{nodeArray.GetLength(0)} {nodeArray.GetLength(1)} {nodeArray.GetLength(2)} ");
            for (int x = 0; x < size3.x; x++) {
                for (int y = 0; y < size3.y; y++) {
                    for (int z = 0; z < size3.z; z++) {
                       //Debug.Log($"connect A {x} {y} {z} {nodeArray[x, y, z].NodeIndex}");
                        if (x + 1 < size3.x) {GraphNode.Connect(nodeArray[x, y, z], nodeArray[x + 1, y, z], 100);}
                        if (x - 1 >= 0) {GraphNode.Connect(nodeArray[x, y, z], nodeArray[x - 1, y, z], 100);}
                        if (y + 1 < size3.y) {GraphNode.Connect(nodeArray[x, y, z], nodeArray[x , y+1, z], 100);}
                        if (y - 1 >= 0){ GraphNode.Connect(nodeArray[x, y, z], nodeArray[x, y-1, z], 100);}
                        if (z + 1 < size3.z) {GraphNode.Connect(nodeArray[x, y, z], nodeArray[x, y, z + 1], 100);}
                        if (z - 1 >= 0){ GraphNode.Connect(nodeArray[x, y, z], nodeArray[x, y, z - 1], 100);}
                      //  Debug.Log($"connect B {x} {y} {z}");
                    }
                }
            }
        }
    }

    protected override IGraphUpdatePromise ScanInternal () => new PathGraph2DScanPromise {
        graph = this,
        nodeArray = nodeArray,
        transform = transform,
        size3 = size3
    };
    
    public PointNode AddCreateNode(int x, int y) {
        var pos = (Int3) CalculateNodePosition(x, 0, y, transform);
        var node = AddNode(new PointNode(AstarPath.active),pos);
        return node;
    }
    
    public static Vector3 CalculateNodePosition (int x, int y, int z , GraphTransform transform) {
        var pos = new Vector3(x, y, z);
        // Multiply it with the matrix to get the node position in world space
        pos = transform.Transform(pos);
        //Debug.Log($"CalculateNodePosition {x},{y},{z}=> {pos} ");
        return pos;
    }
    
    public void DebugPath(Path path) {
        List<GraphNode> longestNodePath = new();
        var longestPathLength = 0;
        MultiTargetPath mp = path as MultiTargetPath;
        if (mp != null) {
            foreach (var vp in mp.nodePaths) {
                if (vp != null && vp.Count > longestPathLength) {
                    longestPathLength = vp.Count;
                    longestNodePath = vp;
                }
            }
        }
        string output =$"l = {longestPathLength} :: ";
        foreach(var node in longestNodePath ) {
            for (int x = 0; x < size3.x; x++) {
                for (int y = 0; y < size3.y; y++) {
                    for (int z = 0; z < size3.z; z++) {
                        if (nodeArray[x, y, z] == node) {
                            output += $", {new Vector3Int(x, y, z)}";
                        }
                    }
                }
            }
        }
        Debug.Log($"Debug Path {output}");
    }
    
    public void DebugConnectNodes() {
        Debug.Log($"Debug size {size3} node " +
                  $"{nodeArray.GetLength(0)} {nodeArray.GetLength(1)} {nodeArray.GetLength(2)} ");
        for (int x = 0; x < size3.x; x++) {
            for (int y = 0; y < size3.y; y++) {
                for (int z = 0; z < size3.z; z++) {
                    foreach (var c in nodeArray[x, y, z].connections) {
                        Debug.Log($"Debug connect {x} {y} {z}  {nodeArray[x, y, z].NodeIndex} to {c.node.NodeIndex}" );
                    }

                }
            }
        }
    }

}

Code to check for the longest shortest path between any pair of nodes

public void CheckLongestPath() {
        if (!PuzzleManager.inst.savePuzzle.usePathLength) return;
        longestPathV3 = null;
        longestPathLength = 0;
        pathsCalculated = 0;
        //pathPairs = OpenPairsList();
        pathPairs = OpenPairsList2();
        //Debug.Log($"CheckLongestPath {pathPairs.Count}");
        pathsToCalculate = pathPairs.Count;
        StartCoroutine(StartNPaths());
    }

    public IEnumerator StartNPaths() {
        while(pathPairs.Count > 0){
            for (int i = 0; i < maxPathStart; i++) {
                if (pathPairs.Count > 0) {
                    CheckMultiPath(pathPairs[0]);
                    pathPairs.RemoveAt(0);
                }
            }
            yield return null;
        }
    }

public void CheckMultiPath(MultiTarget mt) {
        var p = MultiTargetPath.Construct(mt.start, mt.targets.ToArray(), null, LayMultiPath);
        p.pathsForAll = true;
        AstarPath.StartPath(p);
    }

To create the parameters for use in MultiTargetPath.

OpenPairsList has multiple targets for each start, – produces a path wich too long because it does not use a connection
OpenPairsList2 has a single target for each start and works. – works fine
Walkable is not changed in testing

public class MultiTarget {
        public Vector3 start;
        public List<Vector3> targets;
    }

    public List<MultiTarget> OpenPairsList() {
        List<PointNode> openNodes = new();
        foreach (var node in pathGraph.nodes) {
            if (node.Walkable) {
                openNodes.Add(node);
            }
        }
        List<MultiTarget> result = new () ;
        for (int i = 0; i < openNodes.Count; i++) {
            MultiTarget subList = new() {
                start =(Vector3) openNodes[i].position,
                targets = new()
            };
            result.Add(subList);
            for (int j = i + 1; j < openNodes.Count; j++) {
                subList.targets.Add((Vector3)openNodes[j].position);
            }
        }
        return result;
    }
    public List<MultiTarget> OpenPairsList2() {
        List<PointNode> openNodes = new();
        foreach (var node in pathGraph.nodes) {
            if (node.Walkable) {
                openNodes.Add(node);
            }
        }
        List<MultiTarget> result = new () ;
        for (int i = 0; i < openNodes.Count; i++) {
            for (int j = i + 1; j < openNodes.Count; j++) {
                MultiTarget subList = new() {
                    start = (Vector3) openNodes[i].position,
                    targets = new()
                };
                result.Add(subList);
                subList.targets.Add((Vector3) openNodes[j].position);
            }
        }
        return result;
    }

Hi

One thing that could be throwing things off is that the costs between your nodes is much smaller than the A* algorithm expects. The A* algorithm expects that the cost to reach the goal is at least the euclidean (by default) distance (multiplied by 1000 to make it an integer). In your case, the cost to move between two adjacent nodes (distance 1) is only 100, while the algorithm expects at least 1000.
See Admissible heuristic - Wikipedia

You can either increase it to 1000, or set A* inspector → settings → Heuristic to None.

See also Int3 - A* Pathfinding Project

Changing Heuristic to None worked. Thanks.

I originally ran it with 1,000 but switched to 100 after I had the problem. Changing back to 1,000 did not work, but changing it to 2,000 did work.

1 Like