Hybrid pathfinding implementation. Issue with long paths

Hello everyone!

Has anyone tried implementing hybrid pathfinding using Aron’s solution? It combines a PointGraph for global pathfinding with a RecastGraph for local and more precise navigation around the agent (see https://youtu.be/yqZE5O8VPAU?t=1717). It’s straightforward to set up both graphs and snap the local one to the player using ProceduralGraphMover, but how do you correctly calculate and merge the two resulting paths?

I have a working prototype – using GridGraph and RecastGraph (instead of RecastGraph and PointGraph, GridGraph snaped with agent using ProceduralGraphMover) – but I’ve run into a specific issue: when the target is far enough away, the IAstarAI component (I’m using FollowerEntity) refuses to move the agent and logs a warning (Path Failed : Computation Time 0,00 ms Searched Nodes 0 Error: Couldn’t find a node close to the end point). There is link to demonstation in official discord channel: Discord

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

[RequireComponent(typeof(IAstarAI))]
public class HybridDestinationSetter : MonoBehaviour
{
    public Transform target;
    private Vector3 lastTargetPos;

    private IAstarAI ai;
    private GridGraph localGrid;
    private RecastGraph recastGraph;
    private NNConstraint gridConstraint;
    private NNConstraint recastConstraint;

    private ABPath firstPath;
    private ABPath secondPath;
    private bool firstDone;
    private bool secondDone;
    private bool isCalculating;

    void Awake()
    {
        ai = GetComponent<IAstarAI>();

        lastTargetPos = target.position;

        localGrid = AstarPath.active.data.gridGraph;
        recastGraph = AstarPath.active.data.recastGraph;

        gridConstraint = new NNConstraint { graphMask = GraphMask.FromGraph(localGrid) };
        recastConstraint = new NNConstraint { graphMask = GraphMask.FromGraph(recastGraph) };
    }

    void Update()
    {
        if (target == null || !ai.canSearch || ai.pathPending || isCalculating)
            return;

        //uncomment later
        //if ((lastTargetPos - target.position).sqrMagnitude < 1f)
        //    return;

        lastTargetPos = target.position;
        StartCoroutine(ComputeHybridPath());
    }

    IEnumerator ComputeHybridPath()
    {
        isCalculating = true;

        Vector3 startPos = ai.position;
        Vector3 targetPos = target.position;

        // ——— GridGraph segment ——————————————————

        // 1. Find nearest GridGraph node to start
        var nnGridStart = localGrid.GetNearest(startPos, gridConstraint);
        //Debug.Log(nnGridStart.node?.GraphIndex);
        if (nnGridStart.node == null)
        {
            Debug.LogError("[Hybrid] GridGraph start node == null");
            isCalculating = false;
            yield break;
        }
        Vector3 gridStart = nnGridStart.position;

        // 2. Clamp targetPos to GridGraph bounds and find exit node
        Vector3 clampPoint = localGrid.bounds.ClosestPoint(targetPos);
        var nnGridEnd = localGrid.GetNearest(clampPoint, gridConstraint);
        //Debug.Log(nnGridEnd.node?.GraphIndex);
        if (nnGridEnd.node == null)
        {
            Debug.LogError("[Hybrid] GridGraph exit node == null");
            isCalculating = false;
            yield break;
        }
        Vector3 exitPoint = nnGridEnd.position;

        Debug.Log($"[Hybrid] Grid Start @ {gridStart}  Exit @ {exitPoint}");

        // 3. Build GridGraph path
        firstDone = false;
        firstPath = ABPath.Construct(gridStart, exitPoint, p => { firstPath = p as ABPath; firstDone = true; });
        firstPath.nnConstraint = gridConstraint;
        AstarPath.StartPath(firstPath);
        yield return new WaitUntil(() => firstDone);
        if (firstPath.error)
        {
            Debug.LogError("[Hybrid] GridGraph path error: " + firstPath.errorLog);
            isCalculating = false;
            yield break;
        }

        // ——— RecastGraph segment ——————————————————

        // 4. Find entry node in RecastGraph
        var nnRecastStart = recastGraph.GetNearest(exitPoint, recastConstraint);
        //Debug.Log(nnRecastStart.node?.GraphIndex);
        if (nnRecastStart.node == null)
        {
            Debug.LogError("[Hybrid] RecastGraph start node == null");
            isCalculating = false;
            yield break;
        }
        Vector3 recastEntry = nnRecastStart.position;

        // 5. Find exit node in RecastGraph (near target)
        var nnRecastEnd = recastGraph.GetNearest(targetPos, recastConstraint);
        //Debug.Log(nnRecastEnd.node?.GraphIndex);
        if (nnRecastEnd.node == null)
        {
            Debug.LogError("[Hybrid] RecastGraph end node == null");
            isCalculating = false;
            yield break;
        }
        Vector3 recastExit = nnRecastEnd.position;

        Debug.Log($"[Hybrid] Recast Entry @ {recastEntry}  Recast Exit @ {recastExit}");

        // 6. Build RecastGraph path
        secondDone = false;
        secondPath = ABPath.Construct(recastEntry, recastExit, p => { secondPath = p as ABPath; secondDone = true; });
        secondPath.nnConstraint = recastConstraint;
        AstarPath.StartPath(secondPath);
        yield return new WaitUntil(() => secondDone);
        if (secondPath.error)
        {
            Debug.LogError("[Hybrid] RecastGraph path error: " + secondPath.errorLog);
            isCalculating = false;
            yield break;
        }

        // ——— Merge segments —————————————————————————

        var merged = new List<Vector3>();

        // a) from real startPos to gridStart
        merged.Add(startPos);
        if ((gridStart - startPos).sqrMagnitude > 0.001f)
            merged.Add(gridStart);

        // b) full GridGraph path (skip duplicate start)
        merged.AddRange(firstPath.vectorPath.Skip(1));

        // c) add transition into RecastGraph
        if ((recastEntry - exitPoint).sqrMagnitude > 0.001f)
            merged.Add(recastEntry);

        // d) full RecastGraph path (skip duplicate entry)
        merged.AddRange(secondPath.vectorPath.Skip(1));

        // e) ensure final targetPos
        if ((merged.Last() - targetPos).sqrMagnitude > 0.001f)
            merged.Add(targetPos);

        // Debug draw
        for (int i = 0; i < merged.Count - 1; i++)
            Debug.DrawLine(merged[i], merged[i + 1], Color.blue, 1.5f);

        // ——— Final path application ——————————————————

        var hybrid = ABPath.Construct(startPos, targetPos, null);
        hybrid.vectorPath = merged;

        // Use SetPath to hand over the precomputed vectorPath
        ai.SetPath(hybrid);
        Debug.Log($"[Hybrid] Applied ai.SetPath: hasPath={ai.hasPath}, canMove={ai.canMove}");

        isCalculating = false;
    }
}

My prototype is now fully functional. Representing the resulting path as a fake path was the solution.

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

[RequireComponent(typeof(IAstarAI))]
public class HybridDestinationSetter : MonoBehaviour
{
    [SerializeField] private Transform _target;

    private IAstarAI _ai;
    private GridGraph _localGrid;
    private RecastGraph _recastGraph;
    private NNConstraint _gridConstraint;
    private NNConstraint _recastConstraint;

    private ABPath _firstPath;
    private ABPath _secondPath;
    private bool _isCalculating;

    private float _pathUpdateInterval = 0.25f;
    private float _timeSinceLastPath;

    void Awake()
    {
        _ai = GetComponent<IAstarAI>();

        _localGrid = AstarPath.active.data.gridGraph;
        _recastGraph = AstarPath.active.data.recastGraph;

        _gridConstraint = new NNConstraint { graphMask = GraphMask.FromGraph(_localGrid) };
        _recastConstraint = new NNConstraint { graphMask = GraphMask.FromGraph(_recastGraph) };
    }

    void Update()
    {
        if (_target == null || !_ai.canSearch || _isCalculating)
            return;

        _timeSinceLastPath += Time.deltaTime;

        if (_timeSinceLastPath < _pathUpdateInterval)
            return;

        if ((_ai.position - _target.position).sqrMagnitude < 1f)
            return;

        _timeSinceLastPath = 0f;
        StartCoroutine(ComputeHybridPath());
    }

    IEnumerator ComputeHybridPath()
    {
        _isCalculating = true;

        Vector3 startPos = _ai.position;
        Vector3 targetPos = _target.position;

        // ——— GridGraph segment ——————————————————

        // 1. Find nearest GridGraph node to start
        var nnGridStart = _localGrid.GetNearest(startPos, _gridConstraint);
        if (nnGridStart.node == null)
        {
            Debug.LogError("[Hybrid] GridGraph start node == null");
            _isCalculating = false;
            yield break;
        }
        Vector3 gridStart = nnGridStart.position;

        // 2. Clamp targetPos to GridGraph bounds and find exit node
        Vector3 clampPoint = _localGrid.bounds.ClosestPoint(targetPos);
        var nnGridEnd = _localGrid.GetNearest(clampPoint, _gridConstraint);
        if (nnGridEnd.node == null)
        {
            Debug.LogError("[Hybrid] GridGraph exit node == null");
            _isCalculating = false;
            yield break;
        }
        Vector3 exitPoint = nnGridEnd.position;

        // 3. Build GridGraph path
        _firstPath = ABPath.Construct(gridStart, exitPoint);
        _firstPath.nnConstraint = _gridConstraint;
        AstarPath.StartPath(_firstPath);
        yield return new WaitUntil(() => _firstPath.IsDone());
        if (_firstPath.error)
        {
            Debug.LogError("[Hybrid] GridGraph path error: " + _firstPath.errorLog);
            _isCalculating = false;
            yield break;
        }

        // ——— RecastGraph segment ——————————————————

        // 4. Find entry node in RecastGraph
        var nnRecastStart = _recastGraph.GetNearest(exitPoint, _recastConstraint);
        if (nnRecastStart.node == null)
        {
            Debug.LogError("[Hybrid] RecastGraph start node == null");
            _isCalculating = false;
            yield break;
        }
        Vector3 recastEntry = nnRecastStart.position;

        // 5. Find exit node in RecastGraph (near target)
        var nnRecastEnd = _recastGraph.GetNearest(targetPos, _recastConstraint);
        if (nnRecastEnd.node == null)
        {
            Debug.LogError("[Hybrid] RecastGraph end node == null");
            _isCalculating = false;
            yield break;
        }
        Vector3 recastExit = nnRecastEnd.position;

        // 6. Build RecastGraph path
        _secondPath = ABPath.Construct(recastEntry, recastExit);
        _secondPath.nnConstraint = _recastConstraint;
        AstarPath.StartPath(_secondPath);
        yield return new WaitUntil(() => _secondPath.IsDone());
        if (_secondPath.error)
        {
            Debug.LogError("[Hybrid] RecastGraph path error: " + _secondPath.errorLog);
            _isCalculating = false;
            yield break;
        }

        // ——— Merge segments —————————————————————————

        var merged = new List<Vector3>();

        // a) from real startPos to gridStart
        merged.Add(startPos);
        if ((gridStart - startPos).sqrMagnitude > 0.001f)
            merged.Add(gridStart);

        // b) full GridGraph path (skip duplicate start)
        foreach (var point in _firstPath.vectorPath.AsValueEnumerable().Skip(1))
        {
            merged.Add(point);
        }

        // c) add transition into RecastGraph
        if ((recastEntry - exitPoint).sqrMagnitude > 0.001f)
            merged.Add(recastEntry);

        // d) full RecastGraph path (skip duplicate entry)
        foreach (var point in _secondPath.vectorPath.AsValueEnumerable().Skip(1))
        {
            merged.Add(point);
        }

        // e) ensure final targetPos
        if ((merged.AsValueEnumerable().Last() - targetPos).sqrMagnitude > 0.001f)
            merged.Add(targetPos);

        // Debug draw
        for (int i = 0; i < merged.Count - 1; i++)
            Debug.DrawLine(merged[i], merged[i + 1], Color.blue, _pathUpdateInterval + 0.15f);

        //// ——— Final path application ——————————————————
        var allNodes = new List<GraphNode>();
        allNodes.AddRange(_firstPath.path);             
        foreach (var point in _secondPath.path.AsValueEnumerable().Skip(1))
        {
            allNodes.Add(point);
        }

        var hybrid = ABPath.FakePath(merged, allNodes);

        _ai.SetPath(hybrid, updateDestinationFromPath: true);

        _isCalculating = false;
    }
}
1 Like

Marvelous work here- love to see an inventive solution like this. Thanks for sharing the results with the community :smiley: