using System.Collections.Generic; using Unity.Collections; using UnityEngine; namespace Pathfinding { using Pathfinding.Collections; using Unity.Profiling; /// /// NNConstraint which also takes an into account. /// /// Paths will automatically use this if an ITraversalProvider is set on the path. /// public class NNConstraintWithTraversalProvider : NNConstraint { public ITraversalProvider traversalProvider; public NNConstraint baseConstraint; public Path path; public void Reset () { traversalProvider = null; baseConstraint = null; path = null; } public bool isSet => traversalProvider != null; public void Set (Path path, NNConstraint constraint, ITraversalProvider traversalProvider) { this.path = path; this.traversalProvider = traversalProvider; // Note: We need to pass most requests to the base constraint, because it may be a subclass of NNConstraint and have additional logic baseConstraint = constraint; // We also need to copy some fields to this instance, because // some fields are used directly. Primarily distanceMetric and constrainDistance, // but we copy all of them for good measure. this.graphMask = constraint.graphMask; this.constrainArea = constraint.constrainArea; this.area = constraint.area; this.distanceMetric = constraint.distanceMetric; this.constrainWalkability = constraint.constrainWalkability; this.walkable = constraint.walkable; this.constrainTags = constraint.constrainTags; this.tags = constraint.tags; this.constrainDistance = constraint.constrainDistance; } public override bool SuitableGraph (int graphIndex, NavGraph graph) { return baseConstraint.SuitableGraph(graphIndex, graph); } public override bool Suitable (GraphNode node) { return baseConstraint.Suitable(node) && traversalProvider.CanTraverse(path, node); } } /// /// Stores temporary node data for a single pathfinding request. /// Every node has one PathNode per thread used. /// It stores e.g G score, H score and other temporary variables needed /// for path calculation, but which are not part of the graph structure. /// /// See: Pathfinding.PathHandler /// See: https://en.wikipedia.org/wiki/A*_search_algorithm /// public struct PathNode { /// The path request (in this thread, if multithreading is used) which last used this node public ushort pathID; /// /// Index of the node in the binary heap. /// The open list in the A* algorithm is backed by a binary heap. /// To support fast 'decrease key' operations, the index of the node /// is saved here. /// public ushort heapIndex; /// Bitpacked variable which stores several fields private uint flags; public static readonly PathNode Default = new PathNode { pathID = 0, heapIndex = BinaryHeap.NotInHeap, flags = 0 }; /// Parent index uses the first 26 bits private const uint ParentIndexMask = (1U << 26) - 1U; private const int FractionAlongEdgeOffset = 26; private const uint FractionAlongEdgeMask = ((1U << 30) - 1U) & ~ParentIndexMask; public const int FractionAlongEdgeQuantization = 1 << (30 - 26); public static uint ReverseFractionAlongEdge(uint v) => (FractionAlongEdgeQuantization - 1) - v; public static uint QuantizeFractionAlongEdge (float v) { v *= FractionAlongEdgeQuantization - 1; v += 0.5f; return Unity.Mathematics.math.clamp((uint)v, 0, FractionAlongEdgeQuantization - 1); } public static float UnQuantizeFractionAlongEdge (uint v) { return (float)v * (1.0f / (FractionAlongEdgeQuantization - 1)); } /// Flag 1 is at bit 30 private const int Flag1Offset = 30; private const uint Flag1Mask = 1U << Flag1Offset; /// Flag 2 is at bit 31 private const int Flag2Offset = 31; private const uint Flag2Mask = 1U << Flag2Offset; public uint fractionAlongEdge { get => (flags & FractionAlongEdgeMask) >> FractionAlongEdgeOffset; set => flags = (flags & ~FractionAlongEdgeMask) | ((value << FractionAlongEdgeOffset) & FractionAlongEdgeMask); } public uint parentIndex { get => flags & ParentIndexMask; set => flags = (flags & ~ParentIndexMask) | value; } /// /// Use as temporary flag during pathfinding. /// Path types can use this during pathfinding to mark /// nodes. When done, this flag should be reverted to its default state (false) to /// avoid messing up other pathfinding requests. /// public bool flag1 { get => (flags & Flag1Mask) != 0; set => flags = (flags & ~Flag1Mask) | (value ? Flag1Mask : 0U); } /// /// Use as temporary flag during pathfinding. /// Path types can use this during pathfinding to mark /// nodes. When done, this flag should be reverted to its default state (false) to /// avoid messing up other pathfinding requests. /// public bool flag2 { get => (flags & Flag2Mask) != 0; set => flags = (flags & ~Flag2Mask) | (value ? Flag2Mask : 0U); } } public enum TemporaryNodeType { Start, End, Ignore, } public struct TemporaryNode { public uint associatedNode; public Int3 position; public int targetIndex; public TemporaryNodeType type; } /// Handles thread specific path data. public class PathHandler { /// /// Current PathID. /// See: /// private ushort pathID; public readonly int threadID; public readonly int totalThreadCount; public readonly NNConstraintWithTraversalProvider constraintWrapper = new NNConstraintWithTraversalProvider(); internal readonly GlobalNodeStorage nodeStorage; public int numTemporaryNodes { [IgnoredByDeepProfiler] get; private set; } /// /// All path nodes with an index greater or equal to this are temporary nodes that only exist for the duration of a single path. /// /// This is a copy of NodeStorage.nextNodeIndex. This is used to avoid having to access the NodeStorage while pathfinding as it's an extra indirection. /// public uint temporaryNodeStartIndex { [IgnoredByDeepProfiler] get; private set; } UnsafeSpan temporaryNodes; /// /// Reference to the per-node data for this thread. /// /// Note: Only guaranteed to point to a valid allocation while the path is being calculated. /// /// Be careful when storing copies of this array, as it may be re-allocated by the AddTemporaryNode method. /// public UnsafeSpan pathNodes; #if UNITY_EDITOR UnsafeSpan debugPathNodes; #endif /// /// Binary heap to keep track of nodes on the "Open list". /// See: https://en.wikipedia.org/wiki/A*_search_algorithm /// public BinaryHeap heap = new BinaryHeap(128); /// ID for the path currently being calculated or last path that was calculated public ushort PathID { get { return pathID; } } /// /// StringBuilder that paths can use to build debug strings. /// Better for performance and memory usage to use a single StringBuilder instead of each path creating its own /// public readonly System.Text.StringBuilder DebugStringBuilder = new System.Text.StringBuilder(); internal PathHandler (GlobalNodeStorage nodeStorage, int threadID, int totalThreadCount) { this.threadID = threadID; this.totalThreadCount = totalThreadCount; this.nodeStorage = nodeStorage; temporaryNodes = default; } public void InitializeForPath (Path p) { var lastPathId = pathID; pathID = p.pathID; numTemporaryNodes = 0; // Get the path nodes for this thread (may have been resized since last we calculated a path) pathNodes = nodeStorage.pathfindingThreadData[threadID].pathNodes; // The index at which temporary nodes start. The GlobalNodeStorage allocates some extra path nodes // for use as temporary nodes. This is usually a small number, as they are only needed for the start/end points, // and for off-mesh links (in some cases). // We could hypothetically start already at nodeStorage.nextNodeIndex, but this could lead to us // allocating an unnecessarily large array for the #temporaryNodes array, which would be a waste of memory. temporaryNodeStartIndex = nodeStorage.reservedPathNodeData; // The number of temporary nodes we are allowed to use during this path calculation. // Note: This value will never shrink between path calculations, because the GlobalNodeStorage will never // reduce the number of temporary nodes it reserves. var tempNodeCount = pathNodes.Length - (int)temporaryNodeStartIndex; if (tempNodeCount > temporaryNodes.Length) { temporaryNodes = temporaryNodes.Reallocate(Allocator.Persistent, tempNodeCount); } #if UNITY_EDITOR var astar = AstarPath.active; var shouldLog = astar.showGraphs && (astar.debugMode == GraphDebugMode.F || astar.debugMode == GraphDebugMode.H || astar.debugMode == GraphDebugMode.G || astar.showSearchTree); debugPathNodes = shouldLog ? nodeStorage.pathfindingThreadData[threadID].debugPathNodes : default; #endif // Path IDs have overflowed 65K, cleanup is needed to avoid bugs where we think // we have already visited a node when we haven't. // Since pathIDs are handed out sequentially, we can check if the new path id // is smaller than the last one. if (pathID < lastPathId) { ClearPathIDs(); } } /// /// Returns the PathNode corresponding to the specified node. /// The PathNode is specific to this PathHandler since multiple PathHandlers /// are used at the same time if multithreading is enabled. /// public PathNode GetPathNode (GraphNode node, uint variant = 0) { return pathNodes[node.NodeIndex + variant]; } public bool IsTemporaryNode(uint pathNodeIndex) => pathNodeIndex >= temporaryNodeStartIndex; /// /// Add a new temporary node for this path request. /// /// Warning: This may invalidate all memory references to path nodes in this path. /// public uint AddTemporaryNode (TemporaryNode node) { if (numTemporaryNodes >= temporaryNodes.Length) { // Reallocate the node storage to fit more temporary nodes // This will invalidate all references to path nodes and temporary nodes, // so we must ensure that no `ref` variables life across this call. nodeStorage.GrowTemporaryNodeStorage(threadID); pathNodes = nodeStorage.pathfindingThreadData[threadID].pathNodes; temporaryNodes = temporaryNodes.Reallocate(Allocator.Persistent, pathNodes.Length - (int)temporaryNodeStartIndex); } var index = temporaryNodeStartIndex + (uint)numTemporaryNodes; temporaryNodes[numTemporaryNodes] = node; pathNodes[index] = PathNode.Default; numTemporaryNodes++; return index; } public GraphNode GetNode(uint nodeIndex) => nodeStorage.GetNode(nodeIndex); public ref TemporaryNode GetTemporaryNode (uint nodeIndex) { if (nodeIndex < temporaryNodeStartIndex || nodeIndex >= temporaryNodeStartIndex + numTemporaryNodes) throw new System.ArgumentOutOfRangeException(); return ref temporaryNodes[(int)(nodeIndex - temporaryNodeStartIndex)]; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void LogVisitedNode (uint pathNodeIndex, uint h, uint g) { #if UNITY_EDITOR if (debugPathNodes.Length > 0 && !IsTemporaryNode(pathNodeIndex)) { var parent = pathNodes[pathNodeIndex].parentIndex; debugPathNodes[pathNodeIndex] = new GlobalNodeStorage.DebugPathNode { h = h, g = g, parentIndex = parent >= temporaryNodeStartIndex ? 0 : parent, pathID = pathID, fractionAlongEdge = (byte)pathNodes[pathNodeIndex].fractionAlongEdge, }; } #endif } /// /// Set all nodes' pathIDs to 0. /// See: Pathfinding.PathNode.pathID /// public void ClearPathIDs () { for (int i = 0; i < pathNodes.Length; i++) { pathNodes[i].pathID = 0; } } public void Dispose () { heap.Dispose(); temporaryNodes.Free(Allocator.Persistent); pathNodes = default; } } }