-
A* version: [5.4.6]
-
Unity version: [6000.3.0f1]
I use the class below to generate offmesh links dynamically depending on the placement of a group of walkable surfaces in this case barrels. When a barrel gets destroyed RecalculateNavmesh() gets called. It disables the nodelinks that were created by this group so that IsPathPossible doesnt consider valid paths through the links. When the callback on graphsUpdated gets triggered the calculation for the dynamic links runs and reenables links (that is what pool.SpawnObject does). The issue I ran into is that depending on the barrel getting disabled the links don’t reapply by themselves (or when calling link.Apply()). To combat this, I added a single frame delay and it seems to fix the issue. Is this workaround valid? Why could this be happening? Do you see any “red flag” with my approach?
public class BarrelGroupController : MonoBehaviour
{
[Header("Global Settings")]
public LayerMask layerMask;
public float navMeshCheckDistance;
[Header("Debugging")]
public bool enableDebugging = false;
private List<BoxCollider> _colliders = new();
private List<OilDrum> _barrels = new();
private Bounds _bounds;
private readonly List<NodeLink2> _activeLinks = new();
private OffMeshLinkScanConfig[] _scanConfigs;
void Awake()
{
_colliders = GetComponentsInChildren<BoxCollider>().Where(x => !x.isTrigger).ToList();
_scanConfigs = GetComponentsInChildren<OffMeshLinkScanConfig>();
_barrels = GetComponentsInChildren<OilDrum>().ToList();
var navmeshModifiers = GetComponentsInChildren<RecastNavmeshModifier>();
for (int i = 0; i < navmeshModifiers.Length; i++)
{
var mod = navmeshModifiers[i];
mod.mode = RecastNavmeshModifier.Mode.WalkableSurfaceWithSeam;
mod.surfaceID = i + 100;
}
_bounds = CalculateBounds();
}
private void OnEnable()
{
#pragma warning disable UDR0005 // Domain Reload Analyzer
AstarPath.OnGraphsUpdated += OnGraphUpdated;
#pragma warning restore UDR0005 // Domain Reload Analyzer
RecalculateNavmesh();
foreach (var barrel in _barrels)
{
barrel.ItemDisabled += OnBarrelDisabled;
}
}
private void OnDisable()
{
AstarPath.OnGraphsUpdated -= OnGraphUpdated;
foreach (var barrel in _barrels)
{
barrel.ItemDisabled -= OnBarrelDisabled;
}
}
private void OnBarrelDisabled(OilDrum barrel)
{
RecalculateNavmesh();
}
[ContextMenu("Recalculate Navmesh")]
public void RecalculateNavmesh()
{
if (AstarPath.active == null) return;
foreach (var link in _activeLinks)
{
if (link != null)
{
link.gameObject.SetActive(false);
}
}
_activeLinks.Clear();
_bounds = CalculateBounds();
var guo = new GraphUpdateObject(_bounds);
AstarPath.active.UpdateGraphs(guo);
}
private void OnGraphUpdated(AstarPath path)
{
foreach (var scanConfig in _scanConfigs)
{
if (!scanConfig.gameObject.activeInHierarchy) continue;
var origin = scanConfig.transform.position;
var constraint = NearestNodeConstraint.Walkable;
constraint.maxDistance = navMeshCheckDistance;
NNInfo info = AstarPath.active.GetNearest(origin, constraint);
if (info.node == null) continue;
var hits = Physics.SphereCastAll(
scanConfig.Start,
scanConfig.Radius,
scanConfig.Direction,
scanConfig.CastDistance,
layerMask,
QueryTriggerInteraction.Ignore
);
if (hits.Length == 0) continue;
// Sort by distance so we evaluate in ray order
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
foreach (var hit in hits)
{
if (hit.transform.IsChildOf(scanConfig.transform.parent.parent))
continue;
NNInfo infoHit = AstarPath.active.GetNearest(hit.point, constraint);
if (infoHit.node == null || !infoHit.node.Walkable)
break;
if (PathUtilities.IsPathPossible(node1: info.node, node2: infoHit.node))
break;
var linkInfo = CreateLink(info.position, infoHit.position, scanConfig.linkPrefabPool);
break;
}
}
}
private NodeLink2 CreateLink(Vector3 start, Vector3 end, PoolingSystemSO pool)
{
GameObject go = pool.SpawnObject(start);
NodeLink2 link = go.GetComponent<NodeLink2>();
go.transform.GetChild(0).position = end;
_activeLinks.Add(link);
StartCoroutine(DelayApply(link));
return link;
}
IEnumerator DelayApply(NodeLink2 link)
{
yield return null;
link.Apply();
}
private Bounds CalculateBounds()
{
var activeColliders = _colliders.Where(x => x.gameObject.activeInHierarchy);
if (activeColliders.Count() == 0) return new Bounds();
Bounds bounds = _colliders[0].bounds;
foreach (BoxCollider collider in _colliders)
{
bounds.Encapsulate(collider.bounds);
}
bounds.Expand(0.1f);
return bounds;
}
private void OnDrawGizmosSelected()
{
if (!enableDebugging) return;
Gizmos.color = Color.aquamarine;
Gizmos.DrawWireCube(_bounds.center, _bounds.size);
}
}