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 