using System.Collections.Generic; using Unity.Collections; using UnityEngine; using UnityEngine.Profiling; using Pathfinding.Graphs.Navmesh; using Pathfinding.Util; using System; using UnityEngine.Assertions; namespace Pathfinding { /// /// Manager for off-mesh links. /// /// This manager tracks all active off-mesh links in the scene and recalculates them when needed. /// If an off-mesh link is activated, a will also be added to the graph list to store the special nodes necessary for the links to work. /// /// Whenever a graph update happens, the method should be called with the bounds of the updated area. /// This will cause the links touching that bounding box to be recalculated at the end of the graph update step. /// /// Typically you will not need to interact with this class yourself, instead you can use the pre-built components like . /// public class OffMeshLinks { AABBTree tree = new AABBTree(); List pendingAdd = new List(); bool updateScheduled; AstarPath astar; public OffMeshLinks(AstarPath astar) { this.astar = astar; } /// /// The start or end point of an off-mesh link. /// /// See: /// public struct Anchor { /// Where the link connects to the navmesh public Vector3 center; /// Rotation that the character should align itself with when traversing the link public Quaternion rotation; /// /// Width of the link. /// /// Note: No values other than 0 are currently supported. /// public float width; /// First point on the segment that makes up this anchor public readonly Vector3 point1 => center + rotation * new Vector3(-0.5f * width, 0, 0); /// Second point on the segment that makes up this anchor public readonly Vector3 point2 => center + rotation * new Vector3(0.5f * width, 0, 0); public static bool operator ==(Anchor a, Anchor b) => a.center == b.center && a.rotation == b.rotation && a.width == b.width; public static bool operator !=(Anchor a, Anchor b) => a.center != b.center || a.rotation != b.rotation || a.width != b.width; public override bool Equals(object obj) => obj is Anchor && this == (Anchor)obj; public override int GetHashCode() => (center.GetHashCode() * 23 ^ rotation.GetHashCode()) * 23 ^ width.GetHashCode(); } /// Determines how a link is connected in the graph public enum Directionality { /// Movement is only allowed from the start point to the end point OneWay, /// Movement is allowed in both directions TwoWay, } [System.Flags] public enum OffMeshLinkStatus { Inactive = 1 << 0, Pending = 1 << 1, Active = 1 << 2, FailedToConnectStart = Inactive | 1 << 3, FailedToConnectEnd = Inactive | 1 << 4, PendingRemoval = 1 << 5, } /// /// Information about an off-mesh link. /// /// Off-mesh links connect two points on the navmesh which are not necessarily connected by a normal navmesh connection. /// /// See: /// See: /// public readonly struct OffMeshLinkTracer { public OffMeshLinkTracer(OffMeshLinkConcrete link, bool reversed) { this.link = link; this.relativeStart = reversed ? link.end.center : link.start.center; this.relativeEnd = reversed ? link.start.center : link.end.center; this.isReverse = reversed; } public OffMeshLinkTracer(OffMeshLinkConcrete link, Vector3 relativeStart, Vector3 relativeEnd, bool isReverse) { this.link = link; this.relativeStart = relativeStart; this.relativeEnd = relativeEnd; this.isReverse = isReverse; } /// /// The off-mesh link that the agent is traversing. /// /// Note: If the off-mesh link is destroyed while the agent is traversing it, properties like , may refer to a destroyed gameObject. /// public readonly OffMeshLinkConcrete link; /// /// The start point of the off-mesh link from the agent's perspective. /// /// This is the point where the agent starts traversing the off-mesh link, regardless of if the link is traversed from the start to end or from end to start. /// public readonly Vector3 relativeStart; /// /// The end point of the off-mesh link from the agent's perspective. /// /// This is the point where the agent will finish traversing the off-mesh link, regardless of if the link is traversed from start to end or from end to start. /// public readonly Vector3 relativeEnd; /// /// True if the agent is traversing the off-mesh link from original link's end to its start point. /// /// Note: The and fields are always set from the agent's perspective. So the agent always moves from to . /// public readonly bool isReverse; /// \copydocref{OffMeshLinkSource.component} public Component component => link.component; /// \copydocref{OffMeshLinkSource.gameObject} public GameObject gameObject => link.gameObject; } public class OffMeshLinkSource { /// The start of the link public Anchor start; /// The end of the link public Anchor end; public Directionality directionality; /// /// Tag to apply to this link. /// /// See: tags (view in online documentation for working links) /// public PathfindingTag tag; /// Multiplies the cost of traversing this link by this amount public float costFactor; // TODO: Add constant cost? /// /// Maximum distance from the start/end points to the navmesh. /// /// If the distance is greater than this, the link will not be connected to the navmesh. /// public float maxSnappingDistance; /// /// Graph mask for which graphs the link is allowed to connect to. /// /// The link's endpoints will be connected to the closest valid node on any graph that matches the mask. /// public GraphMask graphMask; public IOffMeshLinkHandler handler; /// /// The Component associated with this link. /// /// Typically this will be a component. But users can also create their own components and fill out this field as appropriate. /// /// This field is not used for anything by the pathfinding system itself, it is only used to make it easier for users to find the component associated with a link. /// /// Warning: If the link has been destroyed, this may return a destroyed component. /// A link may be destroyed even while a character is traversing it. /// public Component component; /// /// The GameObject associated with this link. /// /// This field is not used for anything by the pathfinding system itself, it is only used to make it easier for users to find the GameObject associated with a link. /// /// Warning: If the link has been destroyed, this may return a destroyed GameObject. /// A link may be destroyed even while a character is traversing it. /// public GameObject gameObject => component != null ? component.gameObject : null; internal AABBTree.Key treeKey; public OffMeshLinkStatus status { get; internal set; } = OffMeshLinkStatus.Inactive; /// /// Bounding box which encapsulates the link and any position on the navmesh it could possibly be connected to. /// /// This is used to determine which links need to be recalculated when a graph update happens. /// public Bounds bounds { get { var b = new Bounds(); b.SetMinMax(start.point1, start.point2); b.Encapsulate(end.point1); b.Encapsulate(end.point2); b.Expand(maxSnappingDistance*2); return b; } } } internal class OffMeshLinkCombined { public OffMeshLinkSource source; public OffMeshLinkConcrete concrete; } public class OffMeshLinkConcrete { /// \copydocref{OffMeshLinkSource.start} public Anchor start; /// \copydocref{OffMeshLinkSource.end} public Anchor end; public GraphNode[] startNodes; public GraphNode[] endNodes; public LinkNode startLinkNode; public LinkNode endLinkNode; /// \copydocref{OffMeshLinkSource.directionality} public Directionality directionality; /// \copydocref{OffMeshLinkSource.tag} public PathfindingTag tag; public float costFactor; internal bool staleConnections; internal OffMeshLinkSource source; /// \copydocref{OffMeshLinkSource.handler} public IOffMeshLinkHandler handler => source.handler; /// \copydocref{OffMeshLinkSource.component} public Component component => source.component; /// \copydocref{OffMeshLinkSource.gameObject} public GameObject gameObject => source.component != null ? source.component.gameObject : null; // public Bounds bounds { // get { // var b = new Bounds(); // b.SetMinMax(start.point1, start.point2); // b.Encapsulate(end.point1); // b.Encapsulate(end.point2); // return b; // } // } public bool Equivalent (OffMeshLinkConcrete other) { if (start != other.start) return false; if (end != other.end) return false; if (startNodes.Length != other.startNodes.Length || endNodes.Length != other.endNodes.Length) return false; if (directionality != other.directionality || tag != other.tag || costFactor != other.costFactor) return false; for (int i = 0; i < startNodes.Length; i++) { if (startNodes[i] != other.startNodes[i]) return false; } for (int i = 0; i < endNodes.Length; i++) { if (endNodes[i] != other.endNodes[i]) return false; } return true; } public void Disconnect () { if (startLinkNode == null) { Assert.IsNull(endLinkNode); } else if (startLinkNode.Destroyed) { Assert.IsTrue(endLinkNode.Destroyed); } else { Assert.IsFalse(endLinkNode.Destroyed); var linkGraph = startLinkNode.Graph as LinkGraph; linkGraph.RemoveNode(startLinkNode); linkGraph.RemoveNode(endLinkNode); } startLinkNode = null; endLinkNode = null; } public void Connect (LinkGraph linkGraph, OffMeshLinkSource source) { Assert.IsNull(startLinkNode); Assert.IsNull(endLinkNode); startLinkNode = linkGraph.AddNode(); startLinkNode.linkSource = source; startLinkNode.linkConcrete = this; startLinkNode.position = (Int3)start.center; startLinkNode.Tag = tag; endLinkNode = linkGraph.AddNode(); endLinkNode.position = (Int3)end.center; endLinkNode.linkSource = source; endLinkNode.linkConcrete = this; endLinkNode.Tag = tag; for (int i = 0; i < startNodes.Length; i++) { var dist = (VectorMath.ClosestPointOnSegment(start.point1, start.point2, (Vector3)startNodes[i].position) - (Vector3)startNodes[i].position).magnitude; var cost = (uint)(Int3.Precision * dist); GraphNode.Connect(startNodes[i], startLinkNode, cost, directionality); } for (int i = 0; i < endNodes.Length; i++) { var dist = (VectorMath.ClosestPointOnSegment(end.point1, end.point2, (Vector3)endNodes[i].position) - (Vector3)endNodes[i].position).magnitude; var cost = (uint)(Int3.Precision * dist); GraphNode.Connect(endLinkNode, endNodes[i], cost, directionality); } var middleCost = (uint)(Int3.Precision * costFactor * (end.center - start.center).magnitude); GraphNode.Connect(startLinkNode, endLinkNode, middleCost, directionality); staleConnections = false; } public OffMeshLinkTracer GetTracer (LinkNode firstNode) { Assert.IsTrue(firstNode == startLinkNode || firstNode == endLinkNode); return new OffMeshLinkTracer(this, firstNode == endLinkNode); } } /// /// Get all graphs that this link is connected to. /// /// Returns: A list of all graphs that this link is connected to. This does not include the link graph. /// An empty list will be returned if the link is not connected to any graphs. /// /// Note: For lower GC pressure, the returned list should be pooled after you are done with it. See: pooling (view in online documentation for working links) /// /// The link to get connected graphs for. public List ConnectedGraphs (OffMeshLinkSource link) { var graphs = ListPool.Claim(); if (link.status != OffMeshLinkStatus.Active) return graphs; Assert.IsTrue(link.treeKey.isValid); var combined = tree[link.treeKey]; Assert.IsNotNull(combined.concrete); var concrete = combined.concrete; for (int i = 0; i < concrete.startNodes.Length; i++) { var graph = concrete.startNodes[i].Graph; if (!graphs.Contains(graph)) graphs.Add(graph); } for (int i = 0; i < concrete.endNodes.Length; i++) { var graph = concrete.endNodes[i].Graph; if (!graphs.Contains(graph)) graphs.Add(graph); } return graphs; } /// /// Adds a new off-mesh link. /// /// If any graphs change in the future, the link will automatically be updated to connect to the updated graphs. /// /// Note: The link will not be added immediately, it will be added at the end of the current graph update step. /// Or, if no graph update is currently running, a graph update will be scheduled, and the link will be added at the end of that update. /// This is to avoid modifying the graph during a graph update. /// /// See: /// /// The link to add. public void Add (OffMeshLinkSource link) { if (link == null) throw new ArgumentNullException("link"); if (link.status != OffMeshLinkStatus.Inactive) throw new System.ArgumentException("Link is already added"); pendingAdd.Add(link); link.status = OffMeshLinkStatus.Pending; ScheduleUpdate(); } internal void OnDisable () { var ls = new List(); tree.Query(new Bounds(Vector3.zero, Vector3.positiveInfinity), ls); for (int i = 0; i < ls.Count; i++) { ls[i].source.status = OffMeshLinkStatus.Inactive; ls[i].source.treeKey = default; } tree.Clear(); for (int i = 0; i < pendingAdd.Count; i++) { pendingAdd[i].status = OffMeshLinkStatus.Inactive; pendingAdd[i].treeKey = default; } pendingAdd.Clear(); } /// /// Removes an existing off-mesh link. /// /// Note: The link will not be removed immediately, it will be removed at the end of the current graph update step. /// Or, if no graph update is currently running, a graph update will be scheduled, and the link will be removed at the end of that update. /// This is to avoid modifying the graph during a graph update. /// /// See: /// /// The link to remove. If the link is already removed, nothing will be done. public void Remove (OffMeshLinkSource link) { if (link == null) throw new ArgumentNullException("link"); if (link.status == OffMeshLinkStatus.Inactive || (link.status & OffMeshLinkStatus.PendingRemoval) != 0) { return; } else if (link.status == OffMeshLinkStatus.Pending) { link.status = OffMeshLinkStatus.Inactive; pendingAdd.Remove(link); } else { link.status |= OffMeshLinkStatus.Pending | OffMeshLinkStatus.PendingRemoval; tree.Tag(link.treeKey); } Assert.IsTrue(link.status == OffMeshLinkStatus.Inactive || (link.status & OffMeshLinkStatus.PendingRemoval) != 0); ScheduleUpdate(); } NNConstraint cachedNNConstraint = NNConstraint.Walkable; bool ClampSegment (Anchor anchor, GraphMask graphMask, float maxSnappingDistance, out Anchor result, List nodes) { var nn = cachedNNConstraint; nn.distanceMetric = DistanceMetric.Euclidean; nn.graphMask = graphMask; Profiler.BeginSample("GetNearest"); var nearest = astar.GetNearest(0.5f*(anchor.point1 + anchor.point2), nn); Profiler.EndSample(); if (nearest.distanceCostSqr > maxSnappingDistance*maxSnappingDistance) nearest = default; if (nearest.node == null) { result = default; return false; } if (anchor.width > 0 && nearest.node.Graph is IRaycastableGraph rayGraph) { var offset = 0.5f * (anchor.point2 - anchor.point1); rayGraph.Linecast(nearest.position, nearest.position - offset, nearest.node, out var hit1, nodes); rayGraph.Linecast(nearest.position, nearest.position + offset, nearest.node, out var hit2, nodes); result = new Anchor { center = (hit1.point + hit2.point) * 0.5f, rotation = anchor.rotation, width = (hit1.point - hit2.point).magnitude, }; // Sort and deduplicate nodes.Sort((a, b) => a.NodeIndex.CompareTo(b.NodeIndex)); for (int j = nodes.Count - 1; j >= 0; j--) { var n = nodes[j]; for (int k = j - 1; k >= 0; k--) { if (nodes[k] == n) { nodes.RemoveAtSwapBack(j); break; } } } } else { result = new Anchor { center = nearest.position, rotation = anchor.rotation, width = 0f, }; nodes.Add(nearest.node); } return true; } /// /// Mark links touching the given bounds as dirty. /// /// The bounds should contain the surface of all nodes that have been changed. /// /// This will cause the links to be recalculated as soon as possible. /// /// Note: Since graphs should only be modified during graph updates, this method should also only be called during a graph update. /// public void DirtyBounds (Bounds bounds) { Profiler.BeginSample("DirtyBounds"); tree.Tag(bounds); Profiler.EndSample(); // Note: We don't have to call ScheduleUpdate here, because DirtyBounds will only be called during a work item/graph update } /// /// Mark a link as dirty. /// /// This will cause the link to be recalculated as soon as possible. /// public void Dirty (OffMeshLinkSource link) { DirtyNoSchedule(link); ScheduleUpdate(); } internal void DirtyNoSchedule (OffMeshLinkSource link) { tree.Tag(link.treeKey); } void ScheduleUpdate () { if (!updateScheduled && !astar.isScanning && !astar.IsAnyWorkItemInProgress) { updateScheduled = true; astar.AddWorkItem(() => {}); } } /// /// Get the nearest link to a point. /// /// Returns: The nearest link to the point or a default if no link was found. /// The returned struct contains both the link and information about which side of the link is closest to the point. /// If the end is closer than the start, then a reversed will be returned. /// /// Point to search around. /// Maximum distance to search. Use a small distance for better performance. public OffMeshLinkTracer GetNearest (Vector3 point, float maxDistance) { if (maxDistance < 0) return default; if (!float.IsFinite(maxDistance)) throw new System.ArgumentOutOfRangeException("maxDistance"); var ls = ListPool.Claim(); tree.Query(new Bounds(point, new Vector3(2*maxDistance, 2*maxDistance, 2*maxDistance)), ls); OffMeshLinkConcrete nearest = null; bool reversed = false; float nearestDist = maxDistance*maxDistance; for (int i = 0; i < ls.Count; i++) { var link = ls[i].concrete; var dist = VectorMath.SqrDistancePointSegment(link.start.point1, link.start.point2, point); if (dist < nearestDist) { nearestDist = dist; nearest = link; reversed = false; } dist = VectorMath.SqrDistancePointSegment(link.end.point1, link.end.point2, point); if (dist < nearestDist) { nearestDist = dist; nearest = link; reversed = true; } } ListPool.Release(ref ls); return nearest != null ? new OffMeshLinkTracer(nearest, reversed) : default; } internal void Refresh () { Profiler.BeginSample("Refresh Off-mesh links"); updateScheduled = false; var pendingUpdate = ListPool.Claim(); // Find all links that require updates // These have previously been tagged using the DirtyBounds method tree.QueryTagged(pendingUpdate, true); // Add all links to the tree which are pending insertion for (int i = 0; i < pendingAdd.Count; i++) { var link = pendingAdd[i]; Assert.IsTrue(link.status == OffMeshLinkStatus.Pending); var combined = new OffMeshLinkCombined { source = link, concrete = null, }; link.treeKey = tree.Add(link.bounds, combined); pendingUpdate.Add(combined); } pendingAdd.Clear(); List startNodes = ListPool.Claim(); List endNodes = ListPool.Claim(); for (int i = 0; i < pendingUpdate.Count; i++) { for (int j = 0; j < i; j++) { if (pendingUpdate[i].source == pendingUpdate[j].source) throw new System.Exception("Duplicate link"); } var source = pendingUpdate[i].source; var combined = tree[source.treeKey]; var prevConcrete = combined.concrete; if ((source.status & OffMeshLinkStatus.PendingRemoval) != 0) { if (prevConcrete != null) { prevConcrete.Disconnect(); combined.concrete = null; } tree.Remove(source.treeKey); source.treeKey = default; source.status = OffMeshLinkStatus.Inactive; continue; } startNodes.Clear(); if (!ClampSegment(source.start, source.graphMask, source.maxSnappingDistance, out var concreteStart, startNodes)) { if (prevConcrete != null) { prevConcrete.Disconnect(); combined.concrete = null; } source.status = OffMeshLinkStatus.FailedToConnectStart; continue; } endNodes.Clear(); if (!ClampSegment(source.end, source.graphMask, source.maxSnappingDistance, out var concreteEnd, endNodes)) { if (prevConcrete != null) { prevConcrete.Disconnect(); combined.concrete = null; } source.status = OffMeshLinkStatus.FailedToConnectEnd; continue; } var concrete = new OffMeshLinkConcrete { start = concreteStart, end = concreteEnd, startNodes = startNodes.ToArrayFromPool(), endNodes = endNodes.ToArrayFromPool(), source = source, directionality = source.directionality, tag = source.tag, costFactor = source.costFactor, }; if (prevConcrete != null && !prevConcrete.staleConnections && prevConcrete.Equivalent(concrete)) { // Nothing to do. The link is already connected like it should be. source.status &= ~OffMeshLinkStatus.Pending; Assert.AreNotEqual(OffMeshLinkStatus.Inactive, source.status); } else { // Remove previous connections if (prevConcrete != null) { prevConcrete.Disconnect(); ArrayPool.Release(ref prevConcrete.startNodes); ArrayPool.Release(ref prevConcrete.endNodes); } // Add new connections if (astar.data.linkGraph == null) astar.data.AddGraph(); concrete.Connect(astar.data.linkGraph, source); combined.concrete = concrete; source.status = OffMeshLinkStatus.Active; } } ListPool.Release(ref pendingUpdate); ListPool.Release(ref startNodes); ListPool.Release(ref endNodes); Profiler.EndSample(); } } }