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;
}
}
}