ArgumentOutOfRangeException Occurs When Setting NodeLink2.enable = true

  • A* version: 5.4.5
  • Unity version: 6000.2.6f2

Hello,

I’m experiencing an intermittent issue when setting NodeLink2.enable = true.

Sometimes, right at the moment the property is set to true, an ArgumentOutOfRangeException occurs. After this exception happens, the FollowerEntity pauses briefly, and then it starts using the NodeLink2 after the delay. This causes an unintended stop in gameplay.

Error example:

ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: partIndex
Pathfinding.PathTracer.GetLinkInfo (System.Int32 partIndex) (at ./Packages/com.arongranberg.astar/Utilities/PathTracer.cs:1694)
Pathfinding.ECS.TraverseOffMeshLinkSystem.NextLinkToTraverse (Pathfinding.ECS.ManagedState state) (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/TraverseOffMeshLinkSystem.cs:69)
Pathfinding.ECS.TraverseOffMeshLinkSystem.StartOffMeshLinkTraversal (Unity.Entities.SystemState& systemState, Unity.Entities.EntityCommandBuffer commandBuffer) (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/TraverseOffMeshLinkSystem.cs:57)
Pathfinding.ECS.TraverseOffMeshLinkSystem.OnUpdate (Unity.Entities.SystemState& systemState) (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/TraverseOffMeshLinkSystem.cs:42)
Pathfinding.ECS.TraverseOffMeshLinkSystem.__codegen__OnUpdate (System.IntPtr self, System.IntPtr state) (at <0cd40b5daa28465da9e65c4e14d13f5e>:0)
Unity.Entities.SystemBaseRegistry+<>c__DisplayClass9_0.b__0 (System.IntPtr system, System.IntPtr state) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBaseRegistry.cs:249)
UnityEngine.Debug:LogException(Exception)
Unity.Debug:LogException(Exception) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/Stubs/Unity/Debug.cs:17)
Unity.Entities.<>c__DisplayClass9_0:b__0(IntPtr, IntPtr) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBaseRegistry.cs:253)
Unity.Entities.UnmanagedUpdate_00001677$BurstDirectCall:wrapper_native_indirect_0x44dd7adf0(IntPtr&, Void*)
Unity.Entities.UnmanagedUpdate_00001677$BurstDirectCall:Invoke(Void*)
Unity.Entities.WorldUnmanagedImpl:UnmanagedUpdate(Void*)
Unity.Entities.WorldUnmanagedImpl:UpdateSystem(SystemHandle) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/WorldUnmanaged.cs:891)
Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:717)
Unity.Entities.ComponentSystemGroup:OnUpdate() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:687)
Pathfinding.ECS.AIMovementSystemGroup:OnUpdate() (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs:211)
Unity.Entities.SystemBase:Update() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBase.cs:418)
Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:723)
Unity.Entities.ComponentSystemGroup:OnUpdate() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:681)
Unity.Entities.SystemBase:Update() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBase.cs:418)
Unity.Entities.DummyDelegateWrapper:TriggerUpdate() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ScriptBehaviourUpdateOrder.cs:523)

It doesn’t occur every time, but it happens frequently enough to be noticeable.

Could you please check what might be causing this issue or suggest a workaround?

Thank you!

Recreating this in a scene right now. Wanted to ask you when were you setting enabled to true? Right when the agent is about to traverse the link?

In our game, we have a unit that places a ladder.
When the ladder is placed, we set NodeLink2.enable = true.

Right after the ladder is placed, the unit immediately starts climbing the ladder.
At this moment, sometimes an ArgumentOutOfRangeException is thrown.
When the exception occurs, the unit pauses briefly and then starts moving again.

There are cases where it works correctly without any issue, but I want to prevent this situation from happening entirely.

And the ladder has the NodeLink2 component then? When it’s “placed” is any scanning done? Or are the NodeLink2 components already in place and they just “put a visual representation down and use it”? Basically, does anything in the graph change, or is an existing NodeLink2 just enabled when needed?

