using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Threading; #if UNITY_5_5_OR_NEWER using UnityEngine.Profiling; #endif namespace Pathfinding { #if NETFX_CORE using Thread = Pathfinding.WindowsStore.Thread; #else using Thread = System.Threading.Thread; #endif public class PathProcessor { public event System.Action OnPathPreSearch; public event System.Action OnPathPostSearch; public event System.Action OnQueueUnblocked; internal readonly ThreadControlQueue queue; readonly AstarPath astar; readonly PathReturnQueue returnQueue; readonly PathHandler[] pathHandlers; /// References to each of the pathfinding threads readonly Thread[] threads; /// /// When no multithreading is used, the IEnumerator is stored here. /// When no multithreading is used, a coroutine is used instead. It is not directly called with StartCoroutine /// but a separate function has just a while loop which increments the main IEnumerator. /// This is done so other functions can step the thread forward at any time, without having to wait for Unity to update it. /// See: CalculatePaths /// See: CalculatePathsHandler /// IEnumerator threadCoroutine; /// /// Holds the next node index which has not been used by any previous node. /// See: nodeIndexPool /// int nextNodeIndex = 1; /// /// Holds indices for nodes that have been destroyed. /// To avoid trashing a lot of memory structures when nodes are /// frequently deleted and created, node indices are reused. /// readonly Stack nodeIndexPool = new Stack(); readonly List locks = new List(); int nextLockID = 0; #if UNITY_2017_3_OR_NEWER CustomSampler profilingSampler; #endif /// /// Number of parallel pathfinders. /// Returns the number of concurrent processes which can calculate paths at once. /// When using multithreading, this will be the number of threads, if not using multithreading it is always 1 (since only 1 coroutine is used). /// See: threadInfos /// See: IsUsingMultithreading /// public int NumThreads { get { return pathHandlers.Length; } } /// Returns whether or not multithreading is used public bool IsUsingMultithreading { get { return threads != null; } } internal PathProcessor (AstarPath astar, PathReturnQueue returnQueue, int processors, bool multithreaded) { this.astar = astar; this.returnQueue = returnQueue; if (processors < 0) { throw new System.ArgumentOutOfRangeException("processors"); } if (!multithreaded && processors != 1) { throw new System.Exception("Only a single non-multithreaded processor is allowed"); } // Set up path queue with the specified number of receivers queue = new ThreadControlQueue(processors); pathHandlers = new PathHandler[processors]; for (int i = 0; i < processors; i++) { pathHandlers[i] = new PathHandler(i, processors); } if (multithreaded) { #if UNITY_2017_3_OR_NEWER profilingSampler = CustomSampler.Create("Calculating Path"); #endif threads = new Thread[processors]; // Start lots of threads for (int i = 0; i < processors; i++) { var pathHandler = pathHandlers[i]; threads[i] = new Thread(() => CalculatePathsThreaded(pathHandler)); #if !UNITY_SWITCH || UNITY_EDITOR // Note: Setting the thread name seems to crash when deploying for Switch: https://forum.arongranberg.com/t/path-processor-crashing-nintendo-switch-build/6584 threads[i].Name = "Pathfinding Thread " + i; #endif threads[i].IsBackground = true; threads[i].Start(); } } else { // Start coroutine if not using multithreading threadCoroutine = CalculatePaths(pathHandlers[0]); } } /// Prevents pathfinding from running while held public struct GraphUpdateLock { PathProcessor pathProcessor; int id; public GraphUpdateLock (PathProcessor pathProcessor, bool block) { this.pathProcessor = pathProcessor; id = pathProcessor.Lock(block); } /// /// True while this lock is preventing the pathfinding threads from processing more paths. /// Note that the pathfinding threads may not be paused yet (if this lock was obtained using PausePathfinding(false)). /// public bool Held { get { return pathProcessor != null && pathProcessor.locks.Contains(id); } } /// Allow pathfinding to start running again if no other locks are still held public void Release () { pathProcessor.Unlock(id); } } int Lock (bool block) { queue.Block(); if (block) { while (!queue.AllReceiversBlocked) { if (IsUsingMultithreading) { Thread.Sleep(1); } else { TickNonMultithreaded(); } } } nextLockID++; locks.Add(nextLockID); return nextLockID; } void Unlock (int id) { if (!locks.Remove(id)) { throw new System.ArgumentException("This lock has already been released"); } // Check if there are no remaining active locks if (locks.Count == 0) { if (OnQueueUnblocked != null) OnQueueUnblocked(); queue.Unblock(); } } /// /// Prevents pathfinding threads from starting to calculate any new paths. /// /// Returns: A lock object. You need to call Unlock on that object to allow pathfinding to resume. /// /// Note: In most cases this should not be called from user code. /// /// If true, this call will block until all pathfinding threads are paused. /// otherwise the threads will be paused as soon as they are done with what they are currently doing. public GraphUpdateLock PausePathfinding (bool block) { return new GraphUpdateLock(this, block); } public void TickNonMultithreaded () { // Process paths if (threadCoroutine != null) { try { threadCoroutine.MoveNext(); } catch (System.Exception e) { //This will kill pathfinding threadCoroutine = null; // Queue termination exceptions should be ignored, they are supposed to kill the thread if (!(e is ThreadControlQueue.QueueTerminationException)) { Debug.LogException(e); Debug.LogError("Unhandled exception during pathfinding. Terminating."); queue.TerminateReceivers(); // This will throw an exception supposed to kill the thread try { queue.PopNoBlock(false); } catch {} } } } } /// Calls 'Join' on each of the threads to block until they have completed public void JoinThreads () { if (threads != null) { for (int i = 0; i < threads.Length; i++) { if (!threads[i].Join(200)) { Debug.LogError("Could not terminate pathfinding thread["+i+"] in 200ms, trying Thread.Abort"); threads[i].Abort(); } } } } /// Calls 'Abort' on each of the threads public void AbortThreads () { if (threads == null) return; for (int i = 0; i < threads.Length; i++) { if (threads[i] != null && threads[i].IsAlive) threads[i].Abort(); } } /// /// Returns a new global node index. /// Warning: This method should not be called directly. It is used by the GraphNode constructor. /// public int GetNewNodeIndex () { return nodeIndexPool.Count > 0 ? nodeIndexPool.Pop() : nextNodeIndex++; } /// /// Initializes temporary path data for a node. /// Warning: This method should not be called directly. It is used by the GraphNode constructor. /// public void InitializeNode (GraphNode node) { if (!queue.AllReceiversBlocked) { throw new System.Exception("Trying to initialize a node when it is not safe to initialize any nodes. Must be done during a graph update. See http://arongranberg.com/astar/docs/graph-updates.php#direct"); } for (int i = 0; i < pathHandlers.Length; i++) { pathHandlers[i].InitializeNode(node); } astar.hierarchicalGraph.OnCreatedNode(node); } /// /// Destroyes the given node. /// This is to be called after the node has been disconnected from the graph so that it cannot be reached from any other nodes. /// It should only be called during graph updates, that is when the pathfinding threads are either not running or paused. /// /// Warning: This method should not be called by user code. It is used internally by the system. /// public void DestroyNode (GraphNode node) { if (node.NodeIndex == -1) return; nodeIndexPool.Push(node.NodeIndex); for (int i = 0; i < pathHandlers.Length; i++) { pathHandlers[i].DestroyNode(node); } astar.hierarchicalGraph.AddDirtyNode(node); } /// /// Main pathfinding method (multithreaded). /// This method will calculate the paths in the pathfinding queue when multithreading is enabled. /// /// See: CalculatePaths /// See: StartPath /// void CalculatePathsThreaded (PathHandler pathHandler) { #if UNITY_2017_3_OR_NEWER UnityEngine.Profiling.Profiler.BeginThreadProfiling("Pathfinding", "Pathfinding thread #" + (pathHandler.threadID+1)); #endif #if !ASTAR_FAST_BUT_NO_EXCEPTIONS try { #endif // Max number of ticks we are allowed to continue working in one run. // One tick is 1/10000 of a millisecond. // We need to check once in a while if the thread should be stopped. long maxTicks = (long)(10*10000); long targetTick = System.DateTime.UtcNow.Ticks + maxTicks; while (true) { // The path we are currently calculating Path path = queue.Pop(); #if UNITY_2017_3_OR_NEWER profilingSampler.Begin(); #endif // Access the internal implementation methods IPathInternals ipath = (IPathInternals)path; // Trying to prevent simple modding to allow more than one thread if (pathHandler.threadID > 0) { throw new System.Exception("Thread Error"); } AstarProfiler.StartFastProfile(0); ipath.PrepareBase(pathHandler); // Now processing the path // Will advance to Processing ipath.AdvanceState(PathState.Processing); // Call some callbacks if (OnPathPreSearch != null) { OnPathPreSearch(path); } // Tick for when the path started, used for calculating how long time the calculation took long startTicks = System.DateTime.UtcNow.Ticks; // Prepare the path ipath.Prepare(); AstarProfiler.EndFastProfile(0); if (path.CompleteState == PathCompleteState.NotCalculated) { // For visualization purposes, we set the last computed path to p, so we can view debug info on it in the editor (scene view). astar.debugPathData = ipath.PathHandler; astar.debugPathID = path.pathID; AstarProfiler.StartFastProfile(1); // Initialize the path, now ready to begin search ipath.Initialize(); AstarProfiler.EndFastProfile(1); // Loop while the path has not been fully calculated while (path.CompleteState == PathCompleteState.NotCalculated) { // Do some work on the path calculation. // The function will return when it has taken too much time // or when it has finished calculation AstarProfiler.StartFastProfile(2); ipath.CalculateStep(targetTick); AstarProfiler.EndFastProfile(2); targetTick = System.DateTime.UtcNow.Ticks + maxTicks; // Cancel function (and thus the thread) if no more paths should be accepted. // This is done when the A* object is about to be destroyed // The path is returned and then this function will be terminated if (queue.IsTerminating) { path.FailWithError("AstarPath object destroyed"); } } path.duration = (System.DateTime.UtcNow.Ticks - startTicks)*0.0001F; #if ProfileAstar System.Threading.Interlocked.Increment(ref AstarPath.PathsCompleted); System.Threading.Interlocked.Add(ref AstarPath.TotalSearchTime, System.DateTime.UtcNow.Ticks - startTicks); #endif } // Cleans up node tagging and other things ipath.Cleanup(); AstarProfiler.StartFastProfile(9); if (path.immediateCallback != null) path.immediateCallback(path); if (OnPathPostSearch != null) { OnPathPostSearch(path); } // Push the path onto the return stack // It will be detected by the main Unity thread and returned as fast as possible (the next late update hopefully) returnQueue.Enqueue(path); // Will advance to ReturnQueue ipath.AdvanceState(PathState.ReturnQueue); AstarProfiler.EndFastProfile(9); #if UNITY_2017_3_OR_NEWER profilingSampler.End(); #endif } #if !ASTAR_FAST_BUT_NO_EXCEPTIONS } catch (System.Exception e) { #if !NETFX_CORE if (e is ThreadAbortException || e is ThreadControlQueue.QueueTerminationException) #else if (e is ThreadControlQueue.QueueTerminationException) #endif { if (astar.logPathResults == PathLog.Heavy) Debug.LogWarning("Shutting down pathfinding thread #" + pathHandler.threadID); return; } Debug.LogException(e); Debug.LogError("Unhandled exception during pathfinding. Terminating."); // Unhandled exception, kill pathfinding queue.TerminateReceivers(); } finally { #if UNITY_2017_3_OR_NEWER UnityEngine.Profiling.Profiler.EndThreadProfiling(); #endif } #endif Debug.LogError("Error : This part should never be reached."); queue.ReceiverTerminated(); } /// /// Main pathfinding method. /// This method will calculate the paths in the pathfinding queue. /// /// See: CalculatePathsThreaded /// See: StartPath /// IEnumerator CalculatePaths (PathHandler pathHandler) { // Max number of ticks before yielding/sleeping long maxTicks = (long)(astar.maxFrameTime*10000); long targetTick = System.DateTime.UtcNow.Ticks + maxTicks; while (true) { // The path we are currently calculating Path p = null; AstarProfiler.StartProfile("Path Queue"); // Try to get the next path to be calculated bool blockedBefore = false; while (p == null) { try { p = queue.PopNoBlock(blockedBefore); blockedBefore |= p == null; } catch (ThreadControlQueue.QueueTerminationException) { yield break; } if (p == null) { AstarProfiler.EndProfile(); yield return null; AstarProfiler.StartProfile("Path Queue"); } } AstarProfiler.EndProfile(); AstarProfiler.StartProfile("Path Calc"); IPathInternals ip = (IPathInternals)p; // Max number of ticks we are allowed to continue working in one run // One tick is 1/10000 of a millisecond maxTicks = (long)(astar.maxFrameTime*10000); ip.PrepareBase(pathHandler); // Now processing the path // Will advance to Processing ip.AdvanceState(PathState.Processing); // Call some callbacks // It needs to be stored in a local variable to avoid race conditions var tmpOnPathPreSearch = OnPathPreSearch; if (tmpOnPathPreSearch != null) tmpOnPathPreSearch(p); // Tick for when the path started, used for calculating how long time the calculation took long startTicks = System.DateTime.UtcNow.Ticks; long totalTicks = 0; AstarProfiler.StartFastProfile(8); AstarProfiler.StartFastProfile(0); //Prepare the path AstarProfiler.StartProfile("Path Prepare"); ip.Prepare(); AstarProfiler.EndProfile("Path Prepare"); AstarProfiler.EndFastProfile(0); // Check if the Prepare call caused the path to complete // If this happens the path usually failed if (p.CompleteState == PathCompleteState.NotCalculated) { // For debug uses, we set the last computed path to p, so we can view debug info on it in the editor (scene view). astar.debugPathData = ip.PathHandler; astar.debugPathID = p.pathID; // Initialize the path, now ready to begin search AstarProfiler.StartProfile("Path Initialize"); ip.Initialize(); AstarProfiler.EndProfile(); // The error can turn up in the Init function while (p.CompleteState == PathCompleteState.NotCalculated) { // Do some work on the path calculation. // The function will return when it has taken too much time // or when it has finished calculation AstarProfiler.StartFastProfile(2); AstarProfiler.StartProfile("Path Calc Step"); ip.CalculateStep(targetTick); AstarProfiler.EndFastProfile(2); AstarProfiler.EndProfile(); // If the path has finished calculation, we can break here directly instead of sleeping // Improves latency if (p.CompleteState != PathCompleteState.NotCalculated) break; AstarProfiler.EndFastProfile(8); totalTicks += System.DateTime.UtcNow.Ticks-startTicks; // Yield/sleep so other threads can work AstarProfiler.EndProfile(); yield return null; AstarProfiler.StartProfile("Path Calc"); startTicks = System.DateTime.UtcNow.Ticks; AstarProfiler.StartFastProfile(8); // Cancel function (and thus the thread) if no more paths should be accepted. // This is done when the A* object is about to be destroyed // The path is returned and then this function will be terminated (see similar IF statement higher up in the function) if (queue.IsTerminating) { p.FailWithError("AstarPath object destroyed"); } targetTick = System.DateTime.UtcNow.Ticks + maxTicks; } totalTicks += System.DateTime.UtcNow.Ticks-startTicks; p.duration = totalTicks*0.0001F; #if ProfileAstar System.Threading.Interlocked.Increment(ref AstarPath.PathsCompleted); #endif } // Cleans up node tagging and other things ip.Cleanup(); AstarProfiler.EndFastProfile(8); // Call the immediate callback // It needs to be stored in a local variable to avoid race conditions var tmpImmediateCallback = p.immediateCallback; if (tmpImmediateCallback != null) tmpImmediateCallback(p); AstarProfiler.StartFastProfile(13); // It needs to be stored in a local variable to avoid race conditions var tmpOnPathPostSearch = OnPathPostSearch; if (tmpOnPathPostSearch != null) tmpOnPathPostSearch(p); AstarProfiler.EndFastProfile(13); // Push the path onto the return stack // It will be detected by the main Unity thread and returned as fast as possible (the next late update) returnQueue.Enqueue(p); ip.AdvanceState(PathState.ReturnQueue); AstarProfiler.EndProfile(); // Wait a bit if we have calculated a lot of paths if (System.DateTime.UtcNow.Ticks > targetTick) { yield return null; targetTick = System.DateTime.UtcNow.Ticks + maxTicks; } } } } }