diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs | 1095 |
1 files changed, 1095 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs new file mode 100644 index 0000000..51f5975 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs @@ -0,0 +1,1095 @@ +//#define ASTAR_POOL_DEBUG //@SHOWINEDITOR Enables debugging of path pooling. Will log warnings and info messages about paths not beeing pooled correctly. + +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using Unity.Mathematics; + +namespace Pathfinding { + /// <summary>Base class for all path types</summary> + [Unity.Burst.BurstCompile] + public abstract class Path : IPathInternals { +#if ASTAR_POOL_DEBUG + private string pathTraceInfo = ""; + private List<string> claimInfo = new List<string>(); + ~Path() { + Debug.Log("Destroying " + GetType().Name + " instance"); + if (claimed.Count > 0) { + Debug.LogWarning("Pool Is Leaking. See list of claims:\n" + + "Each message below will list what objects are currently claiming the path." + + " These objects have removed their reference to the path object but has not called .Release on it (which is bad).\n" + pathTraceInfo+"\n"); + for (int i = 0; i < claimed.Count; i++) { + Debug.LogWarning("- Claim "+ (i+1) + " is by a " + claimed[i].GetType().Name + "\n"+claimInfo[i]); + } + } else { + Debug.Log("Some scripts are not using pooling.\n" + pathTraceInfo + "\n"); + } + } +#endif + + /// <summary>Data for the thread calculating this path</summary> + protected PathHandler pathHandler; + + /// <summary> + /// Callback to call when the path is complete. + /// This is usually sent to the Seeker component which post processes the path and then calls a callback to the script which requested the path + /// </summary> + public OnPathDelegate callback; + + /// <summary> + /// Immediate callback to call when the path is complete. + /// Warning: This may be called from a separate thread. Usually you do not want to use this one. + /// + /// See: callback + /// </summary> + public OnPathDelegate immediateCallback; + + /// <summary>Returns the state of the path in the pathfinding pipeline</summary> + public PathState PipelineState { get; private set; } + + /// <summary> + /// Provides additional traversal information to a path request. + /// See: traversal_provider (view in online documentation for working links) + /// </summary> + public ITraversalProvider traversalProvider; + + + /// <summary>Backing field for <see cref="CompleteState"/></summary> + protected PathCompleteState completeState; + + /// <summary> + /// Current state of the path. + /// Bug: This may currently be set to Complete before the path has actually been fully calculated. In particular the vectorPath and path lists may not have been fully constructed. + /// This can lead to race conditions when using multithreading. Try to avoid using this method to check for if the path is calculated right now, use <see cref="IsDone"/> instead. + /// </summary> + public PathCompleteState CompleteState { + get { return completeState; } + protected set { + // Locking is used to avoid multithreading race conditions in which, for example, + // the error state is set on the main thread to cancel the path, + // and then a pathfinding thread marks the path as completed, + // which would replace the error state (if a lock and check would not have been used). + // We lock on the path object itself. Users should rarely have to use the path object + // themselves for anything before the path is calculated, much less take a lock on it. + lock (this) { + // Once the path is put in the error state, it cannot be set to any other state + if (completeState != PathCompleteState.Error) completeState = value; + } + } + } + + /// <summary> + /// If the path failed, this is true. + /// See: <see cref="errorLog"/> + /// See: This is equivalent to checking path.CompleteState == PathCompleteState.Error + /// </summary> + public bool error { get { return CompleteState == PathCompleteState.Error; } } + + /// <summary> + /// Additional info on why a path failed. + /// See: <see cref="AstarPath.logPathResults"/> + /// </summary> + public string errorLog { get; private set; } + + /// <summary> + /// Holds the path as a <see cref="GraphNode"/> list. + /// + /// These are all nodes that the path traversed, as calculated by the pathfinding algorithm. + /// This may not be the same nodes as the post processed path traverses. + /// + /// See: <see cref="vectorPath"/> + /// </summary> + public List<GraphNode> path; + + /// <summary> + /// Holds the (possibly post-processed) path as a Vector3 list. + /// + /// This list may be modified by path modifiers to be smoother or simpler compared to the raw path generated by the pathfinding algorithm. + /// + /// See: modifiers (view in online documentation for working links) + /// See: <see cref="path"/> + /// </summary> + public List<Vector3> vectorPath; + + /// <summary>How long it took to calculate this path in milliseconds</summary> + public float duration; + + /// <summary>Number of nodes this path has searched</summary> + public int searchedNodes { get; protected set; } + + /// <summary> + /// True if the path is currently pooled. + /// Do not set this value. Only read. It is used internally. + /// + /// See: <see cref="PathPool"/> + /// </summary> + bool IPathInternals.Pooled { get; set; } + + /// <summary> + /// True if the Reset function has been called. + /// Used to alert users when they are doing something wrong. + /// </summary> + protected bool hasBeenReset; + + /// <summary>Constraint for how to search for nodes</summary> + public NNConstraint nnConstraint = PathNNConstraint.Walkable; + + /// <summary>Determines which heuristic to use</summary> + public Heuristic heuristic; + + /// <summary> + /// Scale of the heuristic values. + /// See: AstarPath.heuristicScale + /// </summary> + public float heuristicScale = 1F; + + /// <summary>ID of this path. Used to distinguish between different paths</summary> + public ushort pathID { get; private set; } + + /// <summary>Target to use for H score calculation.</summary> + protected GraphNode hTargetNode; + + /// <summary> + /// Target to use for H score calculations. + /// See: https://en.wikipedia.org/wiki/Admissible_heuristic + /// </summary> + protected HeuristicObjective heuristicObjective; + + internal ref HeuristicObjective heuristicObjectiveInternal => ref heuristicObjective; + + /// <summary> + /// Which graph tags are traversable. + /// This is a bitmask so -1 = all bits set = all tags traversable. + /// For example, to set bit 5 to true, you would do + /// <code> myPath.enabledTags |= 1 << 5; </code> + /// To set it to false, you would do + /// <code> myPath.enabledTags &= ~(1 << 5); </code> + /// + /// The Seeker has a popup field where you can set which tags to use. + /// Note: If you are using a Seeker. The Seeker will set this value to what is set in the inspector field on StartPath. + /// So you need to change the Seeker value via script, not set this value if you want to change it via script. + /// + /// See: <see cref="CanTraverse"/> + /// See: bitmasks (view in online documentation for working links) + /// </summary> + public int enabledTags = -1; + + /// <summary>List of zeroes to use as default tag penalties</summary> + internal static readonly int[] ZeroTagPenalties = new int[32]; + + /// <summary> + /// The tag penalties that are actually used. + /// See: <see cref="tagPenalties"/> + /// </summary> + protected int[] internalTagPenalties; + + /// <summary> + /// Penalties for each tag. + /// Tag 0 which is the default tag, will get a penalty of tagPenalties[0]. + /// These should only be non-negative values since the A* algorithm cannot handle negative penalties. + /// + /// When assigning an array to this property it must have a length of 32. + /// + /// Note: Setting this to null will make all tag penalties be treated as if they are zero. + /// + /// Note: If you are using a Seeker. The Seeker will set this value to what is set in the inspector field when you call seeker.StartPath. + /// So you need to change the Seeker's value via script, not set this value. + /// + /// See: <see cref="Seeker.tagPenalties"/> + /// </summary> + public int[] tagPenalties { + get { + return internalTagPenalties == ZeroTagPenalties ? null : internalTagPenalties; + } + set { + if (value == null) { + internalTagPenalties = ZeroTagPenalties; + } else { + if (value.Length != 32) throw new System.ArgumentException("tagPenalties must have a length of 32"); + + internalTagPenalties = value; + } + } + } + + /// <summary>Copies the given settings into this path</summary> + public void UseSettings (PathRequestSettings settings) { + nnConstraint.graphMask = settings.graphMask; + traversalProvider = settings.traversalProvider; + enabledTags = settings.traversableTags; + tagPenalties = settings.tagPenalties; + } + + /// <summary> + /// Total Length of the path. + /// Calculates the total length of the <see cref="vectorPath"/>. + /// Cache this rather than call this function every time since it will calculate the length every time, not just return a cached value. + /// Returns: Total length of <see cref="vectorPath"/>, if <see cref="vectorPath"/> is null positive infinity is returned. + /// </summary> + public float GetTotalLength () { + if (vectorPath == null) return float.PositiveInfinity; + float tot = 0; + for (int i = 0; i < vectorPath.Count-1; i++) tot += Vector3.Distance(vectorPath[i], vectorPath[i+1]); + return tot; + } + + /// <summary> + /// Waits until this path has been calculated and returned. + /// Allows for very easy scripting. + /// + /// <code> + /// IEnumerator Start () { + /// // Get the seeker component attached to this GameObject + /// var seeker = GetComponent<Seeker>(); + /// + /// var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, null); + /// // Wait... This may take a frame or two depending on how complex the path is + /// // The rest of the game will continue to run while we wait + /// yield return StartCoroutine(path.WaitForPath()); + /// // The path is calculated now + /// + /// // Draw the path in the scene view for 10 seconds + /// for (int i = 0; i < path.vectorPath.Count - 1; i++) { + /// Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10); + /// } + /// } + /// </code> + /// + /// Note: Do not confuse this with AstarPath.BlockUntilCalculated. This one will wait using yield until it has been calculated + /// while AstarPath.BlockUntilCalculated will halt all operations until the path has been calculated. + /// + /// Throws: System.InvalidOperationException if the path is not started. Send the path to <see cref="Seeker.StartPath(Path)"/> or <see cref="AstarPath.StartPath"/> before calling this function. + /// + /// See: <see cref="BlockUntilCalculated"/> + /// See: https://docs.unity3d.com/Manual/Coroutines.html + /// </summary> + public IEnumerator WaitForPath () { + if (PipelineState == PathState.Created) throw new System.InvalidOperationException("This path has not been started yet"); + + while (PipelineState != PathState.Returned) yield return null; + } + + /// <summary> + /// Blocks until this path has been calculated and returned. + /// Normally it takes a few frames for a path to be calculated and returned. + /// This function will ensure that the path will be calculated when this function returns + /// and that the callback for that path has been called. + /// + /// Use this function only if you really need to. + /// There is a point to spreading path calculations out over several frames. + /// It smoothes out the framerate and makes sure requesting a large + /// number of paths at the same time does not cause lag. + /// + /// Note: Graph updates and other callbacks might get called during the execution of this function. + /// + /// <code> + /// var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete); + /// path.BlockUntilCalculated(); + /// + /// // The path is calculated now, and the OnPathComplete callback has been called + /// </code> + /// + /// See: This is equivalent to calling <see cref="AstarPath.BlockUntilCalculated(Path)"/> + /// See: <see cref="WaitForPath"/> + /// </summary> + public void BlockUntilCalculated () { + AstarPath.BlockUntilCalculated(this); + } + + /// <summary> + /// True if this path node might be worth exploring. + /// + /// This is used during a search to filter out nodes which have already been fully searched. + /// </summary> + public bool ShouldConsiderPathNode (uint pathNodeIndex) { + var node = pathHandler.pathNodes[pathNodeIndex]; + return node.pathID != pathID || node.heapIndex != BinaryHeap.NotInHeap; + } + + public static readonly Unity.Profiling.ProfilerMarker MarkerOpenCandidateConnectionsToEnd = new Unity.Profiling.ProfilerMarker("OpenCandidateConnectionsToEnd"); + public static readonly Unity.Profiling.ProfilerMarker MarkerTrace = new Unity.Profiling.ProfilerMarker("Trace"); + + /// <summary> + /// Open a connection to the temporary end node if necessary. + /// + /// The start and end nodes are temporary nodes and are not included in the graph itself. + /// This means that we need to handle connections to and from those nodes as a special case. + /// This function will open a connection from the given node to the end node, if such a connection exists. + /// + /// It is called from the <see cref="GraphNode.Open"/> function. + /// </summary> + /// <param name="position">Position of the path node that is being opened. This may be different from the node's position if \reflink{PathNode.fractionAlongEdge} is being used.</param> + /// <param name="parentPathNode">Index of the path node that is being opened. This is often the same as parentNodeIndex, but may be different if the node has multiple path node variants.</param> + /// <param name="parentNodeIndex">Index of the node that is being opened.</param> + /// <param name="parentG">G score of the parent node. The cost to reach the parent node from the start of the path.</param> + public void OpenCandidateConnectionsToEndNode (Int3 position, uint parentPathNode, uint parentNodeIndex, uint parentG) { + // True iff this node has a connection to one or more temporary nodes + if (pathHandler.pathNodes[parentNodeIndex].flag1) { + MarkerOpenCandidateConnectionsToEnd.Begin(); + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End && node.associatedNode == parentNodeIndex) { + var cost = (uint)(position - node.position).costMagnitude; + OpenCandidateConnection(parentPathNode, nodeIndex, parentG, cost, 0, node.position); + } + } + MarkerOpenCandidateConnectionsToEnd.End(); + } + } + + /// <summary> + /// Opens a connection between two nodes during the A* search. + /// + /// When a node is "opened" (i.e. searched by the A* algorithm), it will open connections to all its neighbours. + /// This function checks those connections to see if passing through the node to its neighbour is the best way to reach the neighbour that we have seen so far, + /// and if so, it will push the neighbour onto the search heap. + /// </summary> + /// <param name="parentPathNode">The node that is being opened.</param> + /// <param name="targetPathNode">A neighbour of the parent that is being considered.</param> + /// <param name="parentG">The G value of the parent node. This is the cost to reach the parent node from the start of the path.</param> + /// <param name="connectionCost">The cost of moving from the parent node to the target node.</param> + /// <param name="fractionAlongEdge">Internal value used by the TriangleMeshNode to store where on the shared edge between the nodes we say we cross over.</param> + /// <param name="targetNodePosition">The position of the target node. This is used by the heuristic to estimate the cost to reach the end node.</param> + public void OpenCandidateConnection (uint parentPathNode, uint targetPathNode, uint parentG, uint connectionCost, uint fractionAlongEdge, Int3 targetNodePosition) { + if (!ShouldConsiderPathNode(targetPathNode)) { + // We have seen this node before, but it is not in the heap. + // This means we have already processed it and it must have had a better F score than this node (or the heuristic was not admissable). + // We can safely discard this connection. + return; + } + + uint candidateEnteringCost; + uint targetNodeIndex; + if (pathHandler.IsTemporaryNode(targetPathNode)) { + candidateEnteringCost = 0; + targetNodeIndex = 0; + } else { + var targetNode = pathHandler.GetNode(targetPathNode); + candidateEnteringCost = GetTraversalCost(targetNode); + targetNodeIndex = targetNode.NodeIndex; + } + var candidateG = parentG + connectionCost + candidateEnteringCost; + var pars = new OpenCandidateParams { + pathID = pathID, + parentPathNode = parentPathNode, + targetPathNode = targetPathNode, + targetNodeIndex = targetNodeIndex, + candidateG = candidateG, + fractionAlongEdge = fractionAlongEdge, + targetNodePosition = (int3)targetNodePosition, + pathNodes = pathHandler.pathNodes, + }; + OpenCandidateConnectionBurst(ref pars, + ref pathHandler.heap, ref heuristicObjective + ); + } + + /// <summary> + /// Parameters to OpenCandidateConnectionBurst. + /// Using a struct instead of passing the parameters as separate arguments is significantly faster. + /// </summary> + public struct OpenCandidateParams { + public Util.UnsafeSpan<PathNode> pathNodes; + public uint parentPathNode; + public uint targetPathNode; + public uint targetNodeIndex; + public uint candidateG; + public uint fractionAlongEdge; + public int3 targetNodePosition; + public ushort pathID; + } + + /// <summary> + /// Burst-compiled internal implementation of OpenCandidateConnection. + /// Compiling it using burst provides a decent 25% speedup. + /// The function itself is much faster, but the overhead of calling it from C# is quite significant. + /// </summary> + [Unity.Burst.BurstCompile] + public static void OpenCandidateConnectionBurst (ref OpenCandidateParams pars, ref BinaryHeap heap, ref HeuristicObjective heuristicObjective) { + var pathID = pars.pathID; + var parentPathNode = pars.parentPathNode; + var targetPathNode = pars.targetPathNode; + var candidateG = pars.candidateG; + var fractionAlongEdge = pars.fractionAlongEdge; + var targetNodePosition = pars.targetNodePosition; + var pathNodes = pars.pathNodes; + ref var target = ref pathNodes[targetPathNode]; + if (target.pathID != pathID) { + // This is the first time we have seen this node. This connection must be optimal. + target.fractionAlongEdge = fractionAlongEdge; + target.pathID = pathID; + target.parentIndex = parentPathNode; + var candidateH = (uint)heuristicObjective.Calculate(targetNodePosition, pars.targetNodeIndex); + var candidateF = candidateG + candidateH; + heap.Add(pathNodes, targetPathNode, candidateG, candidateF); + } else { + // Note: Before this method is called, a check is done for the case target.pathID==pathID && heapIndex == NotInHeap, + // so we know target.heapIndex != NotInHeap here. + + // We have seen this node before and it is in the heap. + // Now we check if this path to the target node is better than the previous one. + + var targetG = heap.GetG(target.heapIndex); + // The previous F score of the node + var targetF = heap.GetF(target.heapIndex); + var targetH = targetF - targetG; + uint candidateH; + + if (target.fractionAlongEdge != fractionAlongEdge) { + // Different fractionAlongEdge, this means that targetNodePosition may have changed + // and therefore the heuristic may also have changed. + candidateH = (uint)heuristicObjective.Calculate(targetNodePosition, pars.targetNodeIndex); + } else { + // If fractionAlongEdge has not changed, then we assume the heuristic is also the same. + // This saves us from having to calculate it again. + candidateH = targetH; + } + + var candidateF = candidateG + candidateH; + if (candidateF < targetF) { + // This connection is better than the previous one. + target.fractionAlongEdge = fractionAlongEdge; + target.parentIndex = parentPathNode; + heap.Add(pathNodes, targetPathNode, candidateG, candidateF); + } else { + // This connection is not better than the previous one. + // We can safely discard this connection. + } + } + } + + /// <summary>Returns penalty for the given tag.</summary> + /// <param name="tag">A value between 0 (inclusive) and 32 (exclusive).</param> + public uint GetTagPenalty (int tag) { + return (uint)internalTagPenalties[tag]; + } + + /// <summary> + /// Returns if the node can be traversed. + /// This by default equals to if the node is walkable and if the node's tag is included in <see cref="enabledTags"/>. + /// + /// See: <see cref="traversalProvider"/> + /// </summary> + public bool CanTraverse (GraphNode node) { + // Use traversal provider if set, otherwise fall back on default behaviour + // This method is hot, but this branch is extremely well predicted so it + // doesn't affect performance much (profiling indicates it is just above + // the noise level, somewhere around 0%-0.3%) + if (traversalProvider != null) + return traversalProvider.CanTraverse(this, node); + + // Manually inlined code from DefaultITraversalProvider + unchecked { return node.Walkable && (enabledTags >> (int)node.Tag & 0x1) != 0; } + } + + + /// <summary> + /// Returns if the path can traverse a link between from and to and if to can be traversed itself. + /// This by default equals to if the to is walkable and if the to's tag is included in <see cref="enabledTags"/>. + /// + /// See: <see cref="traversalProvider"/> + /// </summary> + public bool CanTraverse (GraphNode from, GraphNode to) { + // Use traversal provider if set, otherwise fall back on default behaviour + // This method is hot, but this branch is extremely well predicted so it + // doesn't affect performance much (profiling indicates it is just above + // the noise level, somewhere around 0%-0.3%) + if (traversalProvider != null) + return traversalProvider.CanTraverse(this, from, to); + + // Manually inlined code from DefaultITraversalProvider + unchecked { return to.Walkable && (enabledTags >> (int)to.Tag & 0x1) != 0; } + } + + /// <summary>Returns the cost of traversing the given node</summary> + public uint GetTraversalCost (GraphNode node) { +#if ASTAR_NO_TRAVERSAL_COST + return 0; +#else + // Use traversal provider if set, otherwise fall back on default behaviour + if (traversalProvider != null) + return traversalProvider.GetTraversalCost(this, node); + + unchecked { return GetTagPenalty((int)node.Tag) + node.Penalty; } +#endif + } + + /// <summary> + /// True if this path is done calculating. + /// + /// Note: The callback for the path might not have been called yet. + /// + /// See: <see cref="Seeker.IsDone"/> which also takes into account if the path callback has been called and had modifiers applied. + /// </summary> + public bool IsDone () { + return PipelineState > PathState.Processing; + } + + /// <summary>Threadsafe increment of the state</summary> + void IPathInternals.AdvanceState (PathState s) { + lock (this) { + PipelineState = (PathState)System.Math.Max((int)PipelineState, (int)s); + } + } + + /// <summary>Causes the path to fail and sets <see cref="errorLog"/> to msg</summary> + public void FailWithError (string msg) { + Error(); + if (errorLog != "") errorLog += "\n" + msg; + else errorLog = msg; + } + + /// <summary> + /// Aborts the path because of an error. + /// Sets <see cref="error"/> to true. + /// This function is called when an error has occurred (e.g a valid path could not be found). + /// See: <see cref="FailWithError"/> + /// </summary> + public void Error () { + CompleteState = PathCompleteState.Error; + } + + /// <summary> + /// Performs some error checking. + /// Makes sure the user isn't using old code paths and that no major errors have been made. + /// + /// Causes the path to fail if any errors are found. + /// </summary> + private void ErrorCheck () { + if (!hasBeenReset) FailWithError("Please use the static Construct function for creating paths, do not use the normal constructors."); + if (((IPathInternals)this).Pooled) FailWithError("The path is currently in a path pool. Are you sending the path for calculation twice?"); + if (pathHandler == null) FailWithError("Field pathHandler is not set. Please report this bug."); + if (PipelineState > PathState.Processing) FailWithError("This path has already been processed. Do not request a path with the same path object twice."); + } + + /// <summary> + /// Called when the path enters the pool. + /// This method should release e.g pooled lists and other pooled resources + /// The base version of this method releases vectorPath and path lists. + /// Reset() will be called after this function, not before. + /// Warning: Do not call this function manually. + /// </summary> + protected virtual void OnEnterPool () { + if (vectorPath != null) Pathfinding.Util.ListPool<Vector3>.Release(ref vectorPath); + if (path != null) Pathfinding.Util.ListPool<GraphNode>.Release(ref path); + // Clear the callback to remove a potential memory leak + // while the path is in the pool (which it could be for a long time). + callback = null; + immediateCallback = null; + traversalProvider = null; + pathHandler = null; + } + + /// <summary> + /// Reset all values to their default values. + /// + /// Note: All inheriting path types (e.g ConstantPath, RandomPath, etc.) which declare their own variables need to + /// override this function, resetting ALL their variables to enable pooling of paths. + /// If this is not done, trying to use that path type for pooling could result in weird behaviour. + /// The best way is to reset to default values the variables declared in the extended path type and then + /// call the base function in inheriting types with base.Reset(). + /// </summary> + protected virtual void Reset () { +#if ASTAR_POOL_DEBUG + pathTraceInfo = "This path was got from the pool or created from here (stacktrace):\n"; + pathTraceInfo += System.Environment.StackTrace; +#endif + + if (System.Object.ReferenceEquals(AstarPath.active, null)) + throw new System.NullReferenceException("No AstarPath object found in the scene. " + + "Make sure there is one or do not create paths in Awake"); + + hasBeenReset = true; + PipelineState = (int)PathState.Created; + releasedNotSilent = false; + + pathHandler = null; + callback = null; + immediateCallback = null; + errorLog = ""; + completeState = PathCompleteState.NotCalculated; + + path = Pathfinding.Util.ListPool<GraphNode>.Claim(); + vectorPath = Pathfinding.Util.ListPool<Vector3>.Claim(); + + duration = 0; + searchedNodes = 0; + + nnConstraint = PathNNConstraint.Walkable; + + heuristic = AstarPath.active.heuristic; + heuristicScale = AstarPath.active.heuristicScale; + + enabledTags = -1; + tagPenalties = null; + + pathID = AstarPath.active.GetNextPathID(); + + hTargetNode = null; + + traversalProvider = null; + } + + /// <summary>List of claims on this path with reference objects</summary> + private List<System.Object> claimed = new List<System.Object>(); + + /// <summary> + /// True if the path has been released with a non-silent call yet. + /// + /// See: Release + /// See: Claim + /// </summary> + private bool releasedNotSilent; + + /// <summary> + /// Increase the reference count on this path by 1 (for pooling). + /// A claim on a path will ensure that it is not pooled. + /// If you are using a path, you will want to claim it when you first get it and then release it when you will not + /// use it anymore. When there are no claims on the path, it will be reset and put in a pool. + /// + /// This is essentially just reference counting. + /// + /// The object passed to this method is merely used as a way to more easily detect when pooling is not done correctly. + /// It can be any object, when used from a movement script you can just pass "this". This class will throw an exception + /// if you try to call Claim on the same path twice with the same object (which is usually not what you want) or + /// if you try to call Release with an object that has not been used in a Claim call for that path. + /// The object passed to the Claim method needs to be the same as the one you pass to this method. + /// + /// See: Release + /// See: Pool + /// See: pooling (view in online documentation for working links) + /// See: https://en.wikipedia.org/wiki/Reference_counting + /// </summary> + public void Claim (System.Object o) { + if (System.Object.ReferenceEquals(o, null)) throw new System.ArgumentNullException("o"); + + for (int i = 0; i < claimed.Count; i++) { + // Need to use ReferenceEquals because it might be called from another thread + if (System.Object.ReferenceEquals(claimed[i], o)) + throw new System.ArgumentException("You have already claimed the path with that object ("+o+"). Are you claiming the path with the same object twice?"); + } + + claimed.Add(o); +#if ASTAR_POOL_DEBUG + claimInfo.Add(o.ToString() + "\n\nClaimed from:\n" + System.Environment.StackTrace); +#endif + } + + /// <summary> + /// Reduces the reference count on the path by 1 (pooling). + /// Removes the claim on the path by the specified object. + /// When the reference count reaches zero, the path will be pooled, all variables will be cleared and the path will be put in a pool to be used again. + /// This is great for performance since fewer allocations are made. + /// + /// If the silent parameter is true, this method will remove the claim by the specified object + /// but the path will not be pooled if the claim count reches zero unless a non-silent Release call has been made earlier. + /// This is used by the internal pathfinding components such as Seeker and AstarPath so that they will not cause paths to be pooled. + /// This enables users to skip the claim/release calls if they want without the path being pooled by the Seeker or AstarPath and + /// thus causing strange bugs. + /// + /// See: Claim + /// See: PathPool + /// </summary> + public void Release (System.Object o, bool silent = false) { + if (o == null) throw new System.ArgumentNullException("o"); + + for (int i = 0; i < claimed.Count; i++) { + // Need to use ReferenceEquals because it might be called from another thread + if (System.Object.ReferenceEquals(claimed[i], o)) { + claimed.RemoveAt(i); +#if ASTAR_POOL_DEBUG + claimInfo.RemoveAt(i); +#endif + if (!silent) { + releasedNotSilent = true; + } + + if (claimed.Count == 0 && releasedNotSilent) { + PathPool.Pool(this); + } + return; + } + } + if (claimed.Count == 0) { + throw new System.ArgumentException("You are releasing a path which is not claimed at all (most likely it has been pooled already). " + + "Are you releasing the path with the same object ("+o+") twice?" + + "\nCheck out the documentation on path pooling for help."); + } + throw new System.ArgumentException("You are releasing a path which has not been claimed with this object ("+o+"). " + + "Are you releasing the path with the same object twice?\n" + + "Check out the documentation on path pooling for help."); + } + + /// <summary> + /// Traces the calculated path from the end node to the start. + /// This will build an array (<see cref="path)"/> of the nodes this path will pass through and also set the <see cref="vectorPath"/> array to the <see cref="path"/> arrays positions. + /// Assumes the <see cref="vectorPath"/> and <see cref="path"/> are empty and not null (which will be the case for a correctly initialized path). + /// </summary> + protected virtual void Trace (uint fromPathNodeIndex) { + MarkerTrace.Begin(); + // Current node we are processing + var c = fromPathNodeIndex; + int count = 0; + var pathNodes = pathHandler.pathNodes; + + while (c != 0) { + c = pathNodes[c].parentIndex; + count++; + if (count > 16384) { + Debug.LogWarning("Infinite loop? >16384 node path. Remove this message if you really have that long paths (Path.cs, Trace method)"); + break; + } + } + + // Ensure the lists have enough capacity + if (path.Capacity < count) path.Capacity = count; + UnityEngine.Assertions.Assert.AreEqual(0, path.Count); + + c = fromPathNodeIndex; + + GraphNode lastNode = null; + for (int i = 0; i < count; i++) { + GraphNode node; + if (pathHandler.IsTemporaryNode(c)) { + node = pathHandler.GetNode(pathHandler.GetTemporaryNode(c).associatedNode); + } else { + node = pathHandler.GetNode(c); + } + // If a node has multiple variants (like the triangle mesh node), then we may visit + // the same node multiple times in a sequence (but different variants of it). + // In the final path we don't want the duplicates. + if (node != lastNode) { + path.Add(node); + lastNode = node; + } + c = pathNodes[c].parentIndex; + } + + // Reverse + count = path.Count; + int half = count/2; + for (int i = 0; i < half; i++) { + var tmp = path[i]; + path[i] = path[count-i-1]; + path[count - i - 1] = tmp; + } + + if (vectorPath.Capacity < count) vectorPath.Capacity = count; + for (int i = 0; i < count; i++) { + vectorPath.Add((Vector3)path[i].position); + } + MarkerTrace.End(); + } + + /// <summary> + /// Writes text shared for all overrides of DebugString to the string builder. + /// See: DebugString + /// </summary> + protected void DebugStringPrefix (PathLog logMode, System.Text.StringBuilder text) { + text.Append(error ? "Path Failed : " : "Path Completed : "); + text.Append("Computation Time "); + text.Append(duration.ToString(logMode == PathLog.Heavy ? "0.000 ms " : "0.00 ms ")); + + text.Append("Searched Nodes ").Append(searchedNodes); + + if (!error) { + text.Append(" Path Length "); + text.Append(path == null ? "Null" : path.Count.ToString()); + } + } + + /// <summary> + /// Writes text shared for all overrides of DebugString to the string builder. + /// See: DebugString + /// </summary> + protected void DebugStringSuffix (PathLog logMode, System.Text.StringBuilder text) { + if (error) { + text.Append("\nError: ").Append(errorLog); + } + + // Can only print this from the Unity thread + // since otherwise an exception might be thrown + if (logMode == PathLog.Heavy && !AstarPath.active.IsUsingMultithreading) { + text.Append("\nCallback references "); + if (callback != null) text.Append(callback.Target.GetType().FullName).AppendLine(); + else text.AppendLine("NULL"); + } + + text.Append("\nPath Number ").Append(pathID).Append(" (unique id)"); + } + + /// <summary> + /// Returns a string with information about it. + /// More information is emitted when logMode == Heavy. + /// An empty string is returned if logMode == None + /// or logMode == OnlyErrors and this path did not fail. + /// </summary> + protected virtual string DebugString (PathLog logMode) { + if (logMode == PathLog.None || (!error && logMode == PathLog.OnlyErrors)) { + return ""; + } + + // Get a cached string builder for this thread + System.Text.StringBuilder text = pathHandler.DebugStringBuilder; + text.Length = 0; + + DebugStringPrefix(logMode, text); + DebugStringSuffix(logMode, text); + + return text.ToString(); + } + + /// <summary>Calls callback to return the calculated path. See: <see cref="callback"/></summary> + protected virtual void ReturnPath () { + if (callback != null) { + callback(this); + } + } + + /// <summary> + /// Prepares low level path variables for calculation. + /// Called before a path search will take place. + /// Always called before the Prepare, Initialize and CalculateStep functions + /// </summary> + protected void PrepareBase (PathHandler pathHandler) { + //Make sure the path has a reference to the pathHandler + this.pathHandler = pathHandler; + //Assign relevant path data to the pathHandler + pathHandler.InitializeForPath(this); + + // Make sure that internalTagPenalties is an array which has the length 32 + if (internalTagPenalties == null || internalTagPenalties.Length != 32) + internalTagPenalties = ZeroTagPenalties; + + try { + ErrorCheck(); + } catch (System.Exception e) { + FailWithError(e.Message); + } + } + + /// <summary> + /// Called before the path is started. + /// Called right before Initialize + /// </summary> + protected abstract void Prepare(); + + /// <summary> + /// Always called after the path has been calculated. + /// Guaranteed to be called before other paths have been calculated on + /// the same thread. + /// Use for cleaning up things like node tagging and similar. + /// </summary> + protected virtual void Cleanup () { + // Cleanup any flags set by temporary nodes + var pathNodes = pathHandler.pathNodes; + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + + var associatedNode = pathHandler.GetNode(node.associatedNode); + for (uint v = 0; v < associatedNode.PathNodeVariants; v++) { + pathNodes[node.associatedNode + v].flag1 = false; + pathNodes[node.associatedNode + v].flag2 = false; + } + } + } + + protected int3 FirstTemporaryEndNode () { + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End) { + return (int3)node.position; + } + } + throw new System.InvalidOperationException("There are no end nodes in the path"); + } + + + protected void TemporaryEndNodesBoundingBox (out int3 mn, out int3 mx) { + // These represent a bounding box containing all valid end points. + // Typically there's only one end point, but in some cases there can be more. + mn = (int3)int.MaxValue; + mx = (int3)int.MinValue; + + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End) { + mn = math.min(mn, (int3)node.position); + mx = math.max(mx, (int3)node.position); + } + } + } + + protected void MarkNodesAdjacentToTemporaryEndNodes () { + var pathNodes = pathHandler.pathNodes; + + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End) { + // Mark node with flag1 to mark it as a node connected to an end node + var associatedNode = pathHandler.GetNode(node.associatedNode); + for (uint v = 0; v < associatedNode.PathNodeVariants; v++) { + pathNodes[node.associatedNode + v].flag1 = true; + } + } + } + } + + protected void AddStartNodesToHeap () { + var pathNodes = pathHandler.pathNodes; + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.Start) { + // Note: Setting F score to 0 is technically incorrect, but it doesn't + // matter since we will open the start nodes first anyway. + pathHandler.heap.Add(pathNodes, nodeIndex, 0, 0); + } + } + } + + /// <summary> + /// Called when there are no more nodes to search. + /// + /// This may be used to calculate a partial path as a fallback. + /// </summary> + protected abstract void OnHeapExhausted(); + + /// <summary> + /// Called when a valid node has been found for the end of the path. + /// + /// This function should trace the path back to the start node, and set CompleteState to Complete. + /// If CompleteState is unchanged, the search will continue. + /// </summary> + protected abstract void OnFoundEndNode(uint pathNode, uint hScore, uint gScore); + + /// <summary> + /// Called for every node that the path visits. + /// + /// This is used by path types to check if the target node has been reached, to log debug data, etc. + /// </summary> + public virtual void OnVisitNode (uint pathNode, uint hScore, uint gScore) {} + + /// <summary> + /// Calculates the path until completed or until the time has passed targetTick. + /// Usually a check is only done every 500 nodes if the time has passed targetTick. + /// Time/Ticks are got from System.DateTime.UtcNow.Ticks. + /// + /// Basic outline of what the function does for the standard path (Pathfinding.ABPath). + /// <code> + /// while the end has not been found and no error has occurred + /// pop the next node of the heap and set it as current + /// check if we have reached the end + /// if so, exit and return the path + /// + /// open the current node, i.e loop through its neighbours, mark them as visited and put them on a heap + /// + /// check if there are still nodes left to process (or have we searched the whole graph) + /// if there are none, flag error and exit + /// + /// check if the function has exceeded the time limit + /// if so, return and wait for the function to get called again + /// </code> + /// </summary> + protected virtual void CalculateStep (long targetTick) { + int counter = 0; + var pathNodes = pathHandler.pathNodes; + var temporaryNodeStartIndex = pathHandler.temporaryNodeStartIndex; + + // Continue to search as long as we haven't encountered an error and we haven't found the target + while (CompleteState == PathCompleteState.NotCalculated) { + searchedNodes++; + + // Any nodes left to search? + if (pathHandler.heap.isEmpty) { + OnHeapExhausted(); + return; + } + + // Select the node with the lowest F score and remove it from the open list + var currentPathNodeIndex = pathHandler.heap.Remove(pathNodes, out uint currentNodeG, out uint currentNodeF); + var currentNodeH = currentNodeF - currentNodeG; + + if (currentPathNodeIndex >= temporaryNodeStartIndex) { + // This is a special node + var node = pathHandler.GetTemporaryNode(currentPathNodeIndex); + if (node.type == TemporaryNodeType.Start) { + // A start node. We should open the associated node at this point + pathHandler.GetNode(node.associatedNode).OpenAtPoint(this, currentPathNodeIndex, node.position, currentNodeG); + } else if (node.type == TemporaryNodeType.End) { + // An end node. Yay! We found the path we wanted. + // Now we can just trace the path back to the start and return that. + // However, some path types may choose to continue the search to find more end points (e.g. the multi target path). + { + // Make sure we visit the node associated with the end node. + // This is usually redundant, but it can matter in some cases. + // In particular, triangle mesh nodes can be opened in such a way that the temporary end node + // gets a lower F score than the individual sides of the triangle. This means that the temporary end + // node will be searched before the triangle sides are searched and that might complete the path. + // This would lead to us never actually calling LogVisitedNode for the triangle node, if we didn't have this code. + pathHandler.LogVisitedNode(node.associatedNode, currentNodeH, currentNodeG); + } + OnFoundEndNode(currentPathNodeIndex, currentNodeH, currentNodeG); + if (CompleteState == PathCompleteState.Complete) { + return; + } + } + } else { + pathHandler.LogVisitedNode(currentPathNodeIndex, currentNodeH, currentNodeG); + + OnVisitNode(currentPathNodeIndex, currentNodeH, currentNodeG); + + // Loop through all walkable neighbours of the node and add them to the open list. + var node = pathHandler.GetNode(currentPathNodeIndex); + node.Open(this, currentPathNodeIndex, currentNodeG); + } + + // Check for time every 500 nodes, roughly every 0.5 ms usually + if (counter > 500) { + // Have we exceded the maxFrameTime, if so we should wait one frame before continuing the search since we don't want the game to lag + if (System.DateTime.UtcNow.Ticks >= targetTick) { + return; + } + counter = 0; + + // Mostly for development + if (searchedNodes > 1000000) { + throw new System.Exception("Probable infinite loop. Over 1,000,000 nodes searched"); + } + } + + counter++; + } + } + + PathHandler IPathInternals.PathHandler { get { return pathHandler; } } + void IPathInternals.OnEnterPool () { OnEnterPool(); } + void IPathInternals.Reset () { Reset(); } + void IPathInternals.ReturnPath () { ReturnPath(); } + void IPathInternals.PrepareBase (PathHandler handler) { PrepareBase(handler); } + void IPathInternals.Prepare () { Prepare(); } + void IPathInternals.Cleanup () { Cleanup(); } + void IPathInternals.CalculateStep (long targetTick) { CalculateStep(targetTick); } + string IPathInternals.DebugString (PathLog logMode) { return DebugString(logMode); } + } + + /// <summary>Used for hiding internal methods of the Path class</summary> + internal interface IPathInternals { + PathHandler PathHandler { get; } + bool Pooled { get; set; } + void AdvanceState(PathState s); + void OnEnterPool(); + void Reset(); + void ReturnPath(); + void PrepareBase(PathHandler handler); + void Prepare(); + void Cleanup(); + void CalculateStep(long targetTick); + string DebugString(PathLog logMode); + } +} |