We keep the existing NodeLink2 components disabled, and when a ladder is placed we just enable the corresponding NodeLink2.
There is no change to the whole graph — we are simply activating a pre-existing NodeLink2.

Are you using the enabled from MonoBehaviour? NodeLink2 doesn’t have an enable/enabled property on it’s own. I didn’t notice this until trying to recreate it myself and realized I had only set the enabled state of the component itself. Are you disabling and enabling this through custom logic for your off-mesh link? Not sure, maybe I’m just missing something here.

I created a custom component that handles my off-mesh link logic, and I have a NodeLink2 component attached to the same GameObject.

From that off-mesh link logic component, I toggle the NodeLink2 component’s enabled state when the ladder is placed.

So when I tried a very simplified version of this, if I had my agent go towards a destination that’s only reachable through an offmesh link, they would not make it there at all if the link component was disabled. I’d enable it and they’d need a new path to use the link. So I’m confused how your agent is getting the path to the destination if the off mesh link is disabled. That’s also how I figured out there was no NodeLink2.enabled field, just MonoBehaviour.enabled (or, more accurately a bunch of things that inherit from VersionedMonoBehaviour but that’s not relevant)

A description of what’s going on there or some video may help.

using System.Collections;
using System.Collections.Generic;
using Pathfinding;
using Pathfinding.ECS;
using UnityEngine;
using static AnimatorTags;
using static AnimatorParameters;

[ExecuteAlways]
public class LadderLink : MonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine
{
  [Tooltip("The ladder mesh object (contains rotation, top & bottom points).")]
  public GameObject ladderObj;

  [Header("Setup State")]
  [Tooltip("If true, this ladder is placeable by a unit. Pre-existing ladders should set this to false.")]
  public bool canSetup = false;

  public Transform top;    // Top of the ladder
  public Transform bottom; // Bottom of the ladder

  NodeLink2 nodeLink;

  [Header("OffMeshLink Points")]
  [Tooltip("These points follow the top and bottom automatically.")]
  public Transform bottomLink, topLink;

  public readonly float offset = 0.4f; // Offset between unit and ladder
  [HideInInspector] public Vector3 offsetDir;

  float ladderLength;

  private void Awake()
  {
    nodeLink = GetComponent<NodeLink2>();
    nodeLink.end = topLink;

    offsetDir = ladderObj.transform.forward;
    offsetDir.y = 0;
    offsetDir = -offsetDir.normalized;
    SetLinkPositions();

    ladderLength = (top.position - bottom.position).magnitude;

    // Before the ladder is built: disable the visual and the link
    if (Application.isPlaying && canSetup)
    {
      nodeLink.enabled = false;
      ladderObj.SetActive(false);
    }
  }

  private void OnEnable()
  {
    if (Application.isPlaying)
      GetComponent<NodeLink2>().onTraverseOffMeshLink = this;
  }

  private void OnDisable()
  {
    if (Application.isPlaying)
      GetComponent<NodeLink2>().onTraverseOffMeshLink = null;
  }

#if UNITY_EDITOR
  private void Update()
  {
    // In editor: keep link endpoints synced with the ladder transforms
    if (!Application.isPlaying)
    {
      if (!bottomLink || !topLink || !bottom || !top || !ladderObj) return;

      offsetDir = ladderObj.transform.forward;
      offsetDir.y = 0;
      offsetDir = -offsetDir.normalized;
      SetLinkPositions();
    }
  }
#endif

  /// <summary>
  /// Called when a unit builds the ladder.
  /// Smoothly moves the carried ladder into position and then enables the actual link.
  /// </summary>
  public void StartSmoothPlacement(GameObject carriedLadder, float duration)
  {
    if (!carriedLadder) return;

    canSetup = false;
    nodeLink.enabled = true; // Enable the link at start of placement

    // NOTE: Timing issue can sometimes cause short delay/error
    StartCoroutine(SmoothPlacementRoutine(carriedLadder, duration));
  }

