using System.Collections.Generic; using UnityEngine; namespace Pathfinding { using Pathfinding.Jobs; using Pathfinding.Util; using Unity.Jobs; using Unity.Profiling; using UnityEngine.Assertions; using UnityEngine.Profiling; /// /// Promise representing a graph update. /// /// This is used internally by the system to represent a graph update. /// Generally you shouldn't need to care about it, unless you are implementing your own graph type. /// public interface IGraphUpdatePromise { /// /// Returns the progress of the update. /// /// This should be a value between 0 and 1. /// float Progress => 0.0f; /// /// Coroutine to prepare an update asynchronously. /// /// If a JobHandle is returned, it will be awaited before the coroutine is ticked again, and before the method is called. /// /// After this coroutine has finished, the method will be called. /// /// Note: No changes must be made to the graph in this method. Those should only be done in the method. /// /// May return null if no async work is required. /// IEnumerator Prepare() => null; /// /// Applies the update in a single atomic update. /// /// It is done as a single atomic update (from the main thread's perspective) to ensure /// that even if one does an async scan or update, the graph will always be in a valid state. /// This guarantees that things like GetNearest will still work during an async scan. /// /// Warning: Must only be called after the method has finished. /// // TODO: Pass in a JobHandle and allow returning a JobHandle? void Apply(IGraphUpdateContext context); } /// /// Helper functions for graph updates. /// /// A context is passed to graphs when they are updated, and to work items when they are executed. /// The interface inherits from this interface. /// public interface IGraphUpdateContext { /// /// Mark a particular region of the world as having been changed. /// /// This should be used whenever graphs are changed. /// /// This is used to recalculate off-mesh links that touch these bounds, and it will also ensure events are callled. /// /// The bounding box should cover the surface of all nodes that have been updated. /// It is fine to use a larger bounding box than necessary (even an infinite one), though this may be slower, since more off-mesh links need to be recalculated. /// You can even use an infinitely large bounding box if you don't want to bother calculating a more accurate one. /// You can also call this multiple times to dirty multiple bounding boxes. /// void DirtyBounds(Bounds bounds); } class GraphUpdateProcessor { /// Holds graphs that can be updated readonly AstarPath astar; /// Used for IsAnyGraphUpdateInProgress bool anyGraphUpdateInProgress; /// /// Queue containing all waiting graph update queries. Add to this queue by using \link AddToQueue \endlink. /// See: AddToQueue /// readonly Queue graphUpdateQueue = new Queue(); readonly List<(IGraphUpdatePromise, IEnumerator)> pendingPromises = new List<(IGraphUpdatePromise, IEnumerator)>(); readonly List pendingGraphUpdates = new List(); /// Returns if any graph updates are waiting to be applied public bool IsAnyGraphUpdateQueued { get { return graphUpdateQueue.Count > 0; } } /// Returns if any graph updates are in progress public bool IsAnyGraphUpdateInProgress { get { return anyGraphUpdateInProgress; } } public GraphUpdateProcessor (AstarPath astar) { this.astar = astar; } /// Work item which can be used to apply all queued updates public AstarWorkItem GetWorkItem () { return new AstarWorkItem(QueueGraphUpdatesInternal, ProcessGraphUpdates); } /// /// Update all graphs using the GraphUpdateObject. /// This can be used to, e.g make all nodes in an area unwalkable, or set them to a higher penalty. /// The graphs will be updated as soon as possible (with respect to AstarPath.batchGraphUpdates) /// /// See: FlushGraphUpdates /// public void AddToQueue (GraphUpdateObject ob) { // Put the GUO in the queue graphUpdateQueue.Enqueue(ob); } /// /// Discards all queued graph updates. /// /// Graph updates that are already in progress will not be discarded. /// public void DiscardQueued () { while (graphUpdateQueue.Count > 0) { graphUpdateQueue.Dequeue().internalStage = GraphUpdateObject.STAGE_ABORTED; } } /// Schedules graph updates internally void QueueGraphUpdatesInternal (IWorkItemContext context) { Assert.IsTrue(pendingGraphUpdates.Count == 0); while (graphUpdateQueue.Count > 0) { var ob = graphUpdateQueue.Dequeue(); pendingGraphUpdates.Add(ob); if (ob.internalStage != GraphUpdateObject.STAGE_PENDING) { Debug.LogError("Expected remaining graph update to be pending"); continue; } } foreach (IUpdatableGraph g in astar.data.GetUpdateableGraphs()) { NavGraph gr = g as NavGraph; var updates = ListPool.Claim(); for (int i = 0; i < pendingGraphUpdates.Count; i++) { GraphUpdateObject ob = pendingGraphUpdates[i]; if (ob.nnConstraint == null || ob.nnConstraint.SuitableGraph((int)gr.graphIndex, gr)) { updates.Add(ob); } } if (updates.Count > 0) { var promise = g.ScheduleGraphUpdates(updates); if (promise != null) { var it = promise.Prepare(); pendingPromises.Add((promise, it)); } else { ListPool.Release(ref updates); } } else { ListPool.Release(ref updates); } } context.PreUpdate(); anyGraphUpdateInProgress = true; } static readonly ProfilerMarker MarkerSleep = new ProfilerMarker(ProfilerCategory.Loading, "Sleep"); static readonly ProfilerMarker MarkerCalculate = new ProfilerMarker("Calculating Graph Update"); static readonly ProfilerMarker MarkerApply = new ProfilerMarker("Applying Graph Update"); /// /// Updates graphs. /// Will do some graph updates, possibly signal another thread to do them. /// Will only process graph updates added by QueueGraphUpdatesInternal /// /// Returns: True if all graph updates have been done and pathfinding (or other tasks) may resume. /// False if there are still graph updates being processed or waiting in the queue. /// /// Helper methods for the work items. /// If true, all graph updates will be processed before this function returns. The return value /// will be True. bool ProcessGraphUpdates (IWorkItemContext context, bool force) { Assert.IsTrue(anyGraphUpdateInProgress); if (pendingPromises.Count > 0) { if (!ProcessGraphUpdatePromises(pendingPromises, context, force)) { return false; } pendingPromises.Clear(); } anyGraphUpdateInProgress = false; for (int i = 0; i < pendingGraphUpdates.Count; i++) { pendingGraphUpdates[i].internalStage = GraphUpdateObject.STAGE_APPLIED; } pendingGraphUpdates.Clear(); return true; } public static bool ProcessGraphUpdatePromises (List<(IGraphUpdatePromise, IEnumerator)> promises, IGraphUpdateContext context, bool force = false) { TimeSlice timeSlice = TimeSlice.MillisFromNow(2f); TimeSlice idleTimeSlice = default; while (true) { int firstNonFinished = -1; bool anyMainThreadProgress = false; for (int i = 0; i < promises.Count; i++) { var(promise, it) = promises[i]; if (it == null) continue; if (force) { it.Current.Complete(); } else { if (it.Current.IsCompleted) { it.Current.Complete(); } else { if (firstNonFinished == -1) firstNonFinished = i; continue; } } anyMainThreadProgress = true; MarkerCalculate.Begin(); try { if (it.MoveNext()) { if (firstNonFinished == -1) firstNonFinished = i; } else promises[i] = (promise, null); } catch (System.Exception e) { Debug.LogError(new System.Exception("Error while updating graphs.", e)); promises[i] = (null, null); } MarkerCalculate.End(); } if (firstNonFinished == -1) { break; } else if (!force) { if (timeSlice.expired) { return false; } else if (anyMainThreadProgress) { // Reset the idle time slice if we got something done on the main thread. // This allows us to wait on more very short jobs. idleTimeSlice = TimeSlice.MillisFromNow(0.1f); } else if (idleTimeSlice.expired) { return false; } else { // Allow waiting for a short amount of time to allow very short running // jobs in graph updates to complete without having to wait until the next frame. // While waiting we release our thread's time slice to make sure other threads get priority. if (!anyMainThreadProgress) { MarkerSleep.Begin(); System.Threading.Thread.Yield(); MarkerSleep.End(); } } } } for (int i = 0; i < promises.Count; i++) { var(promise, it) = promises[i]; Assert.IsNull(it); if (promise != null) { MarkerApply.Begin(); try { promise.Apply(context); } catch (System.Exception e) { Debug.LogError(new System.Exception("Error while updating graphs.", e)); } MarkerApply.End(); } } return true; } } }