using UnityEngine; using System.Collections.Generic; using Pathfinding.Serialization; using Pathfinding.Util; using UnityEngine.Assertions; namespace Pathfinding { using System; using Pathfinding.Drawing; using Unity.Jobs; /// /// Graph for off-mesh links. /// /// This is an internal graph type which is used to store off-mesh links. /// An off-mesh link between two nodes A and B is represented as: A <--> N1 <--> N2 <--> B . /// where N1 and N2 are two special nodes added to this graph at the exact start and endpoints of the link. /// /// This graph is not persistent. So it will never be saved to disk and a new one will be created each time the game starts. /// /// It is also not possible to query for the nearest node in this graph. The method will always return an empty result. /// This is by design, as all pathfinding should start on the navmesh, not on an off-mesh link. /// /// See: /// See: /// [JsonOptIn] [Pathfinding.Util.Preserve] public class LinkGraph : NavGraph { LinkNode[] nodes = new LinkNode[0]; int nodeCount; public override bool isScanned => true; public override bool persistent => false; public override bool showInInspector => false; public override int CountNodes() => nodeCount; protected override void DestroyAllNodes () { base.DestroyAllNodes(); nodes = new LinkNode[0]; nodeCount = 0; } public override void GetNodes (Action action) { if (nodes == null) return; for (int i = 0; i < nodeCount; i++) action(nodes[i]); } internal LinkNode AddNode () { AssertSafeToUpdateGraph(); if (nodeCount >= nodes.Length) { Memory.Realloc(ref nodes, Mathf.Max(16, nodeCount * 2)); } nodeCount++; return nodes[nodeCount-1] = new LinkNode(active) { nodeInGraphIndex = nodeCount - 1, GraphIndex = graphIndex, Walkable = true, }; } internal void RemoveNode (LinkNode node) { if (nodes[node.nodeInGraphIndex] != node) throw new ArgumentException("Node is not in this graph"); // Remove and swap with the last node nodeCount--; nodes[node.nodeInGraphIndex] = nodes[nodeCount]; nodes[node.nodeInGraphIndex].nodeInGraphIndex = node.nodeInGraphIndex; nodes[nodeCount] = null; node.Destroy(); } public override float NearestNodeDistanceSqrLowerBound(Vector3 position, NNConstraint constraint = null) => float.PositiveInfinity; /// /// It's not possible to query for the nearest node in a link graph. /// This method will always return an empty result. /// public override NNInfo GetNearest(Vector3 position, NNConstraint constraint, float maxDistanceSqr) => default; public override void OnDrawGizmos (DrawingData gizmos, bool drawNodes, RedrawScope redrawScope) { // We rely on the link components themselves to draw the links // TODO base.OnDrawGizmos(gizmos, drawNodes, redrawScope); } class LinkGraphUpdatePromise : IGraphUpdatePromise { public LinkGraph graph; public void Apply (IGraphUpdateContext ctx) { // Destroy all previous nodes (if any) graph.DestroyAllNodes(); } public IEnumerator Prepare() => null; } protected override IGraphUpdatePromise ScanInternal () => new LinkGraphUpdatePromise { graph = this }; } public class LinkNode : PointNode { public OffMeshLinks.OffMeshLinkSource linkSource; public OffMeshLinks.OffMeshLinkConcrete linkConcrete; public int nodeInGraphIndex; public LinkNode() { } public LinkNode(AstarPath active) : base(active) {} public override void RemovePartialConnection (GraphNode node) { linkConcrete.staleConnections = true; // Mark the link as dirty so that it will be recalculated. // Ensure that this does not immediately schedule an update. // Nodes should only be updated during work items and while graphs are scanned, // and in those cases node links will be refreshed anyway. // However, this can also trigger when the AstarPath component is being destroyed, // or when a graph is removed. In those cases, we don't want to schedule an update. AstarPath.active.offMeshLinks.DirtyNoSchedule(linkSource); base.RemovePartialConnection(node); } public override void Open (Path path, uint pathNodeIndex, uint gScore) { // Note: Not calling path.OpenCandidateConnectionsToEndNode here, because link nodes should never be the end node of a path if (connections == null) return; var pathHandler = (path as IPathInternals).PathHandler; var pn = pathHandler.pathNodes[pathNodeIndex]; // Check if our parent node was also a link node by checking if it is in the same graph as this node. // If it is, we are allowed to connect to non-link nodes. // Otherwise, we are at the start of the link and we must only connect to other link nodes. // This is to avoid the path going to a link node, and then going directly back to a non-link node // without actually traversing the link. It would technically be a valid path, // but it causes confusion for other scripts that look for off-mesh links in the path. // TODO: Store the other link node as a field to be able to do a more robust check here? var isEndOfLink = !pathHandler.IsTemporaryNode(pn.parentIndex) && pathHandler.GetNode(pn.parentIndex).GraphIndex == GraphIndex; var canTraverseNonLinkNodes = isEndOfLink; for (int i = 0; i < connections.Length; i++) { GraphNode other = connections[i].node; if (canTraverseNonLinkNodes == (other.GraphIndex != GraphIndex) && path.CanTraverse(this, other)) { if (other is PointNode) { path.OpenCandidateConnection(pathNodeIndex, other.NodeIndex, gScore, connections[i].cost, 0, other.position); } else { // When connecting to a non-link node, use a special function to open the connection. // The typical case for this is that we are at the end of an off-mesh link and we are connecting to a navmesh node. // In that case, this node's position is in the interior of the navmesh node. We let the navmesh node decide how // that should be handled. other.OpenAtPoint(path, pathNodeIndex, position, gScore); } } } } public override void OpenAtPoint (Path path, uint pathNodeIndex, Int3 pos, uint gScore) { if (path.CanTraverse(this)) { // Note: Not calling path.OpenCandidateConnectionsToEndNode here, because link nodes should never be the end node of a path var cost = (uint)(pos - this.position).costMagnitude; path.OpenCandidateConnection(pathNodeIndex, NodeIndex, gScore, cost, 0, position); } } } }