using UnityEngine;
namespace Pathfinding {
using Pathfinding.Util;
using Pathfinding.Drawing;
///
/// Interface for handling off-mesh links.
///
/// If a component implements this interface, and is attached to the same GameObject as a NodeLink2 component,
/// then the OnTraverseOffMeshLink method will be called when an agent traverses the off-mesh link.
///
/// This only works with the component.
///
/// Note: overrides this callback, if set.
///
/// The component implements this interface, and allows a small state-machine to run when the agent traverses the link.
///
/// See:
/// See: offmeshlinks (view in online documentation for working links)
///
public interface IOffMeshLinkHandler {
///
/// Name of the handler.
/// This is used to identify the handler in the inspector.
///
public string name => null;
#if MODULE_ENTITIES
///
/// Called when an agent starts traversing an off-mesh link.
///
/// This can be used to perform any setup that is necessary for the traversal.
///
/// Returns: An object which will be used to control the agent for the full duration of the link traversal.
///
/// For simple cases, you can often implement and in the same class, and then
/// just make this method return this.
/// For more complex cases, you may want to keep track of the agent's identity, and thus want to return a new object for each agent that traverses the link.
///
/// Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
/// This context is only valid for this method call. Do not store it and use it later.
public IOffMeshLinkStateMachine GetOffMeshLinkStateMachine(ECS.AgentOffMeshLinkTraversalContext context);
#endif
}
public interface IOffMeshLinkStateMachine {
#if MODULE_ENTITIES
///
/// Called when an agent traverses an off-mesh link.
/// This method should be a coroutine (i.e return an IEnumerable) which will be iterated over until it finishes, or the agent is destroyed.
/// The coroutine should yield null every frame until the agent has finished traversing the link.
///
/// When the coroutine completes, the agent will be assumed to have reached the end of the link and control
/// will be returned to the normal movement code.
///
/// The coroutine typically moves the agent to the end of the link over some time, and perform any other actions that are necessary.
/// For example, it could play an animation, or move the agent in a specific way.
///
/// Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
/// This context is only valid when this coroutine steps forward. Do not store it and use it elsewhere.
System.Collections.IEnumerable OnTraverseOffMeshLink(ECS.AgentOffMeshLinkTraversalContext context) => ECS.JobStartOffMeshLinkTransition.DefaultOnTraverseOffMeshLink(context);
///
/// Called when an agent finishes traversing an off-mesh link.
///
/// This can be used to perform any cleanup that is necessary after the traversal.
///
/// Either or will be called, but not both.
///
/// Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
/// This context is only valid for this method call. Do not store it and use it later.
void OnFinishTraversingOffMeshLink (ECS.AgentOffMeshLinkTraversalContext context) {}
#endif
///
/// Called when an agent fails to finish traversing an off-mesh link.
///
/// This can be used to perform any cleanup that is necessary after the traversal.
///
/// An abort can happen if the agent was destroyed while it was traversing the link. It can also happen if the agent was teleported somewhere else while traversing the link.
///
/// Either or will be called, but not both.
///
/// Warning: When this is called, the agent may already be destroyed. The handler component itself could also be destroyed at this point.
///
void OnAbortTraversingOffMeshLink () {}
}
///
/// Connects two nodes using an off-mesh link.
/// In contrast to the component, this link type will not connect the nodes directly
/// instead it will create two link nodes at the start and end position of this link and connect
/// through those nodes.
///
/// If the closest node to this object is called A and the closest node to the end transform is called
/// D, then it will create one link node at this object's position (call it B) and one link node at
/// the position of the end transform (call it C), it will then connect A to B, B to C and C to D.
///
/// This link type is possible to detect while following since it has these special link nodes in the middle.
/// The link corresponding to one of those intermediate nodes can be retrieved using the method
/// which can be of great use if you want to, for example, play a link specific animation when reaching the link.
///
/// \inspectorField{End, NodeLink2.end}
/// \inspectorField{Cost Factor, NodeLink2.costFactor}
/// \inspectorField{One Way, NodeLink2.oneWay}
/// \inspectorField{Pathfinding Tag, NodeLink2.pathfindingTag}
/// \inspectorField{Graph Mask, NodeLink2.graphMask}
///
/// See: offmeshlinks (view in online documentation for working links)
/// See: The example scene RecastExample2 contains a few links which you can take a look at to see how they are used.
///
/// Note: If you make any modifications to the node link's settings after it has been created, you need to call the method in order to apply the changes to the graph.
///
[AddComponentMenu("Pathfinding/Link2")]
[HelpURL("https://arongranberg.com/astar/documentation/stable/nodelink2.html")]
public class NodeLink2 : GraphModifier {
/// End position of the link
public Transform end;
///
/// The connection will be this times harder/slower to traverse.
///
/// 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.
///
/// You should not use a cost factor less than 1 unless you also change the field (A* Inspector -> Settings -> Pathfinding) to at most the minimum cost factor that you use anywhere in the scene (or disable the heuristic altogether).
/// This is because the pathfinding algorithm assumes that paths are at least as costly as walking just the straight line distance to the target, and if you use a cost factor less than 1, that assumption is no longer true.
/// What then happens is that the pathfinding search may ignore some links because it doesn't even think to search in that direction, even if they would have lead to a lower path cost.
///
/// Warning: Reducing the heuristic scale or disabling the heuristic can significantly increase the cpu cost for pathfinding, especially for large graphs.
///
/// Read more about this at https://en.wikipedia.org/wiki/Admissible_heuristic.
///
public float costFactor = 1.0f;
/// Make a one-way connection
public bool oneWay = false;
///
/// The tag to apply to the link.
///
/// This can be used to exclude certain agents from using the link, or make it more expensive to use.
///
/// See: tags (view in online documentation for working links)
///
public PathfindingTag pathfindingTag = 0;
///
/// Which graphs this link is allowed to connect.
///
/// The link will always connect the nodes closest to the start and end points on the graphs that it is allowed to connect.
///
public GraphMask graphMask = -1;
public Transform StartTransform => transform;
public Transform EndTransform => end;
protected OffMeshLinks.OffMeshLinkSource linkSource;
///
/// Returns the link component associated with the specified node.
/// Returns: The link associated with the node or null if the node is not a link node, or it is not associated with a component.
///
/// The node to get the link for.
public static NodeLink2 GetNodeLink(GraphNode node) => node is LinkNode linkNode ? linkNode.linkSource.component as NodeLink2 : null;
///
/// True if the link is connected to the graph.
///
/// This will be true if the link has been successfully connected to the graph, and false if it either has failed, or if the component/gameobject is disabled.
///
/// When the component is enabled, the link will be scheduled to be added to the graph, it will not take effect immediately.
/// This means that this property will return false until the next time graph updates are processed (usually later this frame, or next frame).
/// To ensure the link is refreshed immediately, you can call .
///
internal bool isActive => linkSource != null && (linkSource.status & OffMeshLinks.OffMeshLinkStatus.Active) != 0;
IOffMeshLinkHandler onTraverseOffMeshLinkHandler;
///
/// Callback to be called when an agent starts traversing an off-mesh link.
///
/// The handler will be called when the agent starts traversing an off-mesh link.
/// It allows you to to control the agent for the full duration of the link traversal.
///
/// Use the passed context struct to get information about the link and to control the agent.
///
///
/// using UnityEngine;
/// 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().onTraverseOffMeshLink = this;
/// void OnDisable() => GetComponent().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;
/// }
/// }
/// }
/// }
///
///
/// Warning: Off-mesh links can be destroyed or disabled at any moment. The built-in code will attempt to make the agent continue following the link even if it is destroyed,
/// but if you write your own traversal code, you should be aware of this.
///
/// You can alternatively set the corresponding property property on the agent () to specify a callback for a all off-mesh links.
///
/// Note: The agent's off-mesh link handler takes precedence over the link's off-mesh link handler, if both are set.
///
/// Warning: This property only works with the component. Use if you are using the movement script.
///
/// See: offmeshlinks (view in online documentation for working links) for more details and example code
///
public IOffMeshLinkHandler onTraverseOffMeshLink {
get => onTraverseOffMeshLinkHandler;
set {
onTraverseOffMeshLinkHandler = value;
if (linkSource != null) linkSource.handler = value;
}
}
public override void OnPostScan () {
TryAddLink();
}
protected override void OnEnable () {
base.OnEnable();
if (Application.isPlaying && !BatchedEvents.Has(this)) BatchedEvents.Add(this, BatchedEvents.Event.Update, OnUpdate);
TryAddLink();
}
static void OnUpdate (NodeLink2[] components, int count) {
// Only check for moved links every N frames, for performance
if ((Time.frameCount % 16) != 0) return;
for (int i = 0; i < count; i++) {
var comp = components[i];
var start = comp.StartTransform;
var end = comp.EndTransform;
var added = comp.linkSource != null;
if ((start != null && end != null) != added || (added && (start.hasChanged || end.hasChanged))) {
if (start != null) start.hasChanged = false;
if (end != null) end.hasChanged = false;
comp.RemoveLink();
comp.TryAddLink();
}
}
}
void TryAddLink () {
// In case the AstarPath component has been destroyed (destroying the link).
// But do not clear it if the link is inactive because it failed to become enabled
if (linkSource != null && (linkSource.status == OffMeshLinks.OffMeshLinkStatus.Inactive || (linkSource.status & OffMeshLinks.OffMeshLinkStatus.PendingRemoval) != 0)) linkSource = null;
if (linkSource == null && AstarPath.active != null && EndTransform != null) {
StartTransform.hasChanged = false;
EndTransform.hasChanged = false;
linkSource = new OffMeshLinks.OffMeshLinkSource {
start = new OffMeshLinks.Anchor {
center = StartTransform.position,
rotation = StartTransform.rotation,
width = 0f,
},
end = new OffMeshLinks.Anchor {
center = EndTransform.position,
rotation = EndTransform.rotation,
width = 0f,
},
directionality = oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay,
tag = pathfindingTag,
costFactor = costFactor,
graphMask = graphMask,
maxSnappingDistance = 1, // TODO
component = this,
handler = onTraverseOffMeshLink,
};
AstarPath.active.offMeshLinks.Add(linkSource);
}
}
void RemoveLink () {
if (AstarPath.active != null && linkSource != null) AstarPath.active.offMeshLinks.Remove(linkSource);
linkSource = null;
}
protected override void OnDisable () {
base.OnDisable();
BatchedEvents.Remove(this);
RemoveLink();
}
[ContextMenu("Recalculate neighbours")]
void ContextApplyForce () {
Apply();
}
///
/// Disconnects and then reconnects the link to the graph.
///
/// If you have moved the link or otherwise modified it you need to call this method to apply those changes.
///
public virtual void Apply () {
RemoveLink();
TryAddLink();
}
private readonly static Color GizmosColor = new Color(206.0f/255.0f, 136.0f/255.0f, 48.0f/255.0f, 0.5f);
private readonly static Color GizmosColorSelected = new Color(235.0f/255.0f, 123.0f/255.0f, 32.0f/255.0f, 1.0f);
public override void DrawGizmos () {
if (StartTransform == null || EndTransform == null) return;
var startPos = StartTransform.position;
var endPos = EndTransform.position;
if (linkSource != null && (Time.renderedFrameCount % 16) == 0 && Application.isEditor) {
// Check if the link has moved
// During runtime, this will be done by the OnUpdate method instead
if (linkSource.start.center != startPos || linkSource.end.center != endPos || linkSource.directionality != (oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay) || linkSource.costFactor != costFactor || linkSource.graphMask != graphMask || linkSource.tag != pathfindingTag) {
Apply();
}
}
bool selected = GizmoContext.InActiveSelection(this);
var graphs = linkSource != null && AstarPath.active != null? AstarPath.active.offMeshLinks.ConnectedGraphs(linkSource) : null;
var up = Vector3.up;
// Find the natural up direction of the connected graphs, so that we can orient the gizmos appropriately
if (graphs != null) {
for (int i = 0; i < graphs.Count; i++) {
var graph = graphs[i];
if (graph != null) {
if (graph is NavmeshBase navmesh) {
up = navmesh.transform.WorldUpAtGraphPosition(Vector3.zero);
break;
} else if (graph is GridGraph grid) {
up = grid.transform.WorldUpAtGraphPosition(Vector3.zero);
break;
}
}
}
ListPool.Release(ref graphs);
}
var active = linkSource != null && linkSource.status == OffMeshLinks.OffMeshLinkStatus.Active;
Color color = selected ? GizmosColorSelected : GizmosColor;
if (active) color = Color.green;
Draw.Circle(startPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectStart) ? Color.red : color);
Draw.Circle(endPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectEnd) ? Color.red : color);
NodeLink.DrawArch(startPos, endPos, up, color);
if (selected) {
Vector3 cross = Vector3.Cross(up, endPos-startPos).normalized;
using (Draw.WithLineWidth(2)) {
NodeLink.DrawArch(startPos+cross*0.0f, endPos+cross*0.0f, up, color);
}
// NodeLink.DrawArch(startPos-cross*0.1f, endPos-cross*0.1f, color);
}
}
}
}