Off-mesh links
Tutorial on how to use off-mesh links to implement jumps, climbing, teleporters and other special movement logic.
Contents
Introduction
Off-mesh links are custom connections between two nodes in a single graph, or even between different graphs. They are used for implementing special movement logic such as jumping, climbing, opening doors, teleporters and other things.
When an agent reaches an off-mesh link, it will stop its normal movement code, and instead call a user defined function. This function can then implement special movement logic, such as playing an animation, moving the agent to a different position, or anything else.
The included movement scripts have varying levels of support for off-mesh links.
FollowerEntity: Good support for off-mesh links.
RichAI: Decent support for off-mesh links. Does not support using the Interactable component for movement logic on off-mesh links.
AIPath: Limited support for off-mesh links. The agent can traverse them, but it does not know that it traverses an off-mesh link, and cannot use any special movement logic.
AILerp: Limited support for off-mesh links. The agent can traverse them, but it does not know that it traverses an off-mesh link, and cannot use any special movement logic.
Setting up an off-mesh link
Off-mesh links are created using the NodeLink2 component.
End position of the link.
The connection will be this times harder/slower to traverse.
Make a one-way connection.
The tag to apply to the link.
Which graphs this link is allowed to connect.
The NodeLink2 creates a link between its own position, and the position of the target transform. By default, this link is bidirectional, meaning that the agent can traverse it in both directions. But it can also be configured to be one-way, which is useful for things like jumping down from ledges.
A link can also have a tag applied to it. This is useful if you want to only allow specific agents from traversing it, or if you want to make it more expensive to traverse for some agents. Speaking of cost, the cost factor can be used to make the agent prefer to walk around the link instead of traversing it. A cost factor of 1 means that the link is equally expensive as moving the same distance on the normal navmesh. But a cost factor greater than 1 means that it is proportionally more expensive.
To use a cost factor lower than one (useful for e.g. teleporters), you must do some additional configuration. Read more about this here: NodeLink2.costFactor.
Case studies
The following sections will describe how to implement some common use cases for off-mesh links.
Jumping off a ledge
If a character needs to be able to jump off a ledge, you can add a one-way off mesh link from the ledge to the ground below. This will allow the character to traverse the link, but not to traverse it in the opposite direction.
The default off-mesh link traversal logic will usually work for this, as it just moves the agent towards the end of the link while ignoring the navmesh. However, you may want to add some custom logic to play an animation or to make the character jump in a more realistic way.
Teleporter
A teleporter can also be implemented using an off-mesh link. However, this requires some custom logic to move the agent to the new position. This can be done in multiple ways, but the simplest one is to use the Interactable component, which is included with the example scenes. If the interactable component is attached to the same GameObject as a NodeLink2 component, its workflow will trigger when the agent traverses the link.
The workflow moves the agent to the end of the link, and also triggers some particle systems.
However, to make the agent treat the teleport as a zero-cost action (instead of having a cost proportional to the teleportation distance), we must do a few things. First, we must set the cost factor of the link component to 0.
Secondly, we must set the A* Inspector -> Settings -> Pathfinding -> Heuristic field to None. Otherwise, the agent may walk around the teleporter because the pathfinding search will not even consider going in that direction. You can read more about this in the NodeLink2.costFactor documentation.
Opening a door
There are several ways of handling doors that the agent should be able to open.
If the door is automatic, then excluding it from the graph scan and using a separate script to play an opening/closing animation depending on if agents are near is a good solution. You should not use an off-mesh link in this case.
If the door requires activation the first time, but then it will stay open, you can use a NodeLink2 component as described below.
Create a NodeLink2 component that goes from one side of the door to the other side. We will also use an Interactable component on the link to trigger the door opening animation.
The interactable component will make the agent do a few things.
Start the opening animation of the door (which will also make the navmesh traversable, see Doors for more info).
Move the agent to the start of the link, and make it face the door (the agent should already be at the door when it starts traversing the link, so this is primarily for the rotation).
Wait a few seconds for the door to open.
Deactivate the off-mesh link.
The last step is perhaps unintuitive, but it works very well for this use case. What happens is that when the door opens, it will make the navmesh underneath it traversable. Then, when we deactivate the link, the agent will be forced to recalculate its path, and it will find the new path through the door without using the link this time. In the future, the door will stay open and agents will be able to just walk through.
If necessary, one could also make the door close after a while, and re-activate the off mesh link.
Custom off-mesh link traversal logic
If you need custom off-mesh link traversal logic, for example to play custom animations, or move the character in ways specific to your game, you can register to a callback to control every aspect of the traversal.
Register your handler to the FollowerEntity.onTraverseOffMeshLink or RichAI.onTraverseOffMeshLink properties, depending on which movement script you are using. Alternatively, you can use the NodeLink2.onTraverseOffMeshLink property to register a callback for a specific off-mesh link.
Note
The AIPath and AILerp movement scripts do not support custom movement during off-mesh link traversal.
This section will focus on the FollowerEntity. The RichAI has similar functionality, but the api looks slightly different, and is a bit more limited.
You can assign any class as a handler if it implements the IOffMeshLinkHandler interface. This interface has a single method, GetOffMeshLinkStateMachine, which should return an object that implements the IOffMeshLinkStateMachine interface. The state machine interface has several methods that you can override to control different parts of the traversal.
Called when an agent traverses an off-mesh link.
Called when an agent finishes traversing an off-mesh link.
Called when an agent fails to finish traversing an off-mesh link.
In the simplest case, you may only want to receive an event when the agent traverses a link, but fall back to the default movement implementation. You can do that like this:
using UnityEngine;
The provided context object gives you some helper methods like AgentOffMeshLinkTraversalContext.Teleport and AgentOffMeshLinkTraversalContext.MoveTowards, and it also provides you with access to various agent data.
using Pathfinding;
using Pathfinding.ECS;
public class LogOffMeshLinkTraversal : MonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine {
// Register this class as the handler for off mesh links when the component is enabled.
// This component supports registering to both NodeLink2 and FollowerEntity.
void OnEnable () {
if (TryGetComponent<NodeLink2>(out var link)) link.onTraverseOffMeshLink = this;
if (TryGetComponent<FollowerEntity>(out var ai)) ai.onTraverseOffMeshLink = this;
}
void OnDisable () {
if (TryGetComponent<NodeLink2>(out var link)) link.onTraverseOffMeshLink = null;
if (TryGetComponent<FollowerEntity>(out var ai)) ai.onTraverseOffMeshLink = null;
}
IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine (AgentOffMeshLinkTraversalContext context) {
Debug.Log("An agent started traversing an off-mesh link");
return this;
}
void IOffMeshLinkStateMachine.OnFinishTraversingOffMeshLink (AgentOffMeshLinkTraversalContext context) {
Debug.Log("An agent finished traversing an off-mesh link");
}
void IOffMeshLinkStateMachine.OnAbortTraversingOffMeshLink () {
Debug.Log("An agent aborted traversing an off-mesh link");
}
// Don't implement IOffMeshLinkStateMachine.OnTraverseOffMeshLink to fall back to the default implementation
// System.Collections.IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink(ECS.AgentOffMeshLinkTraversalContext context)
}
See
AgentOffMeshLinkTraversalContext
You can also do more complex things, like controlling the position of the agent completely. In the example below, a jump link is implemented. The agent first rotates to face the other side of the link, and then "jumps" to the other side by moving along an arc controlled by a bezier curve.
using UnityEngine;
Attaching the above script to the same GameObject as a NodeLink2 component will make the agent jump when it traverses the link, as can be seen in the video below.
using Pathfinding;
using System.Collections;
using Pathfinding.ECS;
namespace Pathfinding.Examples {
public class FollowerJumpLink : MonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine {
// Register this class as the handler for off-mesh links when the component is enabled
void OnEnable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = this;
void OnDisable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = null;
IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context) => this;
void IOffMeshLinkStateMachine.OnFinishTraversingOffMeshLink (AgentOffMeshLinkTraversalContext context) {
Debug.Log("An agent finished traversing an off-mesh link");
}
void IOffMeshLinkStateMachine.OnAbortTraversingOffMeshLink () {
Debug.Log("An agent aborted traversing an off-mesh link");
}
IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext ctx) {
var start = (Vector3)ctx.link.relativeStart;
var end = (Vector3)ctx.link.relativeEnd;
var dir = end - start;
// Disable local avoidance while traversing the off-mesh link.
// If it was enabled, it will be automatically re-enabled when the agent finishes traversing the link.
ctx.DisableLocalAvoidance();
// Move and rotate the agent to face the other side of the link.
// When reaching the off-mesh link, the agent may be facing the wrong direction.
while (!ctx.MoveTowards(
position: start,
rotation: Quaternion.LookRotation(dir, ctx.movementPlane.up),
gravity: true,
slowdown: true).reached) {
yield return null;
}
var bezierP0 = start;
var bezierP1 = start + Vector3.up*5;
var bezierP2 = end + Vector3.up*5;
var bezierP3 = end;
var jumpDuration = 1.0f;
// Animate the AI to jump from the start to the end of the link
for (float t = 0; t < jumpDuration; t += ctx.deltaTime) {
ctx.transform.Position = AstarSplines.CubicBezier(bezierP0, bezierP1, bezierP2, bezierP3, Mathf.SmoothStep(0, 1, t / jumpDuration));
yield return null;
}
}
}
}