  private IEnumerator SmoothPlacementRoutine(GameObject carriedLadder, float duration)
  {
    // Detach from parent and disable physics on the carried mesh
    carriedLadder.transform.SetParent(null);

    if (carriedLadder.TryGetComponent<Collider>(out var col))
      col.enabled = false;

    if (carriedLadder.TryGetComponent<Rigidbody>(out var rb))
      rb.isKinematic = true;

    // Smooth movement & rotation to the final target position
    Vector3 startPos = carriedLadder.transform.position;
    Quaternion startRot = carriedLadder.transform.rotation;
    Vector3 endPos = bottom.position;
    Quaternion endRot = ladderObj.transform.rotation;

    duration = Mathf.Max(0.01f, duration);
    float t = 0f;

    while (t < 1f)
    {
      t += Time.deltaTime / duration;
      float e = Mathf.Sqrt(1f - (1f - t) * (1f - t));

      carriedLadder.transform.SetPositionAndRotation(
        Vector3.Lerp(startPos, endPos, e),
        Quaternion.Slerp(startRot, endRot, e)
      );

      yield return null;
    }

    // Activate real ladder mesh and destroy carried one
    ladderObj.SetActive(true);
    Managers.Resource.Destroy(carriedLadder);
  }

  void SetLinkPositions()
  {
    bottomLink.position = bottom.position + offset * offsetDir;
    topLink.position    = top.position    - 1.5f * offset * offsetDir;
  }

  IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context)
  {
    Debug.Log("Started OffMeshLink traversal");
    return this;
  }

  void IOffMeshLinkStateMachine.OnFinishTraversingOffMeshLink(AgentOffMeshLinkTraversalContext ctx)
  {
    if (ctx.gameObject.TryGetComponent<ILadderClimbable>(out var c))
      UnitInitOnLinkEnd(c);
  }

  void IOffMeshLinkStateMachine.OnAbortTraversingOffMeshLink()
  {
    Debug.Log("Aborted OffMeshLink traversal");
  }

  IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink(AgentOffMeshLinkTraversalContext ctx)
  {
    // If ladder is not active yet, do not allow traversal
    if (!ladderObj || !ladderObj.activeInHierarchy)
    {
      ctx.Abort(false);
      yield break;
    }

    if (!ctx.gameObject.TryGetComponent<UnitBase>(out var u) || u is not ILadderClimbable climbable)
    {
      ctx.Abort(false);
      yield break;
    }

    // If not moving animation state -> abort
    if (u.CurrentSMBInfo.tagHash != tagHashMove)
    {
      ctx.Abort(false);
      yield break;
    }

    // Disable rotation/local avoidance/gravity for climbing
    ctx.DisableRotationSmoothing();
    ctx.DisableLocalAvoidance();
    climbable.AI.enableGravity = false;

    bool climbingUp = !ctx.link.isReverse;
    Vector3 startPoint = (climbingUp ? bottom.position : top.position) + offsetDir * offset;
    Vector3 endPoint   = (climbingUp ? top.position : bottom.position) + offsetDir * offset;

    // Move to link start
    while (!ctx.MoveTowards(
        ctx.link.relativeStart,
        Quaternion.LookRotation(ctx.link.relativeStart - (Vector3)ctx.transform.Position, ctx.movementPlane.up),
        gravity: true,
        slowdown: true).reached)
    {
      if (u.CurrentSMBInfo.tagHash != tagHashMove) { AbortLink(ctx, climbable); yield break; }
      yield return null;
    }

    climbable.SetIsClimbUpOrDown(climbingUp);
    climbable.SetOnLadder(true);

    int startHash = climbingUp ? SNHLadderUpStart : SNHLadderDownStart;
    int endHash   = climbingUp ? SNHLadderUpEnd   : SNHLadderDownEnd;

    // Wait for climbing animation to activate
    while (u.CurrentSMBInfo.tagHash == tagHashMove) yield return null;
    if (u.CurrentSMBInfo.tagHash != tagHashClimb) { AbortLink(ctx, climbable); yield break; }

    while (u.CurrentSMBInfo.tagHash == tagHashClimb)
    {
      switch (u.CurrentSMBInfo.shortNameHash)
      {
        case var snh when snh == startHash:
          SyncWithLadder(ctx, u.transform, startPoint, endPoint, true);
          break;

        case var snh when snh == endHash:
          if (u.CurrentSMBInfo.normalizedTime > (climbingUp ? 0.35f : 0.5f))
          {
            RestoreRotation(ctx, u.transform, true);
            MoveToward(ctx, ctx.link.relativeEnd);
          }
          else
          {
            SyncWithLadder(ctx, u.transform, startPoint, endPoint, false);
          }
          break;

        default:
          SyncWithLadder(ctx, u.transform, startPoint, endPoint, false);
          float dist = Vector3.Distance(u.Position, endPoint);
          bool nearEnd = climbingUp ? dist < 1.23f : dist < 0.4f;
          if (nearEnd) climbable.SetOnLadder(false);
          break;
      }

      yield return null;
    }

    while (u.CurrentSMBInfo.tagHash == tagHashClimb) yield return null;
    if (u.CurrentSMBInfo.tagHash != tagHashMove) { AbortLink(ctx, climbable); yield break; }

    while (!ctx.MoveTowards(
        ctx.link.relativeEnd,
        Quaternion.LookRotation(ctx.link.relativeEnd - endPoint, ctx.movementPlane.up),
        gravity: false,
        slowdown: true).reached)
    {
      if (u.CurrentSMBInfo.tagHash != tagHashMove && u.CurrentSMBInfo.tagHash != tagHashIdle)
      {
        FinishLink(ctx, climbable);
        break;
      }
      yield return null;
    }

    Debug.Log("Finished OffMeshLink traversal");
  }

  void SyncWithLadder(AgentOffMeshLinkTraversalContext ctx, Transform tr, Vector3 start, Vector3 end, bool smooth)
  {
    float h = ((Vector3)ctx.transform.Position - start).magnitude;
    float ratio = Mathf.Clamp01(h / ladderLength);
    Vector3 p = Vector3.Lerp(start, end, ratio);

    if (smooth)
    {
      Vector3 next = Vector3.MoveTowards(ctx.transform.Position, p, ctx.deltaTime * 3f);
      ctx.transform.Position = new Vector3(next.x, tr.position.y, next.z);
    }
    else
    {
      ctx.transform.Position = new Vector3(p.x, tr.position.y, p.z);
    }

    Quaternion targetRot = ladderObj.transform.rotation;
    tr.rotation = smooth
      ? Quaternion.Slerp(tr.rotation, targetRot, ctx.deltaTime * 13f)
      : targetRot;
  }

  void MoveToward(AgentOffMeshLinkTraversalContext ctx, Vector3 targetPos)
  {
    ctx.transform.Position = Vector3.MoveTowards(ctx.transform.Position, targetPos, ctx.deltaTime * 2f);
  }

  void RestoreRotation(AgentOffMeshLinkTraversalContext ctx, Transform tr, bool smooth)
  {
    Quaternion look = Quaternion.LookRotation(-offsetDir);
    tr.rotation = smooth
      ? Quaternion.Slerp(tr.rotation, look, ctx.deltaTime * 8f)
      : look;
  }

  void FinishLink(AgentOffMeshLinkTraversalContext ctx, ILadderClimbable c)
  {
    Debug.Log("Finish Link");
    UnitInitOnLinkEnd(c);
    ctx.Teleport(ctx.link.relativeEnd);
  }

  void AbortLink(AgentOffMeshLinkTraversalContext ctx, ILadderClimbable c)
  {
    Debug.Log("Abort Link");
    UnitInitOnLinkEnd(c);
    ctx.Abort(false);
  }

  void UnitInitOnLinkEnd(ILadderClimbable c)
  {
    c.AI.enableGravity = true;
    c.SetOnLadder(false);
  }
}

This is my custom ladder off-mesh-link code.

And here is an example video:

OffMeshLink Issue example

You can see that after the ladder is placed, the agent pauses for a moment, right? At that moment, the following error occurs:

ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: partIndex
Pathfinding.PathTracer.GetLinkInfo (System.Int32 partIndex) (at ./Packages/com.arongranberg.astar/Utilities/PathTracer.cs:1694)
Pathfinding.ECS.TraverseOffMeshLinkSystem.NextLinkToTraverse (Pathfinding.ECS.ManagedState state) (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/TraverseOffMeshLinkSystem.cs:69)
Pathfinding.ECS.TraverseOffMeshLinkSystem.StartOffMeshLinkTraversal (Unity.Entities.SystemState& systemState, Unity.Entities.EntityCommandBuffer commandBuffer) (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/TraverseOffMeshLinkSystem.cs:57)
Pathfinding.ECS.TraverseOffMeshLinkSystem.OnUpdate (Unity.Entities.SystemState& systemState) (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/TraverseOffMeshLinkSystem.cs:42)
Pathfinding.ECS.TraverseOffMeshLinkSystem.__codegen__OnUpdate (System.IntPtr self, System.IntPtr state) (at <0cd40b5daa28465da9e65c4e14d13f5e>:0)
Unity.Entities.SystemBaseRegistry+<>c__DisplayClass9_0.b__0 (System.IntPtr system, System.IntPtr state) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBaseRegistry.cs:249)
UnityEngine.Debug:LogException(Exception)
Unity.Debug:LogException(Exception) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/Stubs/Unity/Debug.cs:17)
Unity.Entities.<>c__DisplayClass9_0:b__0(IntPtr, IntPtr) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBaseRegistry.cs:253)
Unity.Entities.UnmanagedUpdate_00001677$BurstDirectCall:wrapper_native_indirect_0x45b35a5f0(IntPtr&, Void*)
Unity.Entities.UnmanagedUpdate_00001677$BurstDirectCall:Invoke(Void*)
Unity.Entities.WorldUnmanagedImpl:UnmanagedUpdate(Void*)
Unity.Entities.WorldUnmanagedImpl:UpdateSystem(SystemHandle) (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/WorldUnmanaged.cs:891)
Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:717)
Unity.Entities.ComponentSystemGroup:OnUpdate() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:687)
Pathfinding.ECS.AIMovementSystemGroup:OnUpdate() (at ./Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs:211)
Unity.Entities.SystemBase:Update() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBase.cs:418)
Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:723)
Unity.Entities.ComponentSystemGroup:OnUpdate() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ComponentSystemGroup.cs:681)
Unity.Entities.SystemBase:Update() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/SystemBase.cs:418)
Unity.Entities.DummyDelegateWrapper:TriggerUpdate() (at ./Library/PackageCache/com.unity.entities@e581b903be8e/Unity.Entities/ScriptBehaviourUpdateOrder.cs:523)

This doesn’t happen every time.
Sometimes it works correctly: after the ladder is placed, the agent immediately uses the link without any error.

By NodeLink.enabled I mean enabling or disabling the NodeLink2 component itself.
When the NodeLink2 component is disabled, the link is not connected.
When it is enabled, the link becomes connected/usable.

The piece I’m trying to figure out is what your path looks like before/after placing the ladder, both when it works fine and when it fails. I can’t tell what the path looks like in the video since it’s not visually enabled– I have no idea how valid of a path they may have beforehand, since my paths were basically never valid when I tried to make this.

Please refer to the image above.
The ladder being placed is only a visual representation; in reality, it simply enables a previously disabled NodeLink2 component.
Before the ladder is placed, the NodeLink2 is disabled so the link is not connected, and once it is placed, the component is enabled and the link becomes active.

The link is exactly the same in both the normal and failing cases.
Sometimes an error appears briefly before the agent climbs the link, and other times the agent climbs it immediately without any error.
This does not seem to be related to the position of the link, as the same issue occurs with other links as well.

Thanks!

I have managed to replicate this, and implemented a fix. It will be included in the next update.

1 Like