summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc
diff options
context:
space:
mode:
authorchai <215380520@qq.com>2024-05-23 10:08:29 +0800
committerchai <215380520@qq.com>2024-05-23 10:08:29 +0800
commit8722a9920c1f6119bf6e769cba270e63097f8e25 (patch)
tree2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc
parent3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff)
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc')
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs124
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs.meta8
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs344
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs133
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs13
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs252
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs.meta12
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs44
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs274
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs.meta12
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs26
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs.meta8
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs396
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs466
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs153
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs405
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs295
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs.meta8
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs634
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs619
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs.meta12
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs32
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs71
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs47
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs315
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs.meta12
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs38
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs140
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs.meta8
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs419
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs.meta12
44 files changed, 5463 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs
new file mode 100644
index 0000000..f5a8dcf
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs
@@ -0,0 +1,124 @@
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace Pathfinding {
+ using Pathfinding.Util;
+ using Pathfinding.Drawing;
+
+ [HelpURL("https://arongranberg.com/astar/documentation/stable/animationlink.html")]
+ public class AnimationLink : NodeLink2 {
+ public string clip;
+ public float animSpeed = 1;
+ public bool reverseAnim = true;
+
+ public GameObject referenceMesh;
+ public LinkClip[] sequence;
+ public string boneRoot = "bn_COG_Root";
+
+ [System.Serializable]
+ public class LinkClip {
+ public AnimationClip clip;
+ public Vector3 velocity;
+ public int loopCount = 1;
+
+ public string name {
+ get {
+ return clip != null ? clip.name : "";
+ }
+ }
+ }
+
+ static Transform SearchRec (Transform tr, string name) {
+ int childCount = tr.childCount;
+
+ for (int i = 0; i < childCount; i++) {
+ Transform ch = tr.GetChild(i);
+ if (ch.name == name) return ch;
+ else {
+ Transform rec = SearchRec(ch, name);
+ if (rec != null) return rec;
+ }
+ }
+ return null;
+ }
+
+ public void CalculateOffsets (List<Vector3> trace, out Vector3 endPosition) {
+ //Vector3 opos = transform.position;
+ endPosition = transform.position;
+ if (referenceMesh == null) return;
+
+ GameObject ob = GameObject.Instantiate(referenceMesh, transform.position, transform.rotation) as GameObject;
+ ob.hideFlags = HideFlags.HideAndDontSave;
+
+ Transform root = SearchRec(ob.transform, boneRoot);
+ if (root == null) throw new System.Exception("Could not find root transform");
+
+ Animation anim = ob.GetComponent<Animation>();
+ if (anim == null) anim = ob.AddComponent<Animation>();
+
+ for (int i = 0; i < sequence.Length; i++) {
+ anim.AddClip(sequence[i].clip, sequence[i].clip.name);
+ }
+
+ Vector3 prevOffset = Vector3.zero;
+ Vector3 position = transform.position;
+ Vector3 firstOffset = Vector3.zero;
+
+ for (int i = 0; i < sequence.Length; i++) {
+ LinkClip c = sequence[i];
+ if (c == null) {
+ endPosition = position;
+ return;
+ }
+
+ anim[c.clip.name].enabled = true;
+ anim[c.clip.name].weight = 1;
+
+ for (int repeat = 0; repeat < c.loopCount; repeat++) {
+ anim[c.clip.name].normalizedTime = 0;
+ anim.Sample();
+ Vector3 soffset = root.position - transform.position;
+
+ if (i > 0) {
+ position += prevOffset - soffset;
+ } else {
+ firstOffset = soffset;
+ }
+
+ for (int t = 0; t <= 20; t++) {
+ float tf = t/20.0f;
+ anim[c.clip.name].normalizedTime = tf;
+ anim.Sample();
+ Vector3 tmp = position + (root.position-transform.position) + c.velocity*tf*c.clip.length;
+ trace.Add(tmp);
+ }
+ position = position + c.velocity*1*c.clip.length;
+
+ anim[c.clip.name].normalizedTime = 1;
+ anim.Sample();
+ Vector3 eoffset = root.position - transform.position;
+ prevOffset = eoffset;
+ }
+
+ anim[c.clip.name].enabled = false;
+ anim[c.clip.name].weight = 0;
+ }
+
+ position += prevOffset - firstOffset;
+
+ GameObject.DestroyImmediate(ob);
+
+ endPosition = position;
+ }
+
+ public override void DrawGizmos () {
+ base.DrawGizmos();
+ List<Vector3> buffer = Pathfinding.Util.ListPool<Vector3>.Claim();
+ Vector3 endPosition = Vector3.zero;
+ CalculateOffsets(buffer, out endPosition);
+ for (int i = 0; i < buffer.Count-1; i++) {
+ Draw.Line(buffer[i], buffer[i+1], Color.blue);
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs.meta
new file mode 100644
index 0000000..6c2aca2
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AnimationLink.cs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d2e8b1fd6fa484fc29f8a26fb5e8662b
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs
new file mode 100644
index 0000000..b6fac64
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs
@@ -0,0 +1,344 @@
+//#define ProfileAstar
+
+using UnityEngine;
+using System.Text;
+
+namespace Pathfinding {
+ [AddComponentMenu("Pathfinding/Pathfinding Debugger")]
+ [ExecuteInEditMode]
+ /// <summary>
+ /// Debugger for the A* Pathfinding Project.
+ /// This class can be used to profile different parts of the pathfinding system
+ /// and the whole game as well to some extent.
+ ///
+ /// Clarification of the labels shown when enabled.
+ /// All memory related things profiles <b>the whole game</b> not just the A* Pathfinding System.
+ /// - Currently allocated: memory the GC (garbage collector) says the application has allocated right now.
+ /// - Peak allocated: maximum measured value of the above.
+ /// - Last collect peak: the last peak of 'currently allocated'.
+ /// - Allocation rate: how much the 'currently allocated' value increases per second. This value is not as reliable as you can think
+ /// it is often very random probably depending on how the GC thinks this application is using memory.
+ /// - Collection frequency: how often the GC is called. Again, the GC might decide it is better with many small collections
+ /// or with a few large collections. So you cannot really trust this variable much.
+ /// - Last collect fps: FPS during the last garbage collection, the GC will lower the fps a lot.
+ ///
+ /// - FPS: current FPS (not updated every frame for readability)
+ /// - Lowest FPS (last x): As the label says, the lowest fps of the last x frames.
+ ///
+ /// - Size: Size of the path pool.
+ /// - Total created: Number of paths of that type which has been created. Pooled paths are not counted twice.
+ /// If this value just keeps on growing and growing without an apparent stop, you are are either not pooling any paths
+ /// or you have missed to pool some path somewhere in your code.
+ ///
+ /// See: pooling
+ ///
+ /// TODO: Add field showing how many graph updates are being done right now
+ /// </summary>
+ [HelpURL("https://arongranberg.com/astar/documentation/stable/astardebugger.html")]
+ public class AstarDebugger : VersionedMonoBehaviour {
+ public int yOffset = 5;
+
+ public bool show = true;
+ public bool showInEditor = false;
+
+ public bool showFPS = false;
+ public bool showPathProfile = false;
+ public bool showMemProfile = false;
+ public bool showGraph = false;
+
+ public int graphBufferSize = 200;
+
+ /// <summary>
+ /// Font to use.
+ /// A monospaced font is the best
+ /// </summary>
+ public Font font = null;
+ public int fontSize = 12;
+
+ StringBuilder text = new StringBuilder();
+ string cachedText;
+ float lastUpdate = -999;
+
+ private GraphPoint[] graph;
+
+ struct GraphPoint {
+ public float fps, memory;
+ public bool collectEvent;
+ }
+
+ private float delayedDeltaTime = 1;
+ private float lastCollect = 0;
+ private float lastCollectNum = 0;
+ private float delta = 0;
+ private float lastDeltaTime = 0;
+ private int allocRate = 0;
+ private int lastAllocMemory = 0;
+ private float lastAllocSet = -9999;
+ private int allocMem = 0;
+ private int collectAlloc = 0;
+ private int peakAlloc = 0;
+
+ private int fpsDropCounterSize = 200;
+ private float[] fpsDrops;
+
+ private Rect boxRect;
+
+ private GUIStyle style;
+
+ private Camera cam;
+
+ float graphWidth = 100;
+ float graphHeight = 100;
+ float graphOffset = 50;
+
+ public void Start () {
+ useGUILayout = false;
+
+ fpsDrops = new float[fpsDropCounterSize];
+
+ cam = GetComponent<Camera>();
+ if (cam == null) {
+ cam = Camera.main;
+ }
+
+ graph = new GraphPoint[graphBufferSize];
+
+ if (Time.unscaledDeltaTime > 0) {
+ for (int i = 0; i < fpsDrops.Length; i++) {
+ fpsDrops[i] = 1F / Time.unscaledDeltaTime;
+ }
+ }
+ }
+
+ int maxVecPool = 0;
+ int maxNodePool = 0;
+
+ PathTypeDebug[] debugTypes = new PathTypeDebug[] {
+ new PathTypeDebug("ABPath", () => PathPool.GetSize(typeof(ABPath)), () => PathPool.GetTotalCreated(typeof(ABPath)))
+ ,
+ new PathTypeDebug("MultiTargetPath", () => PathPool.GetSize(typeof(MultiTargetPath)), () => PathPool.GetTotalCreated(typeof(MultiTargetPath))),
+ new PathTypeDebug("RandomPath", () => PathPool.GetSize(typeof(RandomPath)), () => PathPool.GetTotalCreated(typeof(RandomPath))),
+ new PathTypeDebug("FleePath", () => PathPool.GetSize(typeof(FleePath)), () => PathPool.GetTotalCreated(typeof(FleePath))),
+ new PathTypeDebug("ConstantPath", () => PathPool.GetSize(typeof(ConstantPath)), () => PathPool.GetTotalCreated(typeof(ConstantPath))),
+ new PathTypeDebug("FloodPath", () => PathPool.GetSize(typeof(FloodPath)), () => PathPool.GetTotalCreated(typeof(FloodPath))),
+ new PathTypeDebug("FloodPathTracer", () => PathPool.GetSize(typeof(FloodPathTracer)), () => PathPool.GetTotalCreated(typeof(FloodPathTracer)))
+ };
+
+ struct PathTypeDebug {
+ string name;
+ System.Func<int> getSize;
+ System.Func<int> getTotalCreated;
+ public PathTypeDebug (string name, System.Func<int> getSize, System.Func<int> getTotalCreated) {
+ this.name = name;
+ this.getSize = getSize;
+ this.getTotalCreated = getTotalCreated;
+ }
+
+ public void Print (StringBuilder text) {
+ int totCreated = getTotalCreated();
+
+ if (totCreated > 0) {
+ text.Append("\n").Append((" " + name).PadRight(25)).Append(getSize()).Append("/").Append(totCreated);
+ }
+ }
+ }
+
+ public void LateUpdate () {
+ if (!show || (!Application.isPlaying && !showInEditor)) return;
+
+ if (Time.unscaledDeltaTime <= 0.0001f)
+ return;
+
+ int collCount = System.GC.CollectionCount(0);
+
+ if (lastCollectNum != collCount) {
+ lastCollectNum = collCount;
+ delta = Time.realtimeSinceStartup-lastCollect;
+ lastCollect = Time.realtimeSinceStartup;
+ lastDeltaTime = Time.unscaledDeltaTime;
+ collectAlloc = allocMem;
+ }
+
+ allocMem = (int)System.GC.GetTotalMemory(false);
+
+ bool collectEvent = allocMem < peakAlloc;
+ peakAlloc = !collectEvent ? allocMem : peakAlloc;
+
+ if (Time.realtimeSinceStartup - lastAllocSet > 0.3F || !Application.isPlaying) {
+ int diff = allocMem - lastAllocMemory;
+ lastAllocMemory = allocMem;
+ lastAllocSet = Time.realtimeSinceStartup;
+ delayedDeltaTime = Time.unscaledDeltaTime;
+
+ if (diff >= 0) {
+ allocRate = diff;
+ }
+ }
+
+ if (Application.isPlaying) {
+ fpsDrops[Time.frameCount % fpsDrops.Length] = Time.unscaledDeltaTime > 0.00001f ? 1F / Time.unscaledDeltaTime : 0;
+ int graphIndex = Time.frameCount % graph.Length;
+ graph[graphIndex].fps = Time.unscaledDeltaTime < 0.00001f ? 1F / Time.unscaledDeltaTime : 0;
+ graph[graphIndex].collectEvent = collectEvent;
+ graph[graphIndex].memory = allocMem;
+ }
+
+ if (Application.isPlaying && cam != null && showGraph) {
+ graphWidth = cam.pixelWidth*0.8f;
+
+
+ float minMem = float.PositiveInfinity, maxMem = 0, minFPS = float.PositiveInfinity, maxFPS = 0;
+ for (int i = 0; i < graph.Length; i++) {
+ minMem = Mathf.Min(graph[i].memory, minMem);
+ maxMem = Mathf.Max(graph[i].memory, maxMem);
+ minFPS = Mathf.Min(graph[i].fps, minFPS);
+ maxFPS = Mathf.Max(graph[i].fps, maxFPS);
+ }
+
+ int currentGraphIndex = Time.frameCount % graph.Length;
+
+ Matrix4x4 m = Matrix4x4.TRS(new Vector3((cam.pixelWidth - graphWidth)/2f, graphOffset, 1), Quaternion.identity, new Vector3(graphWidth, graphHeight, 1));
+
+ for (int i = 0; i < graph.Length-1; i++) {
+ if (i == currentGraphIndex) continue;
+
+ DrawGraphLine(i, m, i/(float)graph.Length, (i+1)/(float)graph.Length, Mathf.InverseLerp(minMem, maxMem, graph[i].memory), Mathf.InverseLerp(minMem, maxMem, graph[i+1].memory), Color.blue);
+ DrawGraphLine(i, m, i/(float)graph.Length, (i+1)/(float)graph.Length, Mathf.InverseLerp(minFPS, maxFPS, graph[i].fps), Mathf.InverseLerp(minFPS, maxFPS, graph[i+1].fps), Color.green);
+ }
+ }
+ }
+
+ void DrawGraphLine (int index, Matrix4x4 m, float x1, float x2, float y1, float y2, Color color) {
+ Debug.DrawLine(cam.ScreenToWorldPoint(m.MultiplyPoint3x4(new Vector3(x1, y1))), cam.ScreenToWorldPoint(m.MultiplyPoint3x4(new Vector3(x2, y2))), color);
+ }
+
+ public void OnGUI () {
+ if (!show || (!Application.isPlaying && !showInEditor)) return;
+
+ if (style == null) {
+ style = new GUIStyle();
+ style.normal.textColor = Color.white;
+ style.padding = new RectOffset(5, 5, 5, 5);
+ }
+
+ if (Time.realtimeSinceStartup - lastUpdate > 0.5f || cachedText == null || !Application.isPlaying) {
+ lastUpdate = Time.realtimeSinceStartup;
+
+ boxRect = new Rect(5, yOffset, 310, 40);
+
+ text.Length = 0;
+ text.AppendLine("A* Pathfinding Project Debugger");
+ text.Append("A* Version: ").Append(AstarPath.Version.ToString());
+
+ if (showMemProfile) {
+ boxRect.height += 200;
+
+ text.AppendLine();
+ text.AppendLine();
+ text.Append("Currently allocated".PadRight(25));
+ text.Append((allocMem/1000000F).ToString("0.0 MB"));
+ text.AppendLine();
+
+ text.Append("Peak allocated".PadRight(25));
+ text.Append((peakAlloc/1000000F).ToString("0.0 MB")).AppendLine();
+
+ text.Append("Last collect peak".PadRight(25));
+ text.Append((collectAlloc/1000000F).ToString("0.0 MB")).AppendLine();
+
+
+ text.Append("Allocation rate".PadRight(25));
+ text.Append((allocRate/1000000F).ToString("0.0 MB")).AppendLine();
+
+ text.Append("Collection frequency".PadRight(25));
+ text.Append(delta.ToString("0.00"));
+ text.Append("s\n");
+
+ text.Append("Last collect fps".PadRight(25));
+ text.Append((1F/lastDeltaTime).ToString("0.0 fps"));
+ text.Append(" (");
+ text.Append(lastDeltaTime.ToString("0.000 s"));
+ text.Append(")");
+ }
+
+ if (showFPS) {
+ text.AppendLine();
+ text.AppendLine();
+ var delayedFPS = delayedDeltaTime > 0.00001f ? 1F/delayedDeltaTime : 0;
+ text.Append("FPS".PadRight(25)).Append(delayedFPS.ToString("0.0 fps"));
+
+
+ float minFps = Mathf.Infinity;
+
+ for (int i = 0; i < fpsDrops.Length; i++) if (fpsDrops[i] < minFps) minFps = fpsDrops[i];
+
+ text.AppendLine();
+ text.Append(("Lowest fps (last " + fpsDrops.Length + ")").PadRight(25)).Append(minFps.ToString("0.0"));
+ }
+
+ if (showPathProfile) {
+ AstarPath astar = AstarPath.active;
+
+ text.AppendLine();
+
+ if (astar == null) {
+ text.Append("\nNo AstarPath Object In The Scene");
+ } else {
+#if ProfileAstar
+ double searchSpeed = (double)AstarPath.TotalSearchedNodes*10000 / (double)AstarPath.TotalSearchTime;
+ text.Append("\nSearch Speed (nodes/ms) ").Append(searchSpeed.ToString("0")).Append(" ("+AstarPath.TotalSearchedNodes+" / ").Append(((double)AstarPath.TotalSearchTime/10000F).ToString("0")+")");
+#endif
+
+ if (Pathfinding.Util.ListPool<Vector3>.GetSize() > maxVecPool) maxVecPool = Pathfinding.Util.ListPool<Vector3>.GetSize();
+ if (Pathfinding.Util.ListPool<Pathfinding.GraphNode>.GetSize() > maxNodePool) maxNodePool = Pathfinding.Util.ListPool<Pathfinding.GraphNode>.GetSize();
+
+ text.Append("\nPool Sizes (size/total created)");
+
+ for (int i = 0; i < debugTypes.Length; i++) {
+ debugTypes[i].Print(text);
+ }
+ }
+ }
+
+ cachedText = text.ToString();
+ }
+
+
+ if (font != null) {
+ style.font = font;
+ style.fontSize = fontSize;
+ }
+
+ boxRect.height = style.CalcHeight(new GUIContent(cachedText), boxRect.width);
+
+ GUI.Box(boxRect, "");
+ GUI.Label(boxRect, cachedText, style);
+
+ if (showGraph) {
+ float minMem = float.PositiveInfinity, maxMem = 0, minFPS = float.PositiveInfinity, maxFPS = 0;
+ for (int i = 0; i < graph.Length; i++) {
+ minMem = Mathf.Min(graph[i].memory, minMem);
+ maxMem = Mathf.Max(graph[i].memory, maxMem);
+ minFPS = Mathf.Min(graph[i].fps, minFPS);
+ maxFPS = Mathf.Max(graph[i].fps, maxFPS);
+ }
+
+ float line;
+ GUI.color = Color.blue;
+ // Round to nearest x.x MB
+ line = Mathf.RoundToInt(maxMem/(100.0f*1000));
+ GUI.Label(new Rect(5, Screen.height - AstarMath.MapTo(minMem, maxMem, 0 + graphOffset, graphHeight + graphOffset, line*1000*100) - 10, 100, 20), (line/10.0f).ToString("0.0 MB"));
+
+ line = Mathf.Round(minMem/(100.0f*1000));
+ GUI.Label(new Rect(5, Screen.height - AstarMath.MapTo(minMem, maxMem, 0 + graphOffset, graphHeight + graphOffset, line*1000*100) - 10, 100, 20), (line/10.0f).ToString("0.0 MB"));
+
+ GUI.color = Color.green;
+ // Round to nearest x.x MB
+ line = Mathf.Round(maxFPS);
+ GUI.Label(new Rect(55, Screen.height - AstarMath.MapTo(minFPS, maxFPS, 0 + graphOffset, graphHeight + graphOffset, line) - 10, 100, 20), line.ToString("0 FPS"));
+
+ line = Mathf.Round(minFPS);
+ GUI.Label(new Rect(55, Screen.height - AstarMath.MapTo(minFPS, maxFPS, 0 + graphOffset, graphHeight + graphOffset, line) - 10, 100, 20), line.ToString("0 FPS"));
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs.meta
new file mode 100644
index 0000000..8e8a582
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AstarDebugger.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 5103795af2d504ea693528e938005441
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs
new file mode 100644
index 0000000..78a211c
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs
@@ -0,0 +1,133 @@
+using UnityEngine;
+using Unity.Mathematics;
+
+namespace Pathfinding {
+ using Pathfinding.Drawing;
+
+ /// <summary>
+ /// Policy for how often to recalculate an agent's path.
+ ///
+ /// See: <see cref="AIBase.autoRepath"/>
+ /// See: <see cref="AILerp.autoRepath"/>
+ /// </summary>
+ [System.Serializable]
+ public class AutoRepathPolicy {
+ /// <summary>Policy mode for how often to recalculate an agent's path.</summary>
+ public enum Mode {
+ /// <summary>
+ /// Never automatically recalculate the path.
+ /// Paths can be recalculated manually by for example calling <see cref="IAstarAI.SearchPath"/> or <see cref="IAstarAI.SetPath"/>.
+ /// This mode is useful if you want full control of when the agent calculates its path.
+ /// </summary>
+ Never,
+ /// <summary>
+ /// Recalculate the path every <see cref="period"/> seconds.
+ ///
+ /// This is primarily included for historical reasons, but might be useful if you want the path recalculations to happen at a very predictable rate.
+ /// In most cases it is recommended to use the Dynamic mode.
+ /// </summary>
+ EveryNSeconds,
+ /// <summary>
+ /// Recalculate the path at least every <see cref="maximumPeriod"/> seconds but more often if the destination moves a lot.
+ /// This mode is recommended since it allows the agent to quickly respond to new destinations without using up a lot of CPU power to calculate paths
+ /// when it doesn't have to.
+ ///
+ /// More precisely:
+ /// Let C be a circle centered at the destination for the last calculated path, with a radius equal to the distance to that point divided by <see cref="sensitivity"/>.
+ /// If the new destination is outside that circle the path will be immediately recalculated.
+ /// Otherwise let F be the 1 - (distance from the circle's center to the new destination divided by the circle's radius).
+ /// So F will be 1 if the new destination is the same as the old one and 0 if it is at the circle's edge.
+ /// Recalculate the path if the time since the last path recalculation is greater than <see cref="maximumPeriod"/> multiplied by F.
+ ///
+ /// Thus if the destination doesn't change the path will be recalculated every <see cref="maximumPeriod"/> seconds.
+ /// </summary>
+ Dynamic,
+ }
+
+ /// <summary>
+ /// Policy to use when recalculating paths.
+ ///
+ /// See: <see cref="AutoRepathPolicy.Mode"/> for more details.
+ /// </summary>
+ public Mode mode = Mode.Dynamic;
+
+ /// <summary>Number of seconds between each automatic path recalculation for Mode.EveryNSeconds</summary>
+ [UnityEngine.Serialization.FormerlySerializedAs("interval")]
+ public float period = 0.5f;
+
+ /// <summary>
+ /// How sensitive the agent should be to changes in its destination for Mode.Dynamic.
+ /// A higher value means the destination has to move less for the path to be recalculated.
+ ///
+ /// See: <see cref="Mode"/>
+ /// </summary>
+ public float sensitivity = 10.0f;
+
+ /// <summary>Maximum number of seconds between each automatic path recalculation for Mode.Dynamic</summary>
+ [UnityEngine.Serialization.FormerlySerializedAs("maximumInterval")]
+ public float maximumPeriod = 2.0f;
+
+ /// <summary>If true the sensitivity will be visualized as a circle in the scene view when the game is playing</summary>
+ public bool visualizeSensitivity = false;
+
+ Vector3 lastDestination = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
+ float lastRepathTime = float.NegativeInfinity;
+
+ /// <summary>
+ /// True if the path should be recalculated according to the policy
+ ///
+ /// The above parameters are relevant only if <see cref="mode"/> is <see cref="Mode.Dynamic"/>.
+ /// </summary>
+ /// <param name="position">The current position of the agent.</param>
+ /// <param name="radius">The radius of the agent. You may pass 0.0 if the agent doesn't have a radius.</param>
+ /// <param name="destination">The goal of the agent right now</param>
+ /// <param name="time">The current time in seconds</param>
+ public virtual bool ShouldRecalculatePath (Vector3 position, float radius, Vector3 destination, float time) {
+ if (mode == Mode.Never || float.IsPositiveInfinity(destination.x)) return false;
+
+ float timeSinceLast = time - lastRepathTime;
+ if (mode == Mode.EveryNSeconds) {
+ return timeSinceLast >= period;
+ } else {
+ // cost = change in destination / max(distance to destination, radius)
+ float squaredCost = (destination - lastDestination).sqrMagnitude / Mathf.Max((position - lastDestination).sqrMagnitude, radius*radius);
+ float fraction = squaredCost * (sensitivity*sensitivity);
+ if (float.IsNaN(fraction)) {
+ // The agent's radius is zero, and the destination is precisely at the agent's position, which is also the destination of the last calculated path
+ // This is a special case. It happens sometimes for the AILerp component when it reaches its
+ // destination, as the AILerp component has no radius.
+ // In this case we just use the maximum period.
+ fraction = 0;
+ }
+
+ return timeSinceLast >= maximumPeriod*(1 - Mathf.Sqrt(fraction));
+ }
+ }
+
+ /// <summary>Reset the runtime variables so that the policy behaves as if the game just started</summary>
+ public virtual void Reset () {
+ lastRepathTime = float.NegativeInfinity;
+ }
+
+ /// <summary>Must be called when a path request has been scheduled</summary>
+ public virtual void DidRecalculatePath (Vector3 destination, float time) {
+ lastRepathTime = time;
+ lastDestination = destination;
+ // Randomize the repath time slightly so that all agents don't request a path at the same time
+ // in the future. This is useful when there are a lot of agents instantiated at exactly the same time.
+ const float JITTER_AMOUNT = 0.3f;
+ lastRepathTime -= (UnityEngine.Random.value - 0.5f) * JITTER_AMOUNT * (mode == Mode.Dynamic ? maximumPeriod : period);
+ }
+
+ public void DrawGizmos (CommandBuilder draw, Vector3 position, float radius, Util.NativeMovementPlane movementPlane) {
+ if (visualizeSensitivity && !float.IsPositiveInfinity(lastDestination.x)) {
+ float r = Mathf.Sqrt(Mathf.Max((position - lastDestination).sqrMagnitude, radius*radius)/(sensitivity*sensitivity));
+ draw.Circle(lastDestination, movementPlane.ToWorld(float2.zero, 1), r, Color.magenta);
+ }
+ }
+
+ public AutoRepathPolicy Clone () {
+ return MemberwiseClone() as AutoRepathPolicy;
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs.meta
new file mode 100644
index 0000000..d3535cb
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/AutoRepathPolicy.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2664ef60fd2811ba280670298a6d312b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs
new file mode 100644
index 0000000..17f5828
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs
@@ -0,0 +1,13 @@
+using Pathfinding.Serialization;
+
+namespace Pathfinding {
+ [JsonOptIn]
+ /// <summary>
+ /// Base class for all graph editors.
+ /// Defined here only so non-editor classes can use the <see cref="target"/> field
+ /// </summary>
+ public class GraphEditorBase {
+ /// <summary>NavGraph this editor is exposing</summary>
+ public NavGraph target;
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs.meta
new file mode 100644
index 0000000..1a638c9
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphEditorBase.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 704136724bc95455ebe477f42f5c5a84
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs
new file mode 100644
index 0000000..b94de4f
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs
@@ -0,0 +1,252 @@
+using UnityEngine;
+using System.Collections.Generic;
+using Pathfinding.Util;
+
+namespace Pathfinding {
+ /// <summary>
+ /// GraphModifier is used for modifying graphs or processing graph data based on events.
+ /// This class is a simple container for a number of events.
+ ///
+ /// \borderlessimage{graph_events.png}
+ ///
+ /// Warning: Some events will be called both in play mode <b>and in editor mode</b> (at least the scan events).
+ /// So make sure your code handles both cases well. You may choose to ignore editor events.
+ /// See: Application.IsPlaying
+ ///
+ /// Warning: Events may be received before Awake and OnEnable has been called on the component. This is because
+ /// graphs are typically scanned during Awake on the AstarPath component, which may happen before Awake on the graph modifier itself.
+ /// </summary>
+ [ExecuteInEditMode]
+ public abstract class GraphModifier : VersionedMonoBehaviour {
+ /// <summary>All active graph modifiers</summary>
+ private static GraphModifier root;
+
+ private GraphModifier prev;
+ private GraphModifier next;
+
+ /// <summary>Unique persistent ID for this component, used for serialization</summary>
+ [SerializeField]
+ [HideInInspector]
+ protected ulong uniqueID;
+
+ /// <summary>Maps persistent IDs to the component that uses it</summary>
+ protected static Dictionary<ulong, GraphModifier> usedIDs = new Dictionary<ulong, GraphModifier>();
+
+ protected static List<T> GetModifiersOfType<T>() where T : GraphModifier {
+ var current = root;
+ var result = new List<T>();
+
+ while (current != null) {
+ var cast = current as T;
+ if (cast != null) result.Add(cast);
+ current = current.next;
+ }
+ return result;
+ }
+
+ public static void FindAllModifiers () {
+ var allModifiers = UnityCompatibility.FindObjectsByTypeSorted<GraphModifier>();
+
+ for (int i = 0; i < allModifiers.Length; i++) {
+ if (allModifiers[i].enabled) {
+ if (allModifiers[i].next == null) {
+ // The modifier is not yet registered. Presumably it is enabled,
+ // but unity hasn't had time to call OnEnable yet.
+ // Disabling it and enabling it will force unity to call OnEnable immediately.
+ // We don't want to call it ourselves, because then Unity won't know that it has been called,
+ // which could cause issues for lifecycle management.
+ // For example, if we called OnEnable manually (before Unity did), and then the object was destroyed
+ // before Unity had a chance to call OnEnable, then Unity would not call OnDisable.
+ allModifiers[i].enabled = false;
+ allModifiers[i].enabled = true;
+ }
+ }
+ }
+ }
+
+ /// <summary>GraphModifier event type</summary>
+ public enum EventType {
+ PostScan = 1 << 0,
+ PreScan = 1 << 1,
+ LatePostScan = 1 << 2,
+ PreUpdate = 1 << 3,
+ PostUpdate = 1 << 4,
+ PostCacheLoad = 1 << 5,
+ PostUpdateBeforeAreaRecalculation = 1 << 6,
+ PostGraphLoad = 1 << 7,
+ }
+
+ /// <summary>Triggers an event for all active graph modifiers</summary>
+ public static void TriggerEvent (GraphModifier.EventType type) {
+ if (!Application.isPlaying) {
+ FindAllModifiers();
+ }
+
+ try {
+ GraphModifier c = root;
+ switch (type) {
+ case EventType.PreScan:
+ while (c != null) { c.OnPreScan(); c = c.next; }
+ break;
+ case EventType.PostScan:
+ while (c != null) { c.OnPostScan(); c = c.next; }
+ break;
+ case EventType.LatePostScan:
+ while (c != null) { c.OnLatePostScan(); c = c.next; }
+ break;
+ case EventType.PreUpdate:
+ while (c != null) { c.OnGraphsPreUpdate(); c = c.next; }
+ break;
+ case EventType.PostUpdate:
+ while (c != null) { c.OnGraphsPostUpdate(); c = c.next; }
+ break;
+ case EventType.PostUpdateBeforeAreaRecalculation:
+ while (c != null) { c.OnGraphsPostUpdateBeforeAreaRecalculation(); c = c.next; }
+ break;
+ case EventType.PostCacheLoad:
+ while (c != null) { c.OnPostCacheLoad(); c = c.next; }
+ break;
+ case EventType.PostGraphLoad:
+ while (c != null) { c.OnPostGraphLoad(); c = c.next; }
+ break;
+ }
+ } catch (System.Exception e) {
+ Debug.LogException(e);
+ }
+ }
+
+ /// <summary>Adds this modifier to list of active modifiers</summary>
+ protected virtual void OnEnable () {
+ RemoveFromLinkedList();
+ AddToLinkedList();
+ ConfigureUniqueID();
+ }
+
+ /// <summary>Removes this modifier from list of active modifiers</summary>
+ protected virtual void OnDisable () {
+ RemoveFromLinkedList();
+ }
+
+ protected override void Awake () {
+ base.Awake();
+ ConfigureUniqueID();
+ }
+
+ void ConfigureUniqueID () {
+ // Check if any other object is using the same uniqueID
+ // In that case this object may have been duplicated
+ GraphModifier usedBy;
+
+ if (usedIDs.TryGetValue(uniqueID, out usedBy) && usedBy != this) {
+ Reset();
+ }
+
+ usedIDs[uniqueID] = this;
+ }
+
+ void AddToLinkedList () {
+ if (root == null) {
+ root = this;
+ } else {
+ next = root;
+ root.prev = this;
+ root = this;
+ }
+ }
+
+ void RemoveFromLinkedList () {
+ if (root == this) {
+ root = next;
+ if (root != null) root.prev = null;
+ } else {
+ if (prev != null) prev.next = next;
+ if (next != null) next.prev = prev;
+ }
+ prev = null;
+ next = null;
+ }
+
+ protected virtual void OnDestroy () {
+ usedIDs.Remove(uniqueID);
+ }
+
+ /// <summary>
+ /// Called right after all graphs have been scanned.
+ ///
+ /// Note: Area information (see <see cref="Pathfinding.HierarchicalGraph)"/> may not be up to date when this event is sent.
+ /// This means some methods like <see cref="Pathfinding.PathUtilities.IsPathPossible"/> may return incorrect results.
+ /// Use <see cref="OnLatePostScan"/> if you need that info to be up to date.
+ ///
+ /// See: OnLatePostScan
+ /// </summary>
+ public virtual void OnPostScan () {}
+
+ /// <summary>
+ /// Called right before graphs are going to be scanned.
+ ///
+ /// See: OnLatePostScan
+ /// </summary>
+ public virtual void OnPreScan () {}
+
+ /// <summary>
+ /// Called at the end of the scanning procedure.
+ /// This is the absolute last thing done by Scan.
+ /// </summary>
+ public virtual void OnLatePostScan () {}
+
+ /// <summary>
+ /// Called after cached graphs have been loaded.
+ /// When using cached startup, this event is analogous to OnLatePostScan and implementing scripts
+ /// should do roughly the same thing for both events.
+ /// </summary>
+ public virtual void OnPostCacheLoad () {}
+
+ /// <summary>
+ /// Called after a graph has been deserialized and loaded.
+ /// Note: The graph may not have had any valid node data, it might just contain the graph settings.
+ ///
+ /// This will be called often outside of play mode. Make sure to check Application.isPlaying if appropriate.
+ /// </summary>
+ public virtual void OnPostGraphLoad () {}
+
+ /// <summary>Called before graphs are updated using GraphUpdateObjects</summary>
+ public virtual void OnGraphsPreUpdate () {}
+
+ /// <summary>
+ /// Called after graphs have been updated using GraphUpdateObjects or navmesh cutting.
+ ///
+ /// This is among other times called after graphs have been scanned, updated using GraphUpdateObjects, navmesh cuts, or GraphUpdateScene components.
+ ///
+ /// Area recalculations (see <see cref="Pathfinding.HierarchicalGraph"/>) have been done at this stage so things like PathUtilities.IsPathPossible will work.
+ ///
+ /// Use <see cref="OnGraphsPostUpdateBeforeAreaRecalculation"/> instead if you are modifying the graph in any way, especially connections and walkability.
+ /// This is because if you do this then area recalculations
+ /// </summary>
+ public virtual void OnGraphsPostUpdate () {}
+
+ /// <summary>
+ /// Called after graphs have been updated.
+ ///
+ /// This is among other times called after graphs have been scanned, updated using GraphUpdateObjects, navmesh cuts, or GraphUpdateScene components.
+ ///
+ /// Note: Area information (see <see cref="Pathfinding.HierarchicalGraph)"/> may not be up to date when this event is sent.
+ /// This means some methods like <see cref="Pathfinding.PathUtilities.IsPathPossible"/> may return incorrect results.
+ /// Use <see cref="OnLatePostScan"/> if you need that info to be up to date.
+ ///
+ /// Use this if you are modifying any graph connections or walkability.
+ ///
+ /// See: <see cref="OnGraphsPostUpdate"/>
+ /// </summary>
+ public virtual void OnGraphsPostUpdateBeforeAreaRecalculation () {}
+
+ protected override void Reset () {
+ base.Reset();
+ // Create a new random 64 bit value (62 bit actually because we skip negative numbers, but that's still enough by a huge margin)
+ var rnd1 = (ulong)Random.Range(0, int.MaxValue);
+ var rnd2 = ((ulong)Random.Range(0, int.MaxValue) << 32);
+
+ uniqueID = rnd1 | rnd2;
+ usedIDs[uniqueID] = this;
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs.meta
new file mode 100644
index 0000000..e8dbf6d
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphModifier.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 39897fb482672480a817862c3909a4aa
+timeCreated: 1490044676
+licenseType: Store
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: -222
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs
new file mode 100644
index 0000000..4b21d17
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using UnityEngine.Profiling;
+
+namespace Pathfinding.Util {
+ public interface IGraphSnapshot : System.IDisposable {
+ /// <summary>
+ /// Restores the graph data to the state it had when the snapshot was taken, in the bounding box that the snapshot captured.
+ ///
+ /// You can get the context from the callback provided to the <see cref="AstarPath.AddWorkItem"/> method.
+ /// </summary>
+ void Restore(IGraphUpdateContext ctx);
+ }
+
+ /// <summary>
+ /// A snapshot of parts of graphs.
+ ///
+ /// See: <see cref="AstarPath.Snapshot"/>
+ /// </summary>
+ public struct GraphSnapshot : IGraphSnapshot {
+ List<IGraphSnapshot> inner;
+
+ internal GraphSnapshot (List<IGraphSnapshot> inner) {
+ this.inner = inner;
+ }
+
+ /// <summary>\copydocref{IGraphSnapshot.Restore}</summary>
+ public void Restore (IGraphUpdateContext ctx) {
+ Profiler.BeginSample("Restoring Graph Snapshot");
+ for (int i = 0; i < inner.Count; i++) {
+ inner[i].Restore(ctx);
+ }
+ Profiler.EndSample();
+ }
+
+ public void Dispose () {
+ if (inner != null) {
+ for (int i = 0; i < inner.Count; i++) {
+ inner[i].Dispose();
+ }
+ inner = null;
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs.meta
new file mode 100644
index 0000000..b551fc1
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphSnapshot.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1c0eba1e12af0b040baa5627d27fe428
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs
new file mode 100644
index 0000000..01bc0f6
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs
@@ -0,0 +1,274 @@
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace Pathfinding {
+ using Pathfinding.Util;
+
+ /// <summary>
+ /// Contains utility methods for getting useful information out of graph.
+ /// This class works a lot with the <see cref="Pathfinding.GraphNode"/> class, a useful function to get nodes is <see cref="AstarPath.GetNearest"/>.
+ ///
+ /// See: <see cref="AstarPath.GetNearest"/>
+ /// See: <see cref="Pathfinding.GraphUpdateUtilities"/>
+ /// See: <see cref="Pathfinding.PathUtilities"/>
+ /// </summary>
+ public static class GraphUtilities {
+ /// <summary>
+ /// Convenience method to get a list of all segments of the contours of a graph.
+ /// Returns: A list of segments. Every 2 elements form a line segment. The first segment is (result[0], result[1]), the second one is (result[2], result[3]) etc.
+ /// The line segments are oriented so that the navmesh is on the right side of the segments when seen from above.
+ ///
+ /// This method works for navmesh, recast, grid graphs and layered grid graphs. For other graph types it will return an empty list.
+ ///
+ /// If you need more information about how the contours are connected you can take a look at the other variants of this method.
+ ///
+ /// <code>
+ /// // Get the first graph
+ /// var navmesh = AstarPath.active.graphs[0];
+ ///
+ /// // Get all contours of the graph (works for grid, navmesh and recast graphs)
+ /// var segments = GraphUtilities.GetContours(navmesh);
+ ///
+ /// // Every 2 elements form a line segment. The first segment is (segments[0], segments[1]), the second one is (segments[2], segments[3]) etc.
+ /// // The line segments are oriented so that the navmesh is on the right side of the segments when seen from above.
+ /// for (int i = 0; i < segments.Count; i += 2) {
+ /// var start = segments[i];
+ /// var end = segments[i+1];
+ /// Debug.DrawLine(start, end, Color.red, 3);
+ /// }
+ /// </code>
+ ///
+ /// [Open online documentation to see images]
+ /// [Open online documentation to see images]
+ /// </summary>
+ public static List<Vector3> GetContours (NavGraph graph) {
+ List<Vector3> result = ListPool<Vector3>.Claim();
+
+ if (graph is INavmesh) {
+ GetContours(graph as INavmesh, (vertices, cycle) => {
+ for (int j = cycle ? vertices.Count - 1 : 0, i = 0; i < vertices.Count; j = i, i++) {
+ result.Add((Vector3)vertices[j]);
+ result.Add((Vector3)vertices[i]);
+ }
+ });
+#if !ASTAR_NO_GRID_GRAPH
+ } else if (graph is GridGraph) {
+ GetContours(graph as GridGraph, vertices => {
+ for (int j = vertices.Length - 1, i = 0; i < vertices.Length; j = i, i++) {
+ result.Add((Vector3)vertices[j]);
+ result.Add((Vector3)vertices[i]);
+ }
+ }, 0);
+#endif
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Traces the contour of a navmesh.
+ ///
+ /// [Open online documentation to see images]
+ ///
+ /// This image is just used to illustrate the difference between chains and cycles. That it shows a grid graph is not relevant.
+ /// [Open online documentation to see images]
+ ///
+ /// See: <see cref="GetContours(NavGraph)"/>
+ /// </summary>
+ /// <param name="navmesh">The navmesh-like object to trace. This can be a recast or navmesh graph or it could be a single tile in one such graph.</param>
+ /// <param name="results">Will be called once for each contour with the contour as a parameter as well as a boolean indicating if the contour is a cycle or a chain (see second image).</param>
+ public static void GetContours (INavmesh navmesh, System.Action<List<Int3>, bool> results) {
+ // Assume 3 vertices per node
+ var uses = new bool[3];
+
+ var outline = new Dictionary<int, int>();
+ var vertexPositions = new Dictionary<int, Int3>();
+ var hasInEdge = new HashSet<int>();
+
+ navmesh.GetNodes(_node => {
+ var node = _node as TriangleMeshNode;
+
+ uses[0] = uses[1] = uses[2] = false;
+
+ if (node != null) {
+ // Find out which edges are shared with other nodes
+ for (int j = 0; j < node.connections.Length; j++) {
+ var conn = node.connections[j];
+ if (conn.isEdgeShared) uses[conn.shapeEdge] = true;
+ }
+
+ // Loop through all edges on the node
+ for (int j = 0; j < 3; j++) {
+ // The edge is not shared with any other node
+ // I.e it is an exterior edge on the mesh
+ if (!uses[j]) {
+ var i1 = j;
+ var i2 = (j+1) % node.GetVertexCount();
+
+ outline[node.GetVertexIndex(i1)] = node.GetVertexIndex(i2);
+ hasInEdge.Add(node.GetVertexIndex(i2));
+ vertexPositions[node.GetVertexIndex(i1)] = node.GetVertex(i1);
+ vertexPositions[node.GetVertexIndex(i2)] = node.GetVertex(i2);
+ }
+ }
+ }
+ });
+
+ Polygon.TraceContours(outline, hasInEdge, (chain, cycle) => {
+ List<Int3> vertices = ListPool<Int3>.Claim();
+ for (int i = 0; i < chain.Count; i++) vertices.Add(vertexPositions[chain[i]]);
+ results(vertices, cycle);
+ });
+ }
+
+#if !ASTAR_NO_GRID_GRAPH
+ /// <summary>
+ /// Finds all contours of a collection of nodes in a grid graph.
+ ///
+ /// <code>
+ /// var grid = AstarPath.active.data.gridGraph;
+ ///
+ /// // Find all contours in the graph and draw them using debug lines
+ /// GraphUtilities.GetContours(grid, vertices => {
+ /// for (int i = 0; i < vertices.Length; i++) {
+ /// Debug.DrawLine(vertices[i], vertices[(i+1)%vertices.Length], Color.red, 4);
+ /// }
+ /// }, 0);
+ /// </code>
+ ///
+ /// In the image below you can see the contour of a graph.
+ /// [Open online documentation to see images]
+ ///
+ /// In the image below you can see the contour of just a part of a grid graph (when the nodes parameter is supplied)
+ /// [Open online documentation to see images]
+ ///
+ /// Contour of a hexagon graph
+ /// [Open online documentation to see images]
+ ///
+ /// See: <see cref="GetContours(NavGraph)"/>
+ /// </summary>
+ /// <param name="grid">The grid to find the contours of</param>
+ /// <param name="callback">The callback will be called once for every contour that is found with the vertices of the contour. The contour always forms a cycle.</param>
+ /// <param name="yMergeThreshold">Contours will be simplified if the y coordinates for adjacent vertices differ by no more than this value.</param>
+ /// <param name="nodes">Only these nodes will be searched. If this parameter is null then all nodes in the grid graph will be searched.</param>
+ /// <param name="connectionFilter">Allows you to disable connections between nodes. If null, no additional filtering will be done. The filter must be symmetric, so that f(A,B) == f(B,A). A contour edge will be generated between two adjacent nodes if this function returns false for the pair.</param>
+ public static void GetContours (GridGraph grid, System.Action<Vector3[]> callback, float yMergeThreshold, GridNodeBase[] nodes = null, System.Func<GraphNode, GraphNode, bool> connectionFilter = null) {
+ // Set of all allowed nodes or null if all nodes are allowed
+ HashSet<GridNodeBase> nodeSet = nodes != null ? new HashSet<GridNodeBase>(nodes) : null;
+
+ // Use all nodes if the nodes parameter is null
+ if (grid is LayerGridGraph lgraph) nodes = nodes ?? lgraph.nodes;
+ nodes = nodes ?? grid.nodes;
+ int[] neighbourXOffsets = GridGraph.neighbourXOffsets;
+ int[] neighbourZOffsets = GridGraph.neighbourZOffsets;
+ var neighbourIndices = grid.neighbours == NumNeighbours.Six ? GridGraph.hexagonNeighbourIndices : new [] { 0, 1, 2, 3 };
+ var offsetMultiplier = grid.neighbours == NumNeighbours.Six ? 1/3f : 0.5f;
+
+ if (nodes != null) {
+ var trace = ListPool<Vector3>.Claim();
+ var seenStates = new HashSet<int>();
+
+ for (int i = 0; i < nodes.Length; i++) {
+ var startNode = nodes[i];
+ // The third check is a fast check for if the node has connections in all grid directions, if it has then we can skip processing it (unless the nodes parameter was used in which case we have to handle the edge cases)
+ if (startNode != null && startNode.Walkable && (!startNode.HasConnectionsToAllEightNeighbours || nodeSet != null)) {
+ for (int startDir = 0; startDir < neighbourIndices.Length; startDir++) {
+ int startState = ((int)startNode.NodeIndex << 4) | startDir;
+
+ // Check if there is an obstacle in that direction
+ var startNeighbour = startNode.GetNeighbourAlongDirection(neighbourIndices[startDir]);
+ if (connectionFilter != null && startNeighbour != null && !connectionFilter(startNode, startNeighbour)) startNeighbour = null;
+
+ if ((startNeighbour == null || (nodeSet != null && !nodeSet.Contains(startNeighbour))) && !seenStates.Contains(startState)) {
+ // Start tracing a contour here
+ trace.ClearFast();
+ int dir = startDir;
+ GridNodeBase node = startNode;
+
+ while (true) {
+ int state = ((int)node.NodeIndex << 4) | dir;
+ if (state == startState && trace.Count > 0) {
+ break;
+ }
+
+ seenStates.Add(state);
+
+ var neighbour = node.GetNeighbourAlongDirection(neighbourIndices[dir]);
+ if (connectionFilter != null && neighbour != null && !connectionFilter(node, neighbour)) neighbour = null;
+
+ if (neighbour == null || (nodeSet != null && !nodeSet.Contains(neighbour))) {
+ // Draw edge
+ var d0 = neighbourIndices[dir];
+ dir = (dir + 1) % neighbourIndices.Length;
+ var d1 = neighbourIndices[dir];
+
+ // Position in graph space of the vertex
+ Vector3 graphSpacePos = new Vector3(node.XCoordinateInGrid + 0.5f, 0, node.ZCoordinateInGrid + 0.5f);
+ // Offset along diagonal to get the correct XZ coordinates
+ graphSpacePos.x += (neighbourXOffsets[d0] + neighbourXOffsets[d1]) * offsetMultiplier;
+ graphSpacePos.z += (neighbourZOffsets[d0] + neighbourZOffsets[d1]) * offsetMultiplier;
+ graphSpacePos.y = grid.transform.InverseTransform((Vector3)node.position).y;
+
+ if (trace.Count >= 2) {
+ var v0 = trace[trace.Count-2];
+ var v1 = trace[trace.Count-1];
+ var v1d = v1 - v0;
+ var v2d = graphSpacePos - v0;
+ // Replace the previous point if it is colinear with the point just before it and just after it (the current point), because that point wouldn't add much information, but it would add CPU overhead
+ if (((Mathf.Abs(v1d.x) > 0.01f || Mathf.Abs(v2d.x) > 0.01f) && (Mathf.Abs(v1d.z) > 0.01f || Mathf.Abs(v2d.z) > 0.01f)) || (Mathf.Abs(v1d.y) > yMergeThreshold || Mathf.Abs(v2d.y) > yMergeThreshold)) {
+ trace.Add(graphSpacePos);
+ } else {
+ trace[trace.Count-1] = graphSpacePos;
+ }
+ } else {
+ trace.Add(graphSpacePos);
+ }
+ } else {
+#if UNITY_EDITOR
+ if (!neighbour.HasConnectionInDirection(GridNodeBase.OppositeConnectionDirection(neighbourIndices[dir]))) {
+ throw new System.InvalidOperationException("Cannot calculate contour. The graph contains one-way connections. A contour is not well defined if one-way connections exist.");
+ }
+#endif
+ // Move
+ node = neighbour;
+ dir = (dir + neighbourIndices.Length/2 + 1) % neighbourIndices.Length;
+ }
+ }
+
+ // Simplify the contour a bit around the start point.
+ // Otherwise we might return a cycle which was not as simplified as possible and the number of vertices
+ // would depend on where in the cycle the algorithm started to traverse the contour.
+ if (trace.Count >= 3) {
+ var v0 = trace[trace.Count-2];
+ var v1 = trace[trace.Count-1];
+ var v1d = v1 - v0;
+ var v2d = trace[0] - v0;
+ // Replace the previous point if it is colinear with the point just before it and just after it (the current point), because that point wouldn't add much information, but it would add CPU overhead
+ if (!(((Mathf.Abs(v1d.x) > 0.01f || Mathf.Abs(v2d.x) > 0.01f) && (Mathf.Abs(v1d.z) > 0.01f || Mathf.Abs(v2d.z) > 0.01f)) || (Mathf.Abs(v1d.y) > yMergeThreshold || Mathf.Abs(v2d.y) > yMergeThreshold))) {
+ trace.RemoveAt(trace.Count - 1);
+ }
+ }
+
+ if (trace.Count >= 3) {
+ var v0 = trace[trace.Count-1];
+ var v1 = trace[0];
+ var v1d = v1 - v0;
+ var v2d = trace[1] - v0;
+ // Replace the previous point if it is colinear with the point just before it and just after it (the current point), because that point wouldn't add much information, but it would add CPU overhead
+ if (!(((Mathf.Abs(v1d.x) > 0.01f || Mathf.Abs(v2d.x) > 0.01f) && (Mathf.Abs(v1d.z) > 0.01f || Mathf.Abs(v2d.z) > 0.01f)) || (Mathf.Abs(v1d.y) > yMergeThreshold || Mathf.Abs(v2d.y) > yMergeThreshold))) {
+ trace.RemoveAt(0);
+ }
+ }
+ var result = trace.ToArray();
+ grid.transform.Transform(result);
+ callback(result);
+ }
+ }
+ }
+ }
+
+ ListPool<Vector3>.Release(ref trace);
+ }
+ }
+#endif
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs.meta
new file mode 100644
index 0000000..d0417cc
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/GraphUtilities.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: fd178834bf6c54cdb8fc5f76a039a91c
+timeCreated: 1502889881
+licenseType: Store
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs
new file mode 100644
index 0000000..88c5a7b
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs
@@ -0,0 +1,26 @@
+using UnityEngine;
+namespace Pathfinding {
+ using Pathfinding.Util;
+
+ /// <summary>Helper for <see cref="Pathfinding.Examples.LocalSpaceRichAI"/></summary>
+ [HelpURL("https://arongranberg.com/astar/documentation/stable/localspacegraph.html")]
+ public class LocalSpaceGraph : VersionedMonoBehaviour {
+ Matrix4x4 originalMatrix;
+ MutableGraphTransform graphTransform = new MutableGraphTransform(Matrix4x4.identity);
+ public GraphTransform transformation { get { return graphTransform; } }
+
+ void Start () {
+ originalMatrix = transform.worldToLocalMatrix;
+ transform.hasChanged = true;
+ Refresh();
+ }
+
+ public void Refresh () {
+ // Avoid updating the GraphTransform if the object has not moved
+ if (transform.hasChanged) {
+ graphTransform.SetMatrix(transform.localToWorldMatrix * originalMatrix);
+ transform.hasChanged = false;
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs.meta
new file mode 100644
index 0000000..72899ae
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/LocalSpaceGraph.cs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 402bb27f2effb4bd183ec7f7dd47b078
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs
new file mode 100644
index 0000000..22acdf6
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs
@@ -0,0 +1,396 @@
+using System.Collections.Generic;
+using Unity.Collections;
+using Unity.Burst;
+using Unity.Jobs;
+using Unity.Mathematics;
+using UnityEngine;
+using UnityEngine.Profiling;
+using Unity.Collections.LowLevel.Unsafe;
+using Unity.Profiling;
+
+namespace Pathfinding {
+ using Pathfinding.Util;
+ using Pathfinding.RVO;
+ using Pathfinding.Jobs;
+ using Pathfinding.Drawing;
+
+ [BurstCompile]
+ public class NavmeshEdges {
+ public RVO.SimulatorBurst.ObstacleData obstacleData;
+ SpinLock allocationLock = new SpinLock();
+ const int JobRecalculateObstaclesBatchCount = 32;
+ RWLock rwLock = new RWLock();
+ public HierarchicalGraph hierarchicalGraph;
+ int gizmoVersion = 0;
+
+ public void Dispose () {
+ rwLock.WriteSync().Unlock();
+ obstacleData.Dispose();
+ }
+
+ void Init () {
+ obstacleData.Init(Allocator.Persistent);
+ }
+
+ public JobHandle RecalculateObstacles (NativeList<int> dirtyHierarchicalNodes, NativeReference<int> numHierarchicalNodes, JobHandle dependency) {
+ Init();
+
+ unsafe {
+ // Resize the obstacle data arrays if necessary.
+ // We need to do this in a separate single-threaded job before we branch out to multiple threads.
+ var writeLock = rwLock.Write();
+ var lastJob = new JobResizeObstacles {
+ numHierarchicalNodes = numHierarchicalNodes,
+ obstacles = obstacleData.obstacles,
+ }.Schedule(JobHandle.CombineDependencies(dependency, writeLock.dependency));
+ lastJob = new JobCalculateObstacles {
+ hGraphGC = hierarchicalGraph.gcHandle,
+ obstacleVertices = obstacleData.obstacleVertices,
+ obstacleVertexGroups = obstacleData.obstacleVertexGroups,
+ obstacles = obstacleData.obstacles.AsDeferredJobArray(),
+ bounds = hierarchicalGraph.bounds.AsDeferredJobArray(),
+ dirtyHierarchicalNodes = dirtyHierarchicalNodes,
+ // SAFETY: This is safe because the class will wait for the job to complete before it is disposed
+ allocationLock = (SpinLock*)UnsafeUtility.AddressOf(ref this.allocationLock),
+ }.ScheduleBatch(JobRecalculateObstaclesBatchCount, 1, lastJob);
+ writeLock.UnlockAfter(lastJob);
+ gizmoVersion++;
+ return lastJob;
+ }
+ }
+
+ public void OnDrawGizmos (DrawingData gizmos, RedrawScope redrawScope) {
+ if (!obstacleData.obstacleVertices.IsCreated) return;
+
+ var hasher = new NodeHasher(AstarPath.active);
+ hasher.Add(12314127); // Some random constant to avoid hash collisions with other systems
+ hasher.Add(gizmoVersion);
+
+ if (!gizmos.Draw(hasher, redrawScope)) {
+ var readLock = rwLock.ReadSync();
+ try {
+ using (var builder = gizmos.GetBuilder(hasher, redrawScope)) {
+ for (int i = 1; i < obstacleData.obstacles.Length; i++) {
+ var ob = obstacleData.obstacles[i];
+ var vertices = obstacleData.obstacleVertices.GetSpan(ob.verticesAllocation);
+ var groups = obstacleData.obstacleVertexGroups.GetSpan(ob.groupsAllocation);
+ var vertexOffset = 0;
+ for (int g = 0; g < groups.Length; g++) {
+ var group = groups[g];
+ builder.PushLineWidth(2f);
+ for (int j = 0; j < group.vertexCount - 1; j++) {
+ builder.ArrowRelativeSizeHead(vertices[vertexOffset + j], vertices[vertexOffset + j + 1], new float3(0, 1, 0), 0.05f, Color.black);
+ }
+ if (group.type == RVO.ObstacleType.Loop) {
+ builder.Arrow(vertices[vertexOffset + group.vertexCount - 1], vertices[vertexOffset], new float3(0, 1, 0), 0.05f, Color.black);
+ }
+ builder.PopLineWidth();
+ vertexOffset += group.vertexCount;
+ builder.WireBox(0.5f*(group.boundsMn + group.boundsMx), group.boundsMx - group.boundsMn, Color.white);
+ }
+ }
+ }
+ } finally {
+ readLock.Unlock();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Obstacle data for navmesh edges.
+ ///
+ /// Can be queried in burst jobs.
+ /// </summary>
+ public NavmeshBorderData GetNavmeshEdgeData (out RWLock.CombinedReadLockAsync readLock) {
+ Init();
+ var readLock1 = rwLock.Read();
+ var hierarchicalNodeData = hierarchicalGraph.GetHierarhicalNodeData(out var readLock2);
+ readLock = new RWLock.CombinedReadLockAsync(readLock1, readLock2);
+ return new NavmeshBorderData {
+ hierarhicalNodeData = hierarchicalNodeData,
+ obstacleData = obstacleData,
+ };
+ }
+
+ [BurstCompile]
+ struct JobResizeObstacles : IJob {
+ public NativeList<UnmanagedObstacle> obstacles;
+ public NativeReference<int> numHierarchicalNodes;
+
+ public void Execute () {
+ var prevLength = obstacles.Length;
+ var newLength = numHierarchicalNodes.Value;
+ obstacles.Resize(newLength, NativeArrayOptions.UninitializedMemory);
+ for (int i = prevLength; i < obstacles.Length; i++) obstacles[i] = new RVO.UnmanagedObstacle { verticesAllocation = SlabAllocator<float3>.ZeroLengthArray, groupsAllocation = SlabAllocator<ObstacleVertexGroup>.ZeroLengthArray };
+ // First hierarchical node is always invalid
+ if (obstacles.Length > 0) obstacles[0] = new RVO.UnmanagedObstacle { verticesAllocation = SlabAllocator<float3>.InvalidAllocation, groupsAllocation = SlabAllocator<ObstacleVertexGroup>.InvalidAllocation };
+ }
+ }
+
+ struct JobCalculateObstacles : IJobParallelForBatch {
+ public System.Runtime.InteropServices.GCHandle hGraphGC;
+ public SlabAllocator<float3> obstacleVertices;
+ public SlabAllocator<ObstacleVertexGroup> obstacleVertexGroups;
+ [NativeDisableParallelForRestriction]
+ public NativeArray<UnmanagedObstacle> obstacles;
+ [NativeDisableParallelForRestriction]
+ public NativeArray<Bounds> bounds;
+ [ReadOnly]
+ public NativeList<int> dirtyHierarchicalNodes;
+ [NativeDisableUnsafePtrRestriction]
+ public unsafe SpinLock* allocationLock;
+
+ public void Execute (int startIndex, int count) {
+ var hGraph = hGraphGC.Target as HierarchicalGraph;
+ var stepMultiplier = (dirtyHierarchicalNodes.Length + JobRecalculateObstaclesBatchCount - 1) / JobRecalculateObstaclesBatchCount;
+ startIndex *= stepMultiplier;
+ count *= stepMultiplier;
+ var finalIndex = math.min(startIndex + count, dirtyHierarchicalNodes.Length);
+ var edges = new NativeList<RVO.RVOObstacleCache.ObstacleSegment>(Allocator.Temp);
+ for (int i = startIndex; i < finalIndex; i++) {
+ edges.Clear();
+ var hNode = dirtyHierarchicalNodes[i];
+ UnityEngine.Assertions.Assert.IsTrue(hNode > 0 && hNode < obstacles.Length);
+ // These tasks are independent, but they benefit a lot from running at the same time
+ // due to cache locality (they use mostly the same data).
+ CalculateBoundingBox(hGraph, hNode);
+ CalculateObstacles(hGraph, hNode, obstacleVertexGroups, obstacleVertices, obstacles, edges);
+ }
+ }
+
+ private static readonly ProfilerMarker MarkerBBox = new ProfilerMarker("HierarchicalBBox");
+ private static readonly ProfilerMarker MarkerObstacles = new ProfilerMarker("CalculateObstacles");
+ private static readonly ProfilerMarker MarkerCollect = new ProfilerMarker("Collect");
+ private static readonly ProfilerMarker MarkerTrace = new ProfilerMarker("Trace");
+
+ void CalculateBoundingBox (HierarchicalGraph hGraph, int hierarchicalNode) {
+ var nodes = hGraph.children[hierarchicalNode];
+ MarkerBBox.Begin();
+ var b = new Bounds();
+ // We know that all nodes in an hierarchical node only belongs to a single graph,
+ // so we can branch on the type of the first node, and use optimized code for each node type.
+ if (nodes.Count == 0) {
+ // NOOP
+ } else if (nodes[0] is TriangleMeshNode) {
+ var mn = new Int3(int.MaxValue, int.MaxValue, int.MaxValue);
+ var mx = new Int3(int.MinValue, int.MinValue, int.MinValue);
+ for (int i = 0; i < nodes.Count; i++) {
+ var node = nodes[i] as TriangleMeshNode;
+ node.GetVertices(out var v0, out var v1, out var v2);
+ mn = Int3.Min(Int3.Min(Int3.Min(mn, v0), v1), v2);
+ mx = Int3.Max(Int3.Max(Int3.Max(mx, v0), v1), v2);
+ }
+ b.SetMinMax((Vector3)mn, (Vector3)mx);
+ } else {
+ var mn = new Int3(int.MaxValue, int.MaxValue, int.MaxValue);
+ var mx = new Int3(int.MinValue, int.MinValue, int.MinValue);
+ for (int i = 0; i < nodes.Count; i++) {
+ var node = nodes[i];
+ mn = Int3.Min(mn, node.position);
+ mx = Int3.Max(mx, node.position);
+ }
+ if (nodes[0] is GridNodeBase) {
+ float nodeSize;
+ if (nodes[0] is LevelGridNode) nodeSize = LevelGridNode.GetGridGraph(nodes[0].GraphIndex).nodeSize;
+ else
+ nodeSize = GridNode.GetGridGraph(nodes[0].GraphIndex).nodeSize;
+ // Grid nodes have a surface. We don't know how it is oriented, so we pad conservatively in all directions.
+ // The surface can extend at most nodeSize*sqrt(2)/2 in any direction.
+ const float SQRT2_DIV_2 = 0.70710678f;
+ var padding = nodeSize*SQRT2_DIV_2*Vector3.one;
+ b.SetMinMax((Vector3)mn - padding, (Vector3)mx + padding);
+ } else {
+ // Point node, or other custom node type
+ b.SetMinMax((Vector3)mn, (Vector3)mx);
+ }
+ }
+ bounds[hierarchicalNode] = b;
+ MarkerBBox.End();
+ }
+
+ void CalculateObstacles (HierarchicalGraph hGraph, int hierarchicalNode, SlabAllocator<ObstacleVertexGroup> obstacleVertexGroups, SlabAllocator<float3> obstacleVertices, NativeArray<UnmanagedObstacle> obstacles, NativeList<RVO.RVOObstacleCache.ObstacleSegment> edgesScratch) {
+ MarkerObstacles.Begin();
+ MarkerCollect.Begin();
+ RVO.RVOObstacleCache.CollectContours(hGraph.children[hierarchicalNode], edgesScratch);
+ MarkerCollect.End();
+ var prev = obstacles[hierarchicalNode];
+ if (prev.groupsAllocation != SlabAllocator<ObstacleVertexGroup>.ZeroLengthArray) {
+ unsafe {
+ allocationLock->Lock();
+ obstacleVertices.Free(prev.verticesAllocation);
+ obstacleVertexGroups.Free(prev.groupsAllocation);
+ allocationLock->Unlock();
+ }
+ }
+ unsafe {
+ // Find the graph's natural movement plane.
+ // This is used to simplify almost colinear segments into a single segment.
+ var children = hGraph.children[hierarchicalNode];
+ NativeMovementPlane movementPlane;
+ bool simplifyObstacles = true;
+ if (children.Count > 0) {
+ if (children[0] is GridNodeBase) {
+ movementPlane = new NativeMovementPlane((children[0].Graph as GridGraph).transform.rotation);
+ } else if (children[0] is TriangleMeshNode) {
+ var graph = children[0].Graph as NavmeshBase;
+ movementPlane = new NativeMovementPlane(graph.transform.rotation);
+ // If normal recalculation is disabled, the graph may have very a strange shape, like a spherical world.
+ // In that case we should not simplify the obstacles, as there is no well defined movement plane.
+ simplifyObstacles = graph.RecalculateNormals;
+ } else {
+ movementPlane = new NativeMovementPlane(quaternion.identity);
+ simplifyObstacles = false;
+ }
+ } else {
+ movementPlane = default;
+ }
+ MarkerTrace.Begin();
+ var edgesSpan = edgesScratch.AsUnsafeSpan();
+ RVO.RVOObstacleCache.TraceContours(
+ ref edgesSpan,
+ ref movementPlane,
+ hierarchicalNode,
+ (UnmanagedObstacle*)obstacles.GetUnsafePtr(),
+ ref obstacleVertices,
+ ref obstacleVertexGroups,
+ ref UnsafeUtility.AsRef<SpinLock>(allocationLock),
+ simplifyObstacles
+ );
+ MarkerTrace.End();
+ }
+ MarkerObstacles.End();
+ }
+ }
+
+ /// <summary>
+ /// Burst-accessible data about borders in the navmesh.
+ ///
+ /// Can be queried from burst, and from multiple threads in parallel.
+ /// </summary>
+ // TODO: Change to a quadtree/kdtree/aabb tree that stored edges as { index: uint10, prev: uint10, next: uint10 }, with a natural max of 1024 vertices per obstacle (hierarchical node). This is fine because hnodes have at most 256 nodes, which cannot create more than 1024 edges.
+ public struct NavmeshBorderData {
+ public HierarchicalGraph.HierarhicalNodeData hierarhicalNodeData;
+ public RVO.SimulatorBurst.ObstacleData obstacleData;
+
+ /// <summary>
+ /// An empty set of edges.
+ ///
+ /// Must be disposed using <see cref="DisposeEmpty"/>.
+ /// </summary>
+ public static NavmeshBorderData CreateEmpty (Allocator allocator) {
+ return new NavmeshBorderData {
+ hierarhicalNodeData = new HierarchicalGraph.HierarhicalNodeData {
+ connectionAllocator = default,
+ connectionAllocations = new NativeList<int>(0, allocator),
+ bounds = new NativeList<Bounds>(0, allocator),
+ },
+ obstacleData = new RVO.SimulatorBurst.ObstacleData {
+ obstacleVertexGroups = default,
+ obstacleVertices = default,
+ obstacles = new NativeList<UnmanagedObstacle>(0, allocator),
+ }
+ };
+ }
+
+ public void DisposeEmpty (JobHandle dependsOn) {
+ if (hierarhicalNodeData.connectionAllocator.IsCreated) throw new System.InvalidOperationException("NavmeshEdgeData was not empty");
+ hierarhicalNodeData.connectionAllocations.Dispose(dependsOn);
+ hierarhicalNodeData.bounds.Dispose(dependsOn);
+ obstacleData.obstacles.Dispose(dependsOn);
+ }
+
+ static void GetHierarchicalNodesInRangeRec (int hierarchicalNode, Bounds bounds, SlabAllocator<int> connectionAllocator, [NoAlias] NativeList<int> connectionAllocations, NativeList<Bounds> nodeBounds, [NoAlias] NativeList<int> indices) {
+ indices.Add(hierarchicalNode);
+ var conns = connectionAllocator.GetSpan(connectionAllocations[hierarchicalNode]);
+ for (int i = 0; i < conns.Length; i++) {
+ var neighbour = conns[i];
+ if (nodeBounds[neighbour].Intersects(bounds) && !indices.Contains(neighbour)) {
+ GetHierarchicalNodesInRangeRec(neighbour, bounds, connectionAllocator, connectionAllocations, nodeBounds, indices);
+ }
+ }
+ }
+
+ static unsafe void ConvertObstaclesToEdges (ref RVO.SimulatorBurst.ObstacleData obstacleData, NativeList<int> obstacleIndices, Bounds localBounds, NativeList<float2> edgeBuffer, NativeMovementPlane movementPlane) {
+ var globalBounds = movementPlane.ToWorld(localBounds);
+ var worldToMovementPlane = movementPlane.AsWorldToPlaneMatrix();
+ var globalMn = (float3)globalBounds.min;
+ var globalMx = (float3)globalBounds.max;
+ var localMn = (float3)localBounds.min;
+ var localMx = (float3)localBounds.max;
+ int vertexCount = 0;
+ for (int obstacleIndex = 0; obstacleIndex < obstacleIndices.Length; obstacleIndex++) {
+ var obstacle = obstacleData.obstacles[obstacleIndices[obstacleIndex]];
+ vertexCount += obstacleData.obstacleVertices.GetSpan(obstacle.verticesAllocation).Length;
+ }
+ edgeBuffer.ResizeUninitialized(vertexCount*3);
+ int edgeVertexOffset = 0;
+ for (int obstacleIndex = 0; obstacleIndex < obstacleIndices.Length; obstacleIndex++) {
+ var obstacle = obstacleData.obstacles[obstacleIndices[obstacleIndex]];
+ if (obstacle.verticesAllocation != SlabAllocator<float3>.ZeroLengthArray) {
+ var vertices = obstacleData.obstacleVertices.GetSpan(obstacle.verticesAllocation);
+ var groups = obstacleData.obstacleVertexGroups.GetSpan(obstacle.groupsAllocation);
+ int offset = 0;
+ for (int i = 0; i < groups.Length; i++) {
+ var group = groups[i];
+ if (!math.all((group.boundsMx >= globalMn) & (group.boundsMn <= globalMx))) {
+ offset += group.vertexCount;
+ continue;
+ }
+
+ for (int j = 0; j < group.vertexCount - 1; j++) {
+ var p1 = vertices[offset + j];
+ var p2 = vertices[offset + j + 1];
+ var mn = math.min(p1, p2);
+ var mx = math.max(p1, p2);
+ // Check for intersection with the global bounds (coarse check)
+ if (math.all((mx >= globalMn) & (mn <= globalMx))) {
+ var p1local = worldToMovementPlane.ToXZPlane(p1);
+ var p2local = worldToMovementPlane.ToXZPlane(p2);
+ mn = math.min(p1local, p2local);
+ mx = math.max(p1local, p2local);
+ // Check for intersection with the local bounds (more accurate)
+ if (math.all((mx >= localMn) & (mn <= localMx))) {
+ edgeBuffer[edgeVertexOffset++] = p1local.xz;
+ edgeBuffer[edgeVertexOffset++] = p2local.xz;
+ }
+ }
+ }
+ if (group.type == RVO.ObstacleType.Loop) {
+ var p1 = vertices[offset + group.vertexCount - 1];
+ var p2 = vertices[offset];
+ var mn = math.min(p1, p2);
+ var mx = math.max(p1, p2);
+ if (math.all((mx >= globalMn) & (mn <= globalMx))) {
+ var p1local = worldToMovementPlane.ToXZPlane(p1);
+ var p2local = worldToMovementPlane.ToXZPlane(p2);
+ mn = math.min(p1local, p2local);
+ mx = math.max(p1local, p2local);
+ if (math.all((mx >= localMn) & (mn <= localMx))) {
+ edgeBuffer[edgeVertexOffset++] = p1local.xz;
+ edgeBuffer[edgeVertexOffset++] = p2local.xz;
+ }
+ }
+ }
+ offset += group.vertexCount;
+ }
+ }
+ }
+ UnityEngine.Assertions.Assert.IsTrue(edgeVertexOffset <= edgeBuffer.Length);
+ edgeBuffer.Length = edgeVertexOffset;
+ }
+
+ public void GetObstaclesInRange (int hierarchicalNode, Bounds bounds, NativeList<int> obstacleIndexBuffer) {
+ if (!obstacleData.obstacleVertices.IsCreated) return;
+ GetHierarchicalNodesInRangeRec(hierarchicalNode, bounds, hierarhicalNodeData.connectionAllocator, hierarhicalNodeData.connectionAllocations, hierarhicalNodeData.bounds, obstacleIndexBuffer);
+ }
+
+ public void GetEdgesInRange (int hierarchicalNode, Bounds localBounds, NativeList<float2> edgeBuffer, NativeMovementPlane movementPlane) {
+ if (!obstacleData.obstacleVertices.IsCreated) return;
+
+ var indices = new NativeList<int>(8, Allocator.Temp);
+ GetObstaclesInRange(hierarchicalNode, movementPlane.ToWorld(localBounds), indices);
+ ConvertObstaclesToEdges(ref obstacleData, indices, localBounds, edgeBuffer, movementPlane);
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs.meta
new file mode 100644
index 0000000..66750cc
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshEdges.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 619ad2f6a5e696d4d849e05e14bc58f4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs
new file mode 100644
index 0000000..d193821
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs
@@ -0,0 +1,466 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Pathfinding {
+ using Pathfinding.Drawing;
+ using Pathfinding.Graphs.Navmesh;
+ using Pathfinding.Jobs;
+ using Pathfinding.Serialization;
+ using Pathfinding.Util;
+ using Unity.Jobs;
+
+ /// <summary>
+ /// Stores a set of navmesh tiles which can be placed on a recast graph.
+ ///
+ /// This component is used to store chunks of a <see cref="RecastGraph"/> to a file and then be able to efficiently load them and place them on an existing recast graph.
+ /// A typical use case is if you have a procedurally generated level consisting of multiple rooms, and scanning the graph after the level has been generated
+ /// is too expensive. In this scenario, each room can have its own NavmeshPrefab component which stores the navmesh for just that room, and then when the
+ /// level is generated all the NavmeshPrefab components will load their tiles and place them on the recast graph, joining them together at the seams.
+ ///
+ /// Since this component works on tiles, the size of a NavmeshPrefab must be a multiple of the graph's tile size.
+ /// The tile size of a recast graph is determined by multiplying the <see cref="RecastGraph.cellSize"/> with the tile size in voxels (<see cref="RecastGraph.editorTileSize"/>).
+ /// When a NavmeshPrefab is placed on a recast graph, it will load the tiles into the closest spot (snapping the position and rotation).
+ /// The NavmeshPrefab will even resize the graph to make it larger in case you want to place a NavmeshPrefab outside the existing bounds of the graph.
+ ///
+ /// <b>Usage</b>
+ ///
+ /// - Attach a NavmeshPrefab component to a GameObject (typically a prefab) that you want to store the navmesh for.
+ /// - Make sure you have a RecastGraph elsewhere in the scene with the same settings that you use for the game.
+ /// - Adjust the bounding box to fit your game object. The bounding box should be a multiple of the tile size of the recast graph.
+ /// - In the inspector, click the "Scan" button to scan the graph and store the navmesh as a file, referenced by the NavmeshPrefab component.
+ /// - Make sure the rendered navmesh looks ok in the scene view.
+ /// - In your game, instantiate a prefab with the NavmeshComponent. It will automatically load its stored tiles and place them on the first recast graph in the scene.
+ ///
+ /// If you have multiple recast graphs you may not want it to always use the first recast graph.
+ /// In that case you can set the <see cref="applyOnStart"/> field to false and call the <see cref="Apply(RecastGraph)"/> method manually.
+ ///
+ /// <b>Accounting for borders</b>
+ ///
+ /// When scanning a recast graph (and by extension a NavmeshPrefab), a margin is always added around parts of the graph the agent cannot traverse.
+ /// This can become problematic when scanning individual chunks separate from the rest of the world, because each one will have a small border of unwalkable space.
+ /// The result is that when you place them on a recast graph, they will not be able to connect to each other.
+ /// [Open online documentation to see images]
+ /// One way to solve this is to scan the prefab together with a mesh that is slightly larger than the prefab, extending the walkable surface enough
+ /// so that no border is added. In the image below, this mesh is displayed in white. It can be convenient to make this an invisible collider on the prefab
+ /// that is excluded from physics, but is included in the graph's rasterization layer mask.
+ /// [Open online documentation to see images]
+ /// Now that the border has been removed, the chunks can be placed next to each other and be able to connect.
+ /// [Open online documentation to see images]
+ ///
+ /// <b>Loading tiles into a graph</b>
+ ///
+ /// If <see cref="applyOnStart"/> is true, the tiles will be loaded into the first recast graph in the scene when the game starts.
+ /// If the recast graph is not scanned, it will be initialized with empty tiles and then the tiles will be loaded into it.
+ /// So if your world is made up entirely of NavmeshPrefabs, you can skip scanning for performance by setting A* Inspector -> Settings -> Scan On Awake to false.
+ ///
+ /// You can also apply a NavmeshPrefab to a graph manually by calling the <see cref="Apply(RecastGraph)"/> method.
+ ///
+ /// See: <see cref="RecastGraph"/>
+ /// See: <see cref="TileMeshes"/>
+ /// </summary>
+ [AddComponentMenu("Pathfinding/Navmesh Prefab")]
+ [HelpURL("https://arongranberg.com/astar/documentation/stable/navmeshprefab.html")]
+ public class NavmeshPrefab : VersionedMonoBehaviour {
+ /// <summary>Reference to the serialized tile data</summary>
+ public TextAsset serializedNavmesh;
+
+ /// <summary>
+ /// If true, the tiles stored in this prefab will be loaded and applied to the first recast graph in the scene when this component is enabled.
+ /// If false, you will have to call the <see cref="Apply(RecastGraph)"/> method manually.
+ ///
+ /// If this component is disabled and then enabled again, the tiles will be reloaded.
+ /// </summary>
+ public bool applyOnStart = true;
+
+ /// <summary>
+ /// If true, the tiles that this prefab loaded into the graph will be removed when this component is disabled or destroyed.
+ /// If false, the tiles will remain in the graph.
+ /// </summary>
+ public bool removeTilesWhenDisabled = true;
+
+ /// <summary>
+ /// Bounding box for the navmesh to be stored in this prefab.
+ /// Should be a multiple of the tile size of the associated recast graph.
+ ///
+ /// See:
+ /// See: <see cref="RecastGraph.TileWorldSizeX"/>
+ /// </summary>
+ public Bounds bounds = new Bounds(Vector3.zero, new Vector3(10, 10, 10));
+
+ bool startHasRun = false;
+
+ protected override void Reset () {
+ base.Reset();
+ AstarPath.FindAstarPath();
+ if (AstarPath.active != null && AstarPath.active.data.recastGraph != null) {
+ var graph = AstarPath.active.data.recastGraph;
+ // Make the default bounds be 1x1 tiles in the graph
+ bounds = new Bounds(Vector3.zero, new Vector3(graph.TileWorldSizeX, graph.forcedBoundsSize.y, graph.TileWorldSizeZ));
+ }
+ }
+
+#if UNITY_EDITOR
+ public override void DrawGizmos () {
+ using (Draw.WithMatrix(Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one))) {
+ Draw.WireBox(bounds.center, bounds.size);
+ }
+
+ if (!Application.isPlaying && serializedNavmesh != null) {
+ var path = UnityEditor.AssetDatabase.GetAssetPath(serializedNavmesh);
+ var lastEditTime = System.IO.File.GetLastWriteTimeUtc(Application.dataPath + "/../" + path);
+ lastEditTime.ToBinary();
+ // Hash the metadata to avoid somewhat expensive deserialization and drawing every frame.
+ var hasher = new Pathfinding.Drawing.DrawingData.Hasher();
+ hasher.Add(lastEditTime);
+ hasher.Add(transform.position);
+ hasher.Add(transform.rotation);
+ hasher.Add(bounds);
+
+ // Draw a new mesh if the metadata has changed
+ if (!Pathfinding.Drawing.DrawingManager.instance.gizmos.Draw(hasher)) {
+ var builder = Pathfinding.Drawing.DrawingManager.GetBuilder(hasher);
+
+ var tileMeshes = TileMeshes.Deserialize(serializedNavmesh.bytes);
+
+ var center = transform.position + transform.rotation * bounds.center;
+ var corner = center - transform.rotation*bounds.extents;
+ var tileWorldSize = tileMeshes.tileWorldSize;
+ var graphToWorldSpace = Matrix4x4.TRS(corner, transform.rotation, Vector3.one);
+
+ var vertexCount = 0;
+ var trisCount = 0;
+ for (int i = 0; i < tileMeshes.tileMeshes.Length; i++) {
+ vertexCount += tileMeshes.tileMeshes[i].verticesInTileSpace.Length;
+ trisCount += tileMeshes.tileMeshes[i].triangles.Length;
+ }
+
+ var colors = Util.ArrayPool<Color>.Claim(vertexCount);
+ var vertices = Util.ArrayPool<Vector3>.Claim(vertexCount);
+ var triangles = Util.ArrayPool<int>.Claim(trisCount);
+ vertexCount = 0;
+ trisCount = 0;
+
+ using (builder.WithColor(AstarColor.SolidColor)) {
+ for (int z = 0; z < tileMeshes.tileRect.Height; z++) {
+ for (int x = 0; x < tileMeshes.tileRect.Width; x++) {
+ var tile = tileMeshes.tileMeshes[x + z*tileMeshes.tileRect.Width];
+
+ var tileToWorldSpace = graphToWorldSpace * Matrix4x4.Translate(new Vector3(x * tileWorldSize.x, 0, z * tileWorldSize.y));
+ var startVertex = vertexCount;
+ for (int j = 0; j < tile.triangles.Length; trisCount++, j++) {
+ triangles[trisCount] = tile.triangles[j] + startVertex;
+ }
+ for (int j = 0; j < tile.verticesInTileSpace.Length; vertexCount++, j++) {
+ colors[vertexCount] = AstarColor.SolidColor;
+ vertices[vertexCount] = tileToWorldSpace.MultiplyPoint3x4((Vector3)tile.verticesInTileSpace[j]);
+ }
+
+ for (int i = 0; i < tile.triangles.Length; i += 3) {
+ builder.Line(vertices[startVertex + tile.triangles[i+0]], vertices[startVertex + tile.triangles[i+1]]);
+ builder.Line(vertices[startVertex + tile.triangles[i+1]], vertices[startVertex + tile.triangles[i+2]]);
+ builder.Line(vertices[startVertex + tile.triangles[i+2]], vertices[startVertex + tile.triangles[i+0]]);
+ }
+ }
+ }
+ }
+
+ builder.SolidMesh(vertices, triangles, colors, vertexCount, trisCount);
+ Util.ArrayPool<Color>.Release(ref colors);
+ Util.ArrayPool<Vector3>.Release(ref vertices);
+ Util.ArrayPool<int>.Release(ref triangles);
+
+ builder.Dispose();
+ }
+ }
+ }
+#endif
+
+ /// <summary>
+ /// Moves and rotates this object so that it is aligned with tiles in the first recast graph in the scene
+ ///
+ /// See: SnapToClosestTileAlignment(RecastGraph)
+ /// </summary>
+ [ContextMenu("Snap to closest tile alignment")]
+ public void SnapToClosestTileAlignment () {
+ AstarPath.FindAstarPath();
+ if (AstarPath.active != null && AstarPath.active.data.recastGraph != null) {
+ SnapToClosestTileAlignment(AstarPath.active.data.recastGraph);
+ }
+ }
+
+ /// <summary>
+ /// Applies the navmesh stored in this prefab to the first recast graph in the scene.
+ ///
+ /// See: <see cref="Apply(RecastGraph)"/> for more details.
+ /// </summary>
+ [ContextMenu("Apply here")]
+ public void Apply () {
+ AstarPath.FindAstarPath();
+ if (AstarPath.active != null && AstarPath.active.data.recastGraph != null) {
+ var graph = AstarPath.active.data.recastGraph;
+ Apply(graph);
+ }
+ }
+
+ /// <summary>Moves and rotates this object so that it is aligned with tiles in the given graph</summary>
+ public void SnapToClosestTileAlignment (RecastGraph graph) {
+ // Calculate a new tile layout, because the graph may not be scanned yet (especially if this code runs outside of play mode)
+ var tileLayout = new TileLayout(graph);
+ SnapToGraph(tileLayout, transform.position, transform.rotation, bounds, out IntRect tileRect, out int snappedRotation, out float yOffset);
+ var graphSpaceBounds = tileLayout.GetTileBoundsInGraphSpace(tileRect.xmin, tileRect.ymin, tileRect.Width, tileRect.Height);
+ var centerInGraphSpace = new Vector3(graphSpaceBounds.center.x, yOffset, graphSpaceBounds.center.z);
+#if UNITY_EDITOR
+ if (!Application.isPlaying) UnityEditor.Undo.RecordObject(transform, "Snap to closest tile alignment");
+#endif
+ transform.rotation = Quaternion.Euler(graph.rotation) * Quaternion.Euler(0, snappedRotation * 90, 0);
+ transform.position = tileLayout.transform.Transform(centerInGraphSpace) + transform.rotation*(-bounds.center + new Vector3(0, bounds.extents.y, 0));
+
+#if UNITY_EDITOR
+ if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(transform);
+#endif
+ }
+
+ /// <summary>
+ /// Rounds the size of the <see cref="bounds"/> to the closest multiple of the tile size in the graph, ensuring that the bounds cover at least 1x1 tiles.
+ /// The new bounds has the same center and size along the y-axis.
+ /// </summary>
+ public void SnapSizeToClosestTileMultiple (RecastGraph graph) {
+ this.bounds = SnapSizeToClosestTileMultiple(graph, this.bounds);
+ }
+
+ /// <summary>Start is called before the first frame update</summary>
+ void Start () {
+ startHasRun = true;
+ if (applyOnStart && serializedNavmesh != null && AstarPath.active != null && AstarPath.active.data.recastGraph != null) Apply(AstarPath.active.data.recastGraph);
+ }
+
+ void OnEnable () {
+ if (startHasRun && applyOnStart && serializedNavmesh != null && AstarPath.active != null && AstarPath.active.data.recastGraph != null) Apply(AstarPath.active.data.recastGraph);
+ }
+
+ void OnDisable () {
+ if (removeTilesWhenDisabled && serializedNavmesh != null && AstarPath.active != null) {
+ var pos = transform.position;
+ var rot = transform.rotation;
+ AstarPath.active.AddWorkItem(ctx => {
+ var graph = AstarPath.active.data.recastGraph;
+ if (graph != null) {
+ SnapToGraph(new TileLayout(graph), pos, rot, bounds, out IntRect tileRect, out int rotation, out float yOffset);
+ graph.ClearTiles(tileRect);
+ }
+ });
+ }
+ }
+
+ /// <summary>
+ /// Rounds the size of the bounds to the closest multiple of the tile size in the graph, ensuring that the bounds cover at least 1x1 tiles.
+ /// The returned bounds has the same center and size along the y-axis as the input.
+ /// </summary>
+ public static Bounds SnapSizeToClosestTileMultiple (RecastGraph graph, Bounds bounds) {
+ var tileSize = Mathf.Max(graph.editorTileSize * graph.cellSize, 0.001f);
+ var tiles = new Vector2(bounds.size.x / tileSize, bounds.size.z / tileSize);
+ var roundedTiles = new Int2(Mathf.Max(1, Mathf.RoundToInt(tiles.x)), Mathf.Max(1, Mathf.RoundToInt(tiles.y)));
+ return new Bounds(
+ bounds.center,
+ new Vector3(
+ roundedTiles.x * tileSize,
+ bounds.size.y,
+ roundedTiles.y * tileSize
+ )
+ );
+ }
+
+ public static void SnapToGraph (TileLayout tileLayout, Vector3 position, Quaternion rotation, Bounds bounds, out IntRect tileRect, out int snappedRotation, out float yOffset) {
+ var rotInGraphSpace = tileLayout.transform.InverseTransformVector(rotation * Vector3.right);
+ // Snap to increments of 90 degrees
+ snappedRotation = -Mathf.RoundToInt(Mathf.Atan2(rotInGraphSpace.z, rotInGraphSpace.x) / (0.5f*Mathf.PI));
+ var snappedRotationQ = Quaternion.Euler(0, snappedRotation * 90, 0);
+ var localToGraph = tileLayout.transform.inverseMatrix * Matrix4x4.TRS(position + snappedRotationQ * bounds.center, snappedRotationQ, Vector3.one);
+ var cornerInGraphSpace1 = localToGraph.MultiplyPoint3x4(-bounds.extents);
+ var cornerInGraphSpace2 = localToGraph.MultiplyPoint3x4(bounds.extents);
+ var minInGraphSpace = Vector3.Min(cornerInGraphSpace1, cornerInGraphSpace2);
+ var tileCoordinatesF = Vector3.Scale(minInGraphSpace, new Vector3(1.0f/tileLayout.TileWorldSizeX, 1, 1.0f/tileLayout.TileWorldSizeZ));
+ var tileCoordinates = new Int2(Mathf.RoundToInt(tileCoordinatesF.x), Mathf.RoundToInt(tileCoordinatesF.z));
+ var boundsSizeInGraphSpace = new Vector2(bounds.size.x, bounds.size.z);
+ if (((snappedRotation % 2) + 2) % 2 == 1) Util.Memory.Swap(ref boundsSizeInGraphSpace.x, ref boundsSizeInGraphSpace.y);
+ var w = Mathf.Max(1, Mathf.RoundToInt(boundsSizeInGraphSpace.x / tileLayout.TileWorldSizeX));
+ var h = Mathf.Max(1, Mathf.RoundToInt(boundsSizeInGraphSpace.y / tileLayout.TileWorldSizeZ));
+ tileRect = new IntRect(
+ tileCoordinates.x,
+ tileCoordinates.y,
+ tileCoordinates.x + w - 1,
+ tileCoordinates.y + h - 1
+ );
+
+ yOffset = minInGraphSpace.y;
+ }
+
+ /// <summary>
+ /// Applies the navmesh stored in this prefab to the given graph.
+ /// The loaded tiles will be placed at the closest valid spot to this object's current position.
+ /// Some rounding may occur because the tiles need to be aligned to the graph's tile boundaries.
+ ///
+ /// If the recast graph is not scanned, it will be initialized with empty tiles and then the tiles in this prefab will be loaded into it.
+ ///
+ /// If the recast graph is too small and the tiles would have been loaded out of bounds, the graph will first be resized to fit.
+ /// If you have a large graph, this resizing can be a somewhat expensive operation.
+ ///
+ /// See: <see cref="NavmeshPrefab.SnapToClosestTileAlignment()"/>
+ /// </summary>
+ public void Apply (RecastGraph graph) {
+ if (serializedNavmesh == null) throw new System.InvalidOperationException("Cannot Apply NavmeshPrefab because no serialized data has been set");
+
+ AstarPath.active.AddWorkItem(() => {
+ UnityEngine.Profiling.Profiler.BeginSample("NavmeshPrefab.Apply");
+ SnapToGraph(new TileLayout(graph), transform.position, transform.rotation, bounds, out IntRect tileRect, out int rotation, out float yOffset);
+
+ var tileMeshes = TileMeshes.Deserialize(serializedNavmesh.bytes);
+ tileMeshes.Rotate(rotation);
+ if (tileMeshes.tileRect.Width != tileRect.Width || tileMeshes.tileRect.Height != tileRect.Height) {
+ throw new System.Exception("NavmeshPrefab has been scanned with a different size than it is right now (or with a different graph). Expected to find " + tileRect.Width + "x" + tileRect.Height + " tiles, but found " + tileMeshes.tileRect.Width + "x" + tileMeshes.tileRect.Height);
+ }
+ tileMeshes.tileRect = tileRect;
+ graph.ReplaceTiles(tileMeshes, yOffset);
+ UnityEngine.Profiling.Profiler.EndSample();
+ });
+ }
+
+ /// <summary>Scans the navmesh using the first recast graph in the scene, and returns a serialized byte representation</summary>
+ public byte[] Scan () {
+ // Make sure this method works even when called in the editor outside of play mode.
+ AstarPath.FindAstarPath();
+ if (AstarPath.active == null || AstarPath.active.data.recastGraph == null) throw new System.InvalidOperationException("There's no recast graph in the scene. Add one if you want to scan this navmesh prefab.");
+ return Scan(AstarPath.active.data.recastGraph);
+ }
+
+ /// <summary>Scans the navmesh and returns a serialized byte representation</summary>
+ public byte[] Scan (RecastGraph graph) {
+ // Schedule the jobs asynchronously, but immediately wait for them to finish
+ var result = ScanAsync(graph).Complete();
+ var data = result.data;
+ // Dispose of all the unmanaged memory
+ result.Dispose();
+ return data;
+ }
+
+ /// <summary>
+ /// Scans the navmesh asynchronously and returns a promise of a byte representation.
+ ///
+ /// TODO: Maybe change this method to return a <see cref="TileMeshes"/> object instead?
+ /// </summary>
+ public Promise<SerializedOutput> ScanAsync (RecastGraph graph) {
+ var arena = new DisposeArena();
+
+ // First configure the rasterization settings by copying them from the recast graph,
+ // but changing which region we are interested in.
+ var tileLayout = new TileLayout(
+ new Bounds(transform.position + transform.rotation * bounds.center, bounds.size),
+ transform.rotation,
+ graph.cellSize,
+ graph.editorTileSize,
+ graph.useTiles
+ );
+ var buildSettings = RecastBuilder.BuildTileMeshes(graph, tileLayout, new IntRect(0, 0, tileLayout.tileCount.x - 1, tileLayout.tileCount.y - 1));
+ buildSettings.scene = this.gameObject.scene;
+
+ // Schedule the jobs asynchronously
+ var tileMeshesPromise = buildSettings.Schedule(arena);
+ var output = new SerializedOutput {
+ promise = tileMeshesPromise,
+ arena = arena,
+ };
+ var serializeJob = new SerializeJob {
+ tileMeshesPromise = tileMeshesPromise,
+ output = output,
+ }.ScheduleManaged(tileMeshesPromise.handle);
+
+ return new Promise<SerializedOutput>(serializeJob, output);
+ }
+
+ public class SerializedOutput : IProgress, System.IDisposable {
+ public Promise<TileBuilder.TileBuilderOutput> promise;
+ public byte[] data;
+ public DisposeArena arena;
+
+ public float Progress => promise.Progress;
+
+ public void Dispose () {
+ // Dispose of all the unmanaged memory
+ promise.Dispose();
+ arena.DisposeAll();
+ }
+ }
+
+ struct SerializeJob : IJob {
+ public Promise<TileBuilder.TileBuilderOutput> tileMeshesPromise;
+ public SerializedOutput output;
+
+ public void Execute () {
+ // Note: Assumes that the tileMeshesPromise has already completed
+ var tileMeshes = tileMeshesPromise.GetValue();
+ // Serialize the data to a byte array
+ output.data = tileMeshes.tileMeshes.ToManaged().Serialize();
+ }
+ }
+
+#if UNITY_EDITOR
+ /// <summary>
+ /// Saves the given data to the <see cref="serializedNavmesh"/> field, or creates a new file if none exists.
+ ///
+ /// A new file will be created if <see cref="serializedNavmesh"/> is null.
+ /// If this object is part of a prefab, the file name will be based on the prefab's name.
+ ///
+ /// Warning: This method is only available in the editor.
+ ///
+ /// Warning: You should only pass valid serialized tile data to this function.
+ ///
+ /// See: <see cref="Scan"/>
+ /// See: <see cref="ScanAsync"/>
+ /// </summary>
+ public void SaveToFile (byte[] data) {
+ string path;
+ if (serializedNavmesh != null) {
+ // If we already have a file, just overwrite it
+ path = UnityEditor.AssetDatabase.GetAssetPath(serializedNavmesh);
+ } else {
+ // Otherwise create a new file.
+ // If this is a prefab, base the name on the prefab's name.
+ System.IO.Directory.CreateDirectory(Application.dataPath + "/Tiles");
+ var name = "tiles";
+ var prefabPath = UnityEditor.PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(this);
+ if (prefabPath != null && prefabPath != "") {
+ name = System.IO.Path.GetFileNameWithoutExtension(prefabPath);
+ }
+ name = name.Replace("/", "_").Replace(".", "_").Replace("__", "_");
+ path = UnityEditor.AssetDatabase.GenerateUniqueAssetPath("Assets/Tiles/" + name + ".bytes");
+ }
+ var fullPath = Application.dataPath + "/../" + path;
+ System.IO.File.WriteAllBytes(fullPath, data);
+
+ UnityEditor.AssetDatabase.Refresh();
+ serializedNavmesh = UnityEditor.AssetDatabase.LoadAssetAtPath(path, typeof(TextAsset)) as TextAsset;
+ // Required if we do this in edit mode
+ UnityEditor.EditorUtility.SetDirty(this);
+ }
+
+ /// <summary>
+ /// Scans the navmesh and saves it to the <see cref="serializedNavmesh"/> field.
+ /// A new file will be created if <see cref="serializedNavmesh"/> is null.
+ /// If this object is part of a prefab, the file name will be based on the prefab's name.
+ ///
+ /// Note: This method is only available in the editor.
+ /// </summary>
+ public void ScanAndSaveToFile () {
+ SaveToFile(Scan());
+ }
+#endif
+
+ protected override void OnUpgradeSerializedData (ref Migrations migrations, bool unityThread) {
+ migrations.TryMigrateFromLegacyFormat(out var _);
+ if (migrations.AddAndMaybeRunMigration(1 << 0)) {
+ removeTilesWhenDisabled = false;
+ }
+ base.OnUpgradeSerializedData(ref migrations, unityThread);
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs.meta
new file mode 100644
index 0000000..fb977d5
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NavmeshPrefab.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9c8f31ae577c3d7af956114ba0f28148
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs
new file mode 100644
index 0000000..97a3774
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs
@@ -0,0 +1,153 @@
+using UnityEngine;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace Pathfinding {
+ using Pathfinding.Util;
+ using Pathfinding.Drawing;
+
+ /// <summary>
+ /// Connects two nodes with a direct connection.
+ /// It is not possible to detect this link when following a path (which may be good or bad), for that you can use NodeLink2.
+ ///
+ /// [Open online documentation to see images]
+ ///
+ /// See: editing-graphs (view in online documentation for working links)
+ /// </summary>
+ [AddComponentMenu("Pathfinding/Link")]
+ [HelpURL("https://arongranberg.com/astar/documentation/stable/nodelink.html")]
+ public class NodeLink : GraphModifier {
+ /// <summary>End position of the link</summary>
+ public Transform end;
+
+ /// <summary>
+ /// The connection will be this times harder/slower to traverse.
+ /// Note that values lower than one will not always make the pathfinder choose this path instead of another path even though this one should
+ /// lead to a lower total cost unless you also adjust the Heuristic Scale in A* Inspector -> Settings -> Pathfinding or disable the heuristic altogether.
+ /// </summary>
+ public float costFactor = 1.0f;
+
+ /// <summary>Make a one-way connection</summary>
+ public bool oneWay = false;
+
+ /// <summary>Delete existing connection instead of adding one</summary>
+ public bool deleteConnection = false;
+
+ public Transform Start {
+ get { return transform; }
+ }
+
+ public Transform End {
+ get { return end; }
+ }
+
+ public override void OnGraphsPostUpdateBeforeAreaRecalculation () {
+ Apply();
+ }
+
+ public static void DrawArch (Vector3 a, Vector3 b, Vector3 up, Color color) {
+ Vector3 dir = b - a;
+
+ if (dir == Vector3.zero) return;
+
+ var normal = Vector3.Cross(up, dir);
+ var normalUp = Vector3.Cross(dir, normal).normalized * dir.magnitude * 0.1f;
+
+ Draw.Bezier(a, a + normalUp, b + normalUp, b, color);
+ }
+
+ /// <summary>
+ /// Connects the start and end points using a link or refreshes the existing link.
+ ///
+ /// If you have moved the link or otherwise modified it you need to call this method.
+ ///
+ /// Warning: This must only be done when it is safe to update the graph structure.
+ /// The easiest is to do it inside a work item. See <see cref="AstarPath.AddWorkItem"/>.
+ /// </summary>
+ public virtual void Apply () {
+ if (Start == null || End == null || AstarPath.active == null) return;
+
+ GraphNode startNode = AstarPath.active.GetNearest(Start.position).node;
+ GraphNode endNode = AstarPath.active.GetNearest(End.position).node;
+
+ if (startNode == null || endNode == null) return;
+
+
+ if (deleteConnection) {
+ GraphNode.Disconnect(startNode, endNode);
+ } else {
+ uint cost = (uint)System.Math.Round((startNode.position-endNode.position).costMagnitude*costFactor);
+
+ GraphNode.Connect(startNode, endNode, cost, oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay);
+ }
+ }
+
+ public override void DrawGizmos () {
+ if (Start == null || End == null) return;
+
+ NodeLink.DrawArch(Start.position, End.position, Vector3.up, deleteConnection ? Color.red : Color.green);
+ }
+
+#if UNITY_EDITOR
+ [UnityEditor.MenuItem("Edit/Pathfinding/Link Pair %&l")]
+ public static void LinkObjects () {
+ Transform[] tfs = Selection.transforms;
+ if (tfs.Length == 2) {
+ LinkObjects(tfs[0], tfs[1], false);
+ }
+ SceneView.RepaintAll();
+ }
+
+ [UnityEditor.MenuItem("Edit/Pathfinding/Unlink Pair %&u")]
+ public static void UnlinkObjects () {
+ Transform[] tfs = Selection.transforms;
+ if (tfs.Length == 2) {
+ LinkObjects(tfs[0], tfs[1], true);
+ }
+ SceneView.RepaintAll();
+ }
+
+ [UnityEditor.MenuItem("Edit/Pathfinding/Delete Links on Selected %&b")]
+ public static void DeleteLinks () {
+ Transform[] tfs = Selection.transforms;
+ for (int i = 0; i < tfs.Length; i++) {
+ NodeLink[] conns = tfs[i].GetComponents<NodeLink>();
+ for (int j = 0; j < conns.Length; j++) DestroyImmediate(conns[j]);
+ }
+ SceneView.RepaintAll();
+ }
+
+ public static void LinkObjects (Transform a, Transform b, bool removeConnection) {
+ NodeLink connecting = null;
+
+ NodeLink[] conns = a.GetComponents<NodeLink>();
+ for (int i = 0; i < conns.Length; i++) {
+ if (conns[i].end == b) {
+ connecting = conns[i];
+ break;
+ }
+ }
+
+ conns = b.GetComponents<NodeLink>();
+ for (int i = 0; i < conns.Length; i++) {
+ if (conns[i].end == a) {
+ connecting = conns[i];
+ break;
+ }
+ }
+
+ if (removeConnection) {
+ if (connecting != null) DestroyImmediate(connecting);
+ } else {
+ if (connecting == null) {
+ connecting = a.gameObject.AddComponent<NodeLink>();
+ connecting.end = b;
+ } else {
+ connecting.deleteConnection = !connecting.deleteConnection;
+ }
+ }
+ }
+#endif
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs.meta
new file mode 100644
index 0000000..b4282dc
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: ca327ce4e754a4597a70fb963758f8bd
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs
new file mode 100644
index 0000000..9f99d46
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs
@@ -0,0 +1,405 @@
+using UnityEngine;
+
+namespace Pathfinding {
+ using Pathfinding.Util;
+ using Pathfinding.Drawing;
+
+ /// <summary>
+ /// Interface for handling off-mesh links.
+ ///
+ /// If a component implements this interface, and is attached to the same GameObject as a NodeLink2 component,
+ /// then the OnTraverseOffMeshLink method will be called when an agent traverses the off-mesh link.
+ ///
+ /// This only works with the <see cref="FollowerEntity"/> component.
+ ///
+ /// Note: <see cref="FollowerEntity.onTraverseOffMeshLink"/> overrides this callback, if set.
+ ///
+ /// The <see cref="Interactable"/> component implements this interface, and allows a small state-machine to run when the agent traverses the link.
+ ///
+ /// See: <see cref="FollowerEntity.onTraverseOffMeshLink"/>
+ /// See: offmeshlinks (view in online documentation for working links)
+ /// </summary>
+ public interface IOffMeshLinkHandler {
+ /// <summary>
+ /// Name of the handler.
+ /// This is used to identify the handler in the inspector.
+ /// </summary>
+ public string name => null;
+
+#if MODULE_ENTITIES
+ /// <summary>
+ /// Called when an agent starts traversing an off-mesh link.
+ ///
+ /// This can be used to perform any setup that is necessary for the traversal.
+ ///
+ /// Returns: An object which will be used to control the agent for the full duration of the link traversal.
+ ///
+ /// For simple cases, you can often implement <see cref="IOffMeshLinkHandler"/> and <see cref="IOffMeshLinkStateMachine"/> in the same class, and then
+ /// just make this method return this.
+ /// For more complex cases, you may want to keep track of the agent's identity, and thus want to return a new object for each agent that traverses the link.
+ /// </summary>
+ /// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
+ /// This context is only valid for this method call. Do not store it and use it later.</param>
+ public IOffMeshLinkStateMachine GetOffMeshLinkStateMachine(ECS.AgentOffMeshLinkTraversalContext context);
+#endif
+ }
+
+ public interface IOffMeshLinkStateMachine {
+#if MODULE_ENTITIES
+ /// <summary>
+ /// Called when an agent traverses an off-mesh link.
+ /// This method should be a coroutine (i.e return an IEnumerable) which will be iterated over until it finishes, or the agent is destroyed.
+ /// The coroutine should yield null every frame until the agent has finished traversing the link.
+ ///
+ /// When the coroutine completes, the agent will be assumed to have reached the end of the link and control
+ /// will be returned to the normal movement code.
+ ///
+ /// The coroutine typically moves the agent to the end of the link over some time, and perform any other actions that are necessary.
+ /// For example, it could play an animation, or move the agent in a specific way.
+ /// </summary>
+ /// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
+ /// This context is only valid when this coroutine steps forward. Do not store it and use it elsewhere.</param>
+ System.Collections.IEnumerable OnTraverseOffMeshLink(ECS.AgentOffMeshLinkTraversalContext context) => ECS.JobStartOffMeshLinkTransition.DefaultOnTraverseOffMeshLink(context);
+
+ /// <summary>
+ /// Called when an agent finishes traversing an off-mesh link.
+ ///
+ /// This can be used to perform any cleanup that is necessary after the traversal.
+ ///
+ /// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both.
+ /// </summary>
+ /// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
+ /// This context is only valid for this method call. Do not store it and use it later.</param>
+ void OnFinishTraversingOffMeshLink (ECS.AgentOffMeshLinkTraversalContext context) {}
+#endif
+
+ /// <summary>
+ /// Called when an agent fails to finish traversing an off-mesh link.
+ ///
+ /// This can be used to perform any cleanup that is necessary after the traversal.
+ ///
+ /// An abort can happen if the agent was destroyed while it was traversing the link. It can also happen if the agent was teleported somewhere else while traversing the link.
+ ///
+ /// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both.
+ ///
+ /// Warning: When this is called, the agent may already be destroyed. The handler component itself could also be destroyed at this point.
+ /// </summary>
+ void OnAbortTraversingOffMeshLink () {}
+ }
+
+ /// <summary>
+ /// Connects two nodes using an off-mesh link.
+ /// In contrast to the <see cref="NodeLink"/> component, this link type will not connect the nodes directly
+ /// instead it will create two link nodes at the start and end position of this link and connect
+ /// through those nodes.
+ ///
+ /// If the closest node to this object is called A and the closest node to the end transform is called
+ /// D, then it will create one link node at this object's position (call it B) and one link node at
+ /// the position of the end transform (call it C), it will then connect A to B, B to C and C to D.
+ ///
+ /// This link type is possible to detect while following since it has these special link nodes in the middle.
+ /// The link corresponding to one of those intermediate nodes can be retrieved using the <see cref="GetNodeLink"/> method
+ /// which can be of great use if you want to, for example, play a link specific animation when reaching the link.
+ ///
+ /// \inspectorField{End, NodeLink2.end}
+ /// \inspectorField{Cost Factor, NodeLink2.costFactor}
+ /// \inspectorField{One Way, NodeLink2.oneWay}
+ /// \inspectorField{Pathfinding Tag, NodeLink2.pathfindingTag}
+ /// \inspectorField{Graph Mask, NodeLink2.graphMask}
+ ///
+ /// See: offmeshlinks (view in online documentation for working links)
+ /// See: The example scene RecastExample2 contains a few links which you can take a look at to see how they are used.
+ ///
+ /// Note: If you make any modifications to the node link's settings after it has been created, you need to call the <see cref="Apply"/> method in order to apply the changes to the graph.
+ /// </summary>
+ [AddComponentMenu("Pathfinding/Link2")]
+ [HelpURL("https://arongranberg.com/astar/documentation/stable/nodelink2.html")]
+ public class NodeLink2 : GraphModifier {
+ /// <summary>End position of the link</summary>
+ public Transform end;
+
+ /// <summary>
+ /// The connection will be this times harder/slower to traverse.
+ ///
+ /// A cost factor of 1 means that the link is equally expensive as moving the same distance on the normal navmesh. But a cost factor greater than 1 means that it is proportionally more expensive.
+ ///
+ /// You should not use a cost factor less than 1 unless you also change the <see cref="AstarPath.heuristicScale"/> field (A* Inspector -> Settings -> Pathfinding) to at most the minimum cost factor that you use anywhere in the scene (or disable the heuristic altogether).
+ /// This is because the pathfinding algorithm assumes that paths are at least as costly as walking just the straight line distance to the target, and if you use a cost factor less than 1, that assumption is no longer true.
+ /// What then happens is that the pathfinding search may ignore some links because it doesn't even think to search in that direction, even if they would have lead to a lower path cost.
+ ///
+ /// Warning: Reducing the heuristic scale or disabling the heuristic can significantly increase the cpu cost for pathfinding, especially for large graphs.
+ ///
+ /// Read more about this at https://en.wikipedia.org/wiki/Admissible_heuristic.
+ /// </summary>
+ public float costFactor = 1.0f;
+
+ /// <summary>Make a one-way connection</summary>
+ public bool oneWay = false;
+
+ /// <summary>
+ /// The tag to apply to the link.
+ ///
+ /// This can be used to exclude certain agents from using the link, or make it more expensive to use.
+ ///
+ /// See: tags (view in online documentation for working links)
+ /// </summary>
+ public PathfindingTag pathfindingTag = 0;
+
+ /// <summary>
+ /// Which graphs this link is allowed to connect.
+ ///
+ /// The link will always connect the nodes closest to the start and end points on the graphs that it is allowed to connect.
+ /// </summary>
+ public GraphMask graphMask = -1;
+
+ public Transform StartTransform => transform;
+
+ public Transform EndTransform => end;
+
+ protected OffMeshLinks.OffMeshLinkSource linkSource;
+
+ /// <summary>
+ /// Returns the link component associated with the specified node.
+ /// Returns: The link associated with the node or null if the node is not a link node, or it is not associated with a <see cref="NodeLink2"/> component.
+ /// </summary>
+ /// <param name="node">The node to get the link for.</param>
+ public static NodeLink2 GetNodeLink(GraphNode node) => node is LinkNode linkNode ? linkNode.linkSource.component as NodeLink2 : null;
+
+ /// <summary>
+ /// True if the link is connected to the graph.
+ ///
+ /// This will be true if the link has been successfully connected to the graph, and false if it either has failed, or if the component/gameobject is disabled.
+ ///
+ /// When the component is enabled, the link will be scheduled to be added to the graph, it will not take effect immediately.
+ /// This means that this property will return false until the next time graph updates are processed (usually later this frame, or next frame).
+ /// To ensure the link is refreshed immediately, you can call <see cref="AstarPath.active.FlushWorkItems"/>.
+ /// </summary>
+ internal bool isActive => linkSource != null && (linkSource.status & OffMeshLinks.OffMeshLinkStatus.Active) != 0;
+
+ IOffMeshLinkHandler onTraverseOffMeshLinkHandler;
+
+ /// <summary>
+ /// Callback to be called when an agent starts traversing an off-mesh link.
+ ///
+ /// The handler will be called when the agent starts traversing an off-mesh link.
+ /// It allows you to to control the agent for the full duration of the link traversal.
+ ///
+ /// Use the passed context struct to get information about the link and to control the agent.
+ ///
+ /// <code>
+ /// using UnityEngine;
+ /// using Pathfinding;
+ /// using System.Collections;
+ /// using Pathfinding.ECS;
+ ///
+ /// namespace Pathfinding.Examples {
+ /// public class FollowerJumpLink : MonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine {
+ /// // Register this class as the handler for off-mesh links when the component is enabled
+ /// void OnEnable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = this;
+ /// void OnDisable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = null;
+ ///
+ /// IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context) => this;
+ ///
+ /// void IOffMeshLinkStateMachine.OnFinishTraversingOffMeshLink (AgentOffMeshLinkTraversalContext context) {
+ /// Debug.Log("An agent finished traversing an off-mesh link");
+ /// }
+ ///
+ /// void IOffMeshLinkStateMachine.OnAbortTraversingOffMeshLink () {
+ /// Debug.Log("An agent aborted traversing an off-mesh link");
+ /// }
+ ///
+ /// IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext ctx) {
+ /// var start = (Vector3)ctx.link.relativeStart;
+ /// var end = (Vector3)ctx.link.relativeEnd;
+ /// var dir = end - start;
+ ///
+ /// // Disable local avoidance while traversing the off-mesh link.
+ /// // If it was enabled, it will be automatically re-enabled when the agent finishes traversing the link.
+ /// ctx.DisableLocalAvoidance();
+ ///
+ /// // Move and rotate the agent to face the other side of the link.
+ /// // When reaching the off-mesh link, the agent may be facing the wrong direction.
+ /// while (!ctx.MoveTowards(
+ /// position: start,
+ /// rotation: Quaternion.LookRotation(dir, ctx.movementPlane.up),
+ /// gravity: true,
+ /// slowdown: true).reached) {
+ /// yield return null;
+ /// }
+ ///
+ /// var bezierP0 = start;
+ /// var bezierP1 = start + Vector3.up*5;
+ /// var bezierP2 = end + Vector3.up*5;
+ /// var bezierP3 = end;
+ /// var jumpDuration = 1.0f;
+ ///
+ /// // Animate the AI to jump from the start to the end of the link
+ /// for (float t = 0; t < jumpDuration; t += ctx.deltaTime) {
+ /// ctx.transform.Position = AstarSplines.CubicBezier(bezierP0, bezierP1, bezierP2, bezierP3, Mathf.SmoothStep(0, 1, t / jumpDuration));
+ /// yield return null;
+ /// }
+ /// }
+ /// }
+ /// }
+ /// </code>
+ ///
+ /// Warning: Off-mesh links can be destroyed or disabled at any moment. The built-in code will attempt to make the agent continue following the link even if it is destroyed,
+ /// but if you write your own traversal code, you should be aware of this.
+ ///
+ /// You can alternatively set the corresponding property property on the agent (<see cref="FollowerEntity.onTraverseOffMeshLink"/>) to specify a callback for a all off-mesh links.
+ ///
+ /// Note: The agent's off-mesh link handler takes precedence over the link's off-mesh link handler, if both are set.
+ ///
+ /// Warning: This property only works with the <see cref="FollowerEntity"/> component. Use <see cref="RichAI.onTraverseOffMeshLink"/> if you are using the <see cref="RichAI"/> movement script.
+ ///
+ /// See: offmeshlinks (view in online documentation for working links) for more details and example code
+ /// </summary>
+ public IOffMeshLinkHandler onTraverseOffMeshLink {
+ get => onTraverseOffMeshLinkHandler;
+ set {
+ onTraverseOffMeshLinkHandler = value;
+ if (linkSource != null) linkSource.handler = value;
+ }
+ }
+
+ public override void OnPostScan () {
+ TryAddLink();
+ }
+
+ protected override void OnEnable () {
+ base.OnEnable();
+ if (Application.isPlaying && !BatchedEvents.Has(this)) BatchedEvents.Add(this, BatchedEvents.Event.Update, OnUpdate);
+ TryAddLink();
+ }
+
+ static void OnUpdate (NodeLink2[] components, int count) {
+ // Only check for moved links every N frames, for performance
+ if ((Time.frameCount % 16) != 0) return;
+
+ for (int i = 0; i < count; i++) {
+ var comp = components[i];
+ var start = comp.StartTransform;
+ var end = comp.EndTransform;
+ var added = comp.linkSource != null;
+ if ((start != null && end != null) != added || (added && (start.hasChanged || end.hasChanged))) {
+ if (start != null) start.hasChanged = false;
+ if (end != null) end.hasChanged = false;
+ comp.RemoveLink();
+ comp.TryAddLink();
+ }
+ }
+ }
+
+ void TryAddLink () {
+ // In case the AstarPath component has been destroyed (destroying the link).
+ // But do not clear it if the link is inactive because it failed to become enabled
+ if (linkSource != null && (linkSource.status == OffMeshLinks.OffMeshLinkStatus.Inactive || (linkSource.status & OffMeshLinks.OffMeshLinkStatus.PendingRemoval) != 0)) linkSource = null;
+
+ if (linkSource == null && AstarPath.active != null && EndTransform != null) {
+ StartTransform.hasChanged = false;
+ EndTransform.hasChanged = false;
+
+ linkSource = new OffMeshLinks.OffMeshLinkSource {
+ start = new OffMeshLinks.Anchor {
+ center = StartTransform.position,
+ rotation = StartTransform.rotation,
+ width = 0f,
+ },
+ end = new OffMeshLinks.Anchor {
+ center = EndTransform.position,
+ rotation = EndTransform.rotation,
+ width = 0f,
+ },
+ directionality = oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay,
+ tag = pathfindingTag,
+ costFactor = costFactor,
+ graphMask = graphMask,
+ maxSnappingDistance = 1, // TODO
+ component = this,
+ handler = onTraverseOffMeshLink,
+ };
+ AstarPath.active.offMeshLinks.Add(linkSource);
+ }
+ }
+
+ void RemoveLink () {
+ if (AstarPath.active != null && linkSource != null) AstarPath.active.offMeshLinks.Remove(linkSource);
+ linkSource = null;
+ }
+
+ protected override void OnDisable () {
+ base.OnDisable();
+ BatchedEvents.Remove(this);
+ RemoveLink();
+ }
+
+ [ContextMenu("Recalculate neighbours")]
+ void ContextApplyForce () {
+ Apply();
+ }
+
+ /// <summary>
+ /// Disconnects and then reconnects the link to the graph.
+ ///
+ /// If you have moved the link or otherwise modified it you need to call this method to apply those changes.
+ /// </summary>
+ public virtual void Apply () {
+ RemoveLink();
+ TryAddLink();
+ }
+
+ private readonly static Color GizmosColor = new Color(206.0f/255.0f, 136.0f/255.0f, 48.0f/255.0f, 0.5f);
+ private readonly static Color GizmosColorSelected = new Color(235.0f/255.0f, 123.0f/255.0f, 32.0f/255.0f, 1.0f);
+
+ public override void DrawGizmos () {
+ if (StartTransform == null || EndTransform == null) return;
+
+ var startPos = StartTransform.position;
+ var endPos = EndTransform.position;
+ if (linkSource != null && (Time.renderedFrameCount % 16) == 0 && Application.isEditor) {
+ // Check if the link has moved
+ // During runtime, this will be done by the OnUpdate method instead
+ if (linkSource.start.center != startPos || linkSource.end.center != endPos || linkSource.directionality != (oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay) || linkSource.costFactor != costFactor || linkSource.graphMask != graphMask || linkSource.tag != pathfindingTag) {
+ Apply();
+ }
+ }
+
+ bool selected = GizmoContext.InActiveSelection(this);
+ var graphs = linkSource != null && AstarPath.active != null? AstarPath.active.offMeshLinks.ConnectedGraphs(linkSource) : null;
+ var up = Vector3.up;
+
+ // Find the natural up direction of the connected graphs, so that we can orient the gizmos appropriately
+ if (graphs != null) {
+ for (int i = 0; i < graphs.Count; i++) {
+ var graph = graphs[i];
+ if (graph != null) {
+ if (graph is NavmeshBase navmesh) {
+ up = navmesh.transform.WorldUpAtGraphPosition(Vector3.zero);
+ break;
+ } else if (graph is GridGraph grid) {
+ up = grid.transform.WorldUpAtGraphPosition(Vector3.zero);
+ break;
+ }
+ }
+ }
+ ListPool<NavGraph>.Release(ref graphs);
+ }
+
+ var active = linkSource != null && linkSource.status == OffMeshLinks.OffMeshLinkStatus.Active;
+ Color color = selected ? GizmosColorSelected : GizmosColor;
+ if (active) color = Color.green;
+
+ Draw.Circle(startPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectStart) ? Color.red : color);
+ Draw.Circle(endPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectEnd) ? Color.red : color);
+
+ NodeLink.DrawArch(startPos, endPos, up, color);
+ if (selected) {
+ Vector3 cross = Vector3.Cross(up, endPos-startPos).normalized;
+ using (Draw.WithLineWidth(2)) {
+ NodeLink.DrawArch(startPos+cross*0.0f, endPos+cross*0.0f, up, color);
+ }
+ // NodeLink.DrawArch(startPos-cross*0.1f, endPos-cross*0.1f, color);
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs.meta
new file mode 100644
index 0000000..a8151fb
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bd2cfff5dfa8c4244aa00fea9675adb2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: eb30e238fe6211b4fa5f441a73bd01ef, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs
new file mode 100644
index 0000000..aa89999
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs
@@ -0,0 +1,295 @@
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace Pathfinding {
+ using Pathfinding.Drawing;
+
+ public class NodeLink3Node : PointNode {
+ public NodeLink3 link;
+ public Vector3 portalA;
+ public Vector3 portalB;
+
+ public NodeLink3Node (AstarPath astar) {
+ astar.InitializeNode(this);
+ }
+
+ public override bool GetPortal (GraphNode other, out Vector3 left, out Vector3 right) {
+ left = portalA;
+ right = portalB;
+ if (this.connections.Length < 2) return false;
+
+ if (this.connections.Length != 2) throw new System.Exception("Invalid NodeLink3Node. Expected 2 connections, found " + this.connections.Length);
+
+ return true;
+ }
+
+ public GraphNode GetOther (GraphNode a) {
+ if (this.connections.Length < 2) return null;
+ if (this.connections.Length != 2) throw new System.Exception("Invalid NodeLink3Node. Expected 2 connections, found " + this.connections.Length);
+
+ return a == connections[0].node ? (connections[1].node as NodeLink3Node).GetOtherInternal(this) : (connections[0].node as NodeLink3Node).GetOtherInternal(this);
+ }
+
+ GraphNode GetOtherInternal (GraphNode a) {
+ if (this.connections.Length < 2) return null;
+ return a == connections[0].node ? connections[1].node : connections[0].node;
+ }
+ }
+
+ /// <summary>
+ /// Connects two TriangleMeshNodes (recast/navmesh graphs) as if they had shared an edge.
+ /// Note: Usually you do not want to use this type of link, you want to use NodeLink2 or NodeLink (sorry for the not so descriptive names).
+ /// </summary>
+ [AddComponentMenu("Pathfinding/Link3")]
+ [HelpURL("https://arongranberg.com/astar/documentation/stable/nodelink3.html")]
+ public class NodeLink3 : GraphModifier {
+ protected static Dictionary<GraphNode, NodeLink3> reference = new Dictionary<GraphNode, NodeLink3>();
+ public static NodeLink3 GetNodeLink (GraphNode node) {
+ reference.TryGetValue(node, out NodeLink3 v);
+ return v;
+ }
+
+ /// <summary>End position of the link</summary>
+ public Transform end;
+
+ /// <summary>
+ /// The connection will be this times harder/slower to traverse.
+ /// Note that values lower than one will not always make the pathfinder choose this path instead of another path even though this one should
+ /// lead to a lower total cost unless you also adjust the Heuristic Scale in A* Inspector -> Settings -> Pathfinding or disable the heuristic altogether.
+ /// </summary>
+ public float costFactor = 1.0f;
+
+ public Transform StartTransform {
+ get { return transform; }
+ }
+
+ public Transform EndTransform {
+ get { return end; }
+ }
+
+ NodeLink3Node startNode;
+ NodeLink3Node endNode;
+ MeshNode connectedNode1, connectedNode2;
+ Vector3 clamped1, clamped2;
+ bool postScanCalled = false;
+
+ public GraphNode StartNode {
+ get { return startNode; }
+ }
+
+ public GraphNode EndNode {
+ get { return endNode; }
+ }
+
+ public override void OnPostScan () {
+ if (AstarPath.active.isScanning) {
+ InternalOnPostScan();
+ } else {
+ AstarPath.active.AddWorkItem(new AstarWorkItem(_ => {
+ InternalOnPostScan();
+ return true;
+ }));
+ }
+ }
+
+ public void InternalOnPostScan () {
+#if !ASTAR_NO_POINT_GRAPH
+ if (AstarPath.active.data.pointGraph == null) {
+ AstarPath.active.data.AddGraph(typeof(PointGraph));
+ }
+
+ //Get nearest nodes from the first point graph, assuming both start and end transforms are nodes
+ startNode = AstarPath.active.data.pointGraph.AddNode(new NodeLink3Node(AstarPath.active), (Int3)StartTransform.position);
+ startNode.link = this;
+ endNode = AstarPath.active.data.pointGraph.AddNode(new NodeLink3Node(AstarPath.active), (Int3)EndTransform.position);
+ endNode.link = this;
+#else
+ throw new System.Exception("Point graphs are not included. Check your A* Optimization settings.");
+#endif
+ connectedNode1 = null;
+ connectedNode2 = null;
+
+ if (startNode == null || endNode == null) {
+ startNode = null;
+ endNode = null;
+ return;
+ }
+
+ postScanCalled = true;
+ reference[startNode] = this;
+ reference[endNode] = this;
+ Apply(true);
+ }
+
+ public override void OnGraphsPostUpdateBeforeAreaRecalculation () {
+ if (!AstarPath.active.isScanning) {
+ if (connectedNode1 != null && connectedNode1.Destroyed) {
+ connectedNode1 = null;
+ }
+ if (connectedNode2 != null && connectedNode2.Destroyed) {
+ connectedNode2 = null;
+ }
+
+ if (!postScanCalled) {
+ OnPostScan();
+ } else {
+ //OnPostScan will also call this method
+ Apply(false);
+ }
+ }
+ }
+
+ protected override void OnEnable () {
+ base.OnEnable();
+
+#if !ASTAR_NO_POINT_GRAPH
+ if (Application.isPlaying && AstarPath.active != null && AstarPath.active.data != null && AstarPath.active.data.pointGraph != null) {
+ OnGraphsPostUpdate();
+ }
+#endif
+ }
+
+ protected override void OnDisable () {
+ base.OnDisable();
+
+ postScanCalled = false;
+
+ if (startNode != null) reference.Remove(startNode);
+ if (endNode != null) reference.Remove(endNode);
+
+ if (startNode != null && endNode != null) {
+ startNode.RemovePartialConnection(endNode);
+ endNode.RemovePartialConnection(startNode);
+
+ if (connectedNode1 != null && connectedNode2 != null) {
+ startNode.RemovePartialConnection(connectedNode1);
+ connectedNode1.RemovePartialConnection(startNode);
+
+ endNode.RemovePartialConnection(connectedNode2);
+ connectedNode2.RemovePartialConnection(endNode);
+ }
+ }
+ }
+
+ void RemoveConnections (GraphNode node) {
+ //TODO, might be better to replace connection
+ node.ClearConnections(true);
+ }
+
+ [ContextMenu("Recalculate neighbours")]
+ void ContextApplyForce () {
+ if (Application.isPlaying) {
+ Apply(true);
+ }
+ }
+
+ public void Apply (bool forceNewCheck) {
+ //TODO
+ //This function assumes that connections from the n1,n2 nodes never need to be removed in the future (e.g because the nodes move or something)
+ NNConstraint nn = NNConstraint.None;
+
+ nn.distanceMetric = DistanceMetric.ClosestAsSeenFromAboveSoft();
+ int graph = (int)startNode.GraphIndex;
+
+ //Search all graphs but the one which start and end nodes are on
+ nn.graphMask = ~(1 << graph);
+
+ bool same = true;
+
+ {
+ var info = AstarPath.active.GetNearest(StartTransform.position, nn);
+ same &= info.node == connectedNode1 && info.node != null;
+ connectedNode1 = info.node as MeshNode;
+ clamped1 = info.position;
+ if (connectedNode1 != null) Debug.DrawRay((Vector3)connectedNode1.position, Vector3.up*5, Color.red);
+ }
+
+ {
+ var info = AstarPath.active.GetNearest(EndTransform.position, nn);
+ same &= info.node == connectedNode2 && info.node != null;
+ connectedNode2 = info.node as MeshNode;
+ clamped2 = info.position;
+ if (connectedNode2 != null) Debug.DrawRay((Vector3)connectedNode2.position, Vector3.up*5, Color.cyan);
+ }
+
+ if (connectedNode2 == null || connectedNode1 == null) return;
+
+ startNode.SetPosition((Int3)StartTransform.position);
+ endNode.SetPosition((Int3)EndTransform.position);
+
+ if (same && !forceNewCheck) return;
+
+ RemoveConnections(startNode);
+ RemoveConnections(endNode);
+
+ uint cost = (uint)Mathf.RoundToInt(((Int3)(StartTransform.position-EndTransform.position)).costMagnitude*costFactor);
+ GraphNode.Connect(startNode, endNode, cost);
+
+ Int3 dir = connectedNode2.position - connectedNode1.position;
+
+ for (int a = 0; a < connectedNode1.GetVertexCount(); a++) {
+ Int3 va1 = connectedNode1.GetVertex(a);
+ Int3 va2 = connectedNode1.GetVertex((a+1) % connectedNode1.GetVertexCount());
+
+ if (Int3.DotLong((va2-va1).Normal2D(), dir) > 0) continue;
+
+ for (int b = 0; b < connectedNode2.GetVertexCount(); b++) {
+ Int3 vb1 = connectedNode2.GetVertex(b);
+ Int3 vb2 = connectedNode2.GetVertex((b+1) % connectedNode2.GetVertexCount());
+
+ if (Int3.DotLong((vb2-vb1).Normal2D(), dir) < 0) continue;
+
+ if (Int3.Angle((vb2-vb1), (va2-va1)) > (170.0/360.0f)*Mathf.PI*2) {
+ float t1 = 0;
+ float t2 = 1;
+
+ t2 = System.Math.Min(t2, VectorMath.ClosestPointOnLineFactor(va1, va2, vb1));
+ t1 = System.Math.Max(t1, VectorMath.ClosestPointOnLineFactor(va1, va2, vb2));
+
+ if (t2 < t1) {
+ Debug.LogError("Something went wrong! " + t1 + " " + t2 + " " + va1 + " " + va2 + " " + vb1 + " " + vb2+"\nTODO, how can this happen?");
+ } else {
+ Vector3 pa = (Vector3)(va2-va1)*t1 + (Vector3)va1;
+ Vector3 pb = (Vector3)(va2-va1)*t2 + (Vector3)va1;
+
+ startNode.portalA = pa;
+ startNode.portalB = pb;
+
+ endNode.portalA = pb;
+ endNode.portalB = pa;
+
+ //Add connections between nodes, or replace old connections if existing
+ GraphNode.Connect(connectedNode1, startNode, (uint)Mathf.RoundToInt(((Int3)(clamped1 - StartTransform.position)).costMagnitude*costFactor));
+ GraphNode.Connect(endNode, connectedNode2, (uint)Mathf.RoundToInt(((Int3)(clamped2 - EndTransform.position)).costMagnitude*costFactor));
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ private readonly static Color GizmosColor = new Color(206.0f/255.0f, 136.0f/255.0f, 48.0f/255.0f, 0.5f);
+ private readonly static Color GizmosColorSelected = new Color(235.0f/255.0f, 123.0f/255.0f, 32.0f/255.0f, 1.0f);
+
+ public override void DrawGizmos () {
+ bool selected = GizmoContext.InActiveSelection(this);
+ Color color = selected ? GizmosColorSelected : GizmosColor;
+
+ if (StartTransform != null) {
+ Draw.xz.Circle(StartTransform.position, 0.4f, color);
+ }
+ if (EndTransform != null) {
+ Draw.xz.Circle(EndTransform.position, 0.4f, color);
+ }
+
+ if (StartTransform != null && EndTransform != null) {
+ NodeLink.DrawArch(StartTransform.position, EndTransform.position, Vector3.up, color);
+ if (selected) {
+ Vector3 cross = Vector3.Cross(Vector3.up, (EndTransform.position-StartTransform.position)).normalized;
+ NodeLink.DrawArch(StartTransform.position+cross*0.1f, EndTransform.position+cross*0.1f, Vector3.up, color);
+ NodeLink.DrawArch(StartTransform.position-cross*0.1f, EndTransform.position-cross*0.1f, Vector3.up, color);
+ }
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs.meta
new file mode 100644
index 0000000..d8b4f52
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink3.cs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3850d0dc2bead45568e6b5bbcc011606
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs
new file mode 100644
index 0000000..0df08fa
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs
@@ -0,0 +1,634 @@
+using System.Collections.Generic;
+using Unity.Collections;
+using UnityEngine;
+using UnityEngine.Profiling;
+using Pathfinding.Graphs.Navmesh;
+using Pathfinding.Util;
+using System;
+using UnityEngine.Assertions;
+
+namespace Pathfinding {
+ /// <summary>
+ /// Manager for off-mesh links.
+ ///
+ /// This manager tracks all active off-mesh links in the scene and recalculates them when needed.
+ /// If an off-mesh link is activated, a <see cref="LinkGraph"/> will also be added to the graph list to store the special nodes necessary for the links to work.
+ ///
+ /// Whenever a graph update happens, the <see cref="DirtyBounds"/> method should be called with the bounds of the updated area.
+ /// This will cause the links touching that bounding box to be recalculated at the end of the graph update step.
+ ///
+ /// Typically you will not need to interact with this class yourself, instead you can use the pre-built components like <see cref="NodeLink2"/>.
+ /// </summary>
+ public class OffMeshLinks {
+ AABBTree<OffMeshLinkCombined> tree = new AABBTree<OffMeshLinkCombined>();
+ List<OffMeshLinkSource> pendingAdd = new List<OffMeshLinkSource>();
+ bool updateScheduled;
+ AstarPath astar;
+
+ public OffMeshLinks(AstarPath astar) {
+ this.astar = astar;
+ }
+
+ /// <summary>
+ /// The start or end point of an off-mesh link.
+ ///
+ /// See: <see cref="OffMeshLinkSource"/>
+ /// </summary>
+ public struct Anchor {
+ /// <summary>Where the link connects to the navmesh</summary>
+ public Vector3 center;
+ /// <summary>Rotation that the character should align itself with when traversing the link</summary>
+ public Quaternion rotation;
+ /// <summary>
+ /// Width of the link.
+ ///
+ /// Note: No values other than 0 are currently supported.
+ /// </summary>
+ public float width;
+
+ /// <summary>First point on the segment that makes up this anchor</summary>
+ public readonly Vector3 point1 => center + rotation * new Vector3(-0.5f * width, 0, 0);
+
+ /// <summary>Second point on the segment that makes up this anchor</summary>
+ public readonly Vector3 point2 => center + rotation * new Vector3(0.5f * width, 0, 0);
+
+ public static bool operator ==(Anchor a, Anchor b) => a.center == b.center && a.rotation == b.rotation && a.width == b.width;
+ public static bool operator !=(Anchor a, Anchor b) => a.center != b.center || a.rotation != b.rotation || a.width != b.width;
+
+ public override bool Equals(object obj) => obj is Anchor && this == (Anchor)obj;
+ public override int GetHashCode() => (center.GetHashCode() * 23 ^ rotation.GetHashCode()) * 23 ^ width.GetHashCode();
+ }
+
+ /// <summary>Determines how a link is connected in the graph</summary>
+ public enum Directionality {
+ /// <summary>Movement is only allowed from the start point to the end point</summary>
+ OneWay,
+ /// <summary>Movement is allowed in both directions</summary>
+ TwoWay,
+ }
+
+ [System.Flags]
+ public enum OffMeshLinkStatus {
+ Inactive = 1 << 0,
+ Pending = 1 << 1,
+ Active = 1 << 2,
+ FailedToConnectStart = Inactive | 1 << 3,
+ FailedToConnectEnd = Inactive | 1 << 4,
+ PendingRemoval = 1 << 5,
+ }
+
+ /// <summary>
+ /// Information about an off-mesh link.
+ ///
+ /// Off-mesh links connect two points on the navmesh which are not necessarily connected by a normal navmesh connection.
+ ///
+ /// See: <see cref="NodeLink2"/>
+ /// See: <see cref="OffMeshLinks"/>
+ /// </summary>
+ public readonly struct OffMeshLinkTracer {
+ public OffMeshLinkTracer(OffMeshLinkConcrete link, bool reversed) {
+ this.link = link;
+ this.relativeStart = reversed ? link.end.center : link.start.center;
+ this.relativeEnd = reversed ? link.start.center : link.end.center;
+ this.isReverse = reversed;
+ }
+
+
+ public OffMeshLinkTracer(OffMeshLinkConcrete link, Vector3 relativeStart, Vector3 relativeEnd, bool isReverse) {
+ this.link = link;
+ this.relativeStart = relativeStart;
+ this.relativeEnd = relativeEnd;
+ this.isReverse = isReverse;
+ }
+
+ /// <summary>
+ /// The off-mesh link that the agent is traversing.
+ ///
+ /// Note: If the off-mesh link is destroyed while the agent is traversing it, properties like <see cref="OffMeshLinkConcrete.gameObject"/>, may refer to a destroyed gameObject.
+ /// </summary>
+ public readonly OffMeshLinkConcrete link;
+
+ /// <summary>
+ /// The start point of the off-mesh link from the agent's perspective.
+ ///
+ /// This is the point where the agent starts traversing the off-mesh link, regardless of if the link is traversed from the start to end or from end to start.
+ /// </summary>
+ public readonly Vector3 relativeStart;
+
+ /// <summary>
+ /// The end point of the off-mesh link from the agent's perspective.
+ ///
+ /// This is the point where the agent will finish traversing the off-mesh link, regardless of if the link is traversed from start to end or from end to start.
+ /// </summary>
+ public readonly Vector3 relativeEnd;
+
+ /// <summary>
+ /// True if the agent is traversing the off-mesh link from original link's end to its start point.
+ ///
+ /// Note: The <see cref="relativeStart"/> and <see cref="relativeEnd"/> fields are always set from the agent's perspective. So the agent always moves from <see cref="relativeStart"/> to <see cref="relativeEnd"/>.
+ /// </summary>
+ public readonly bool isReverse;
+
+ /// <summary>\copydocref{OffMeshLinkSource.component}</summary>
+ public Component component => link.component;
+ /// <summary>\copydocref{OffMeshLinkSource.gameObject}</summary>
+ public GameObject gameObject => link.gameObject;
+ }
+
+ public class OffMeshLinkSource {
+ /// <summary>The start of the link</summary>
+ public Anchor start;
+
+ /// <summary>The end of the link</summary>
+ public Anchor end;
+ public Directionality directionality;
+
+ /// <summary>
+ /// Tag to apply to this link.
+ ///
+ /// See: tags (view in online documentation for working links)
+ /// </summary>
+ public PathfindingTag tag;
+
+ /// <summary>Multiplies the cost of traversing this link by this amount</summary>
+ public float costFactor; // TODO: Add constant cost?
+
+ /// <summary>
+ /// Maximum distance from the start/end points to the navmesh.
+ ///
+ /// If the distance is greater than this, the link will not be connected to the navmesh.
+ /// </summary>
+ public float maxSnappingDistance;
+
+ /// <summary>
+ /// Graph mask for which graphs the link is allowed to connect to.
+ ///
+ /// The link's endpoints will be connected to the closest valid node on any graph that matches the mask.
+ /// </summary>
+ public GraphMask graphMask;
+
+ public IOffMeshLinkHandler handler;
+
+ /// <summary>
+ /// The Component associated with this link.
+ ///
+ /// Typically this will be a <see cref="NodeLink2"/> component. But users can also create their own components and fill out this field as appropriate.
+ ///
+ /// This field is not used for anything by the pathfinding system itself, it is only used to make it easier for users to find the component associated with a link.
+ ///
+ /// Warning: If the link has been destroyed, this may return a destroyed component.
+ /// A link may be destroyed even while a character is traversing it.
+ /// </summary>
+ public Component component;
+
+ /// <summary>
+ /// The GameObject associated with this link.
+ ///
+ /// This field is not used for anything by the pathfinding system itself, it is only used to make it easier for users to find the GameObject associated with a link.
+ ///
+ /// Warning: If the link has been destroyed, this may return a destroyed GameObject.
+ /// A link may be destroyed even while a character is traversing it.
+ /// </summary>
+ public GameObject gameObject => component != null ? component.gameObject : null;
+
+ internal AABBTree<OffMeshLinkCombined>.Key treeKey;
+
+ public OffMeshLinkStatus status { get; internal set; } = OffMeshLinkStatus.Inactive;
+
+ /// <summary>
+ /// Bounding box which encapsulates the link and any position on the navmesh it could possibly be connected to.
+ ///
+ /// This is used to determine which links need to be recalculated when a graph update happens.
+ /// </summary>
+ public Bounds bounds {
+ get {
+ var b = new Bounds();
+ b.SetMinMax(start.point1, start.point2);
+ b.Encapsulate(end.point1);
+ b.Encapsulate(end.point2);
+ b.Expand(maxSnappingDistance*2);
+ return b;
+ }
+ }
+ }
+
+ internal class OffMeshLinkCombined {
+ public OffMeshLinkSource source;
+ public OffMeshLinkConcrete concrete;
+ }
+
+ public class OffMeshLinkConcrete {
+ /// <summary>\copydocref{OffMeshLinkSource.start}</summary>
+ public Anchor start;
+ /// <summary>\copydocref{OffMeshLinkSource.end}</summary>
+ public Anchor end;
+ public GraphNode[] startNodes;
+ public GraphNode[] endNodes;
+ public LinkNode startLinkNode;
+ public LinkNode endLinkNode;
+ /// <summary>\copydocref{OffMeshLinkSource.directionality}</summary>
+ public Directionality directionality;
+ /// <summary>\copydocref{OffMeshLinkSource.tag}</summary>
+ public PathfindingTag tag;
+ public float costFactor;
+ internal bool staleConnections;
+ internal OffMeshLinkSource source;
+
+ /// <summary>\copydocref{OffMeshLinkSource.handler}</summary>
+ public IOffMeshLinkHandler handler => source.handler;
+
+ /// <summary>\copydocref{OffMeshLinkSource.component}</summary>
+ public Component component => source.component;
+
+ /// <summary>\copydocref{OffMeshLinkSource.gameObject}</summary>
+ public GameObject gameObject => source.component != null ? source.component.gameObject : null;
+
+ // public Bounds bounds {
+ // get {
+ // var b = new Bounds();
+ // b.SetMinMax(start.point1, start.point2);
+ // b.Encapsulate(end.point1);
+ // b.Encapsulate(end.point2);
+ // return b;
+ // }
+ // }
+
+ public bool Equivalent (OffMeshLinkConcrete other) {
+ if (start != other.start) return false;
+ if (end != other.end) return false;
+ if (startNodes.Length != other.startNodes.Length || endNodes.Length != other.endNodes.Length) return false;
+ if (directionality != other.directionality || tag != other.tag || costFactor != other.costFactor) return false;
+
+ for (int i = 0; i < startNodes.Length; i++) {
+ if (startNodes[i] != other.startNodes[i]) return false;
+ }
+ for (int i = 0; i < endNodes.Length; i++) {
+ if (endNodes[i] != other.endNodes[i]) return false;
+ }
+ return true;
+ }
+
+ public void Disconnect () {
+ if (startLinkNode == null) {
+ Assert.IsNull(endLinkNode);
+ } else if (startLinkNode.Destroyed) {
+ Assert.IsTrue(endLinkNode.Destroyed);
+ } else {
+ Assert.IsFalse(endLinkNode.Destroyed);
+ var linkGraph = startLinkNode.Graph as LinkGraph;
+ linkGraph.RemoveNode(startLinkNode);
+ linkGraph.RemoveNode(endLinkNode);
+ }
+ startLinkNode = null;
+ endLinkNode = null;
+ }
+
+ public void Connect (LinkGraph linkGraph, OffMeshLinkSource source) {
+ Assert.IsNull(startLinkNode);
+ Assert.IsNull(endLinkNode);
+ startLinkNode = linkGraph.AddNode();
+ startLinkNode.linkSource = source;
+ startLinkNode.linkConcrete = this;
+ startLinkNode.position = (Int3)start.center;
+ startLinkNode.Tag = tag;
+
+ endLinkNode = linkGraph.AddNode();
+ endLinkNode.position = (Int3)end.center;
+ endLinkNode.linkSource = source;
+ endLinkNode.linkConcrete = this;
+ endLinkNode.Tag = tag;
+
+ for (int i = 0; i < startNodes.Length; i++) {
+ var dist = (VectorMath.ClosestPointOnSegment(start.point1, start.point2, (Vector3)startNodes[i].position) - (Vector3)startNodes[i].position).magnitude;
+ var cost = (uint)(Int3.Precision * dist);
+ GraphNode.Connect(startNodes[i], startLinkNode, cost, directionality);
+ }
+ for (int i = 0; i < endNodes.Length; i++) {
+ var dist = (VectorMath.ClosestPointOnSegment(end.point1, end.point2, (Vector3)endNodes[i].position) - (Vector3)endNodes[i].position).magnitude;
+ var cost = (uint)(Int3.Precision * dist);
+ GraphNode.Connect(endLinkNode, endNodes[i], cost, directionality);
+ }
+ var middleCost = (uint)(Int3.Precision * costFactor * (end.center - start.center).magnitude);
+ GraphNode.Connect(startLinkNode, endLinkNode, middleCost, directionality);
+ staleConnections = false;
+ }
+
+ public OffMeshLinkTracer GetTracer (LinkNode firstNode) {
+ Assert.IsTrue(firstNode == startLinkNode || firstNode == endLinkNode);
+ return new OffMeshLinkTracer(this, firstNode == endLinkNode);
+ }
+ }
+
+ /// <summary>
+ /// Get all graphs that this link is connected to.
+ ///
+ /// Returns: A list of all graphs that this link is connected to. This does not include the link graph.
+ /// An empty list will be returned if the link is not connected to any graphs.
+ ///
+ /// Note: For lower GC pressure, the returned list should be pooled after you are done with it. See: pooling (view in online documentation for working links)
+ /// </summary>
+ /// <param name="link">The link to get connected graphs for.</param>
+ public List<NavGraph> ConnectedGraphs (OffMeshLinkSource link) {
+ var graphs = ListPool<NavGraph>.Claim();
+ if (link.status != OffMeshLinkStatus.Active) return graphs;
+ Assert.IsTrue(link.treeKey.isValid);
+ var combined = tree[link.treeKey];
+ Assert.IsNotNull(combined.concrete);
+ var concrete = combined.concrete;
+ for (int i = 0; i < concrete.startNodes.Length; i++) {
+ var graph = concrete.startNodes[i].Graph;
+ if (!graphs.Contains(graph)) graphs.Add(graph);
+ }
+ for (int i = 0; i < concrete.endNodes.Length; i++) {
+ var graph = concrete.endNodes[i].Graph;
+ if (!graphs.Contains(graph)) graphs.Add(graph);
+ }
+ return graphs;
+ }
+
+ /// <summary>
+ /// Adds a new off-mesh link.
+ ///
+ /// If any graphs change in the future, the link will automatically be updated to connect to the updated graphs.
+ ///
+ /// Note: The link will not be added immediately, it will be added at the end of the current graph update step.
+ /// Or, if no graph update is currently running, a graph update will be scheduled, and the link will be added at the end of that update.
+ /// This is to avoid modifying the graph during a graph update.
+ ///
+ /// See: <see cref="Remove"/>
+ /// </summary>
+ /// <param name="link">The link to add.</param>
+ public void Add (OffMeshLinkSource link) {
+ if (link == null) throw new ArgumentNullException("link");
+ if (link.status != OffMeshLinkStatus.Inactive) throw new System.ArgumentException("Link is already added");
+ pendingAdd.Add(link);
+ link.status = OffMeshLinkStatus.Pending;
+ ScheduleUpdate();
+ }
+
+ internal void OnDisable () {
+ var ls = new List<OffMeshLinkCombined>();
+ tree.Query(new Bounds(Vector3.zero, Vector3.positiveInfinity), ls);
+ for (int i = 0; i < ls.Count; i++) {
+ ls[i].source.status = OffMeshLinkStatus.Inactive;
+ ls[i].source.treeKey = default;
+ }
+ tree.Clear();
+ for (int i = 0; i < pendingAdd.Count; i++) {
+ pendingAdd[i].status = OffMeshLinkStatus.Inactive;
+ pendingAdd[i].treeKey = default;
+ }
+ pendingAdd.Clear();
+ }
+
+ /// <summary>
+ /// Removes an existing off-mesh link.
+ ///
+ /// Note: The link will not be removed immediately, it will be removed at the end of the current graph update step.
+ /// Or, if no graph update is currently running, a graph update will be scheduled, and the link will be removed at the end of that update.
+ /// This is to avoid modifying the graph during a graph update.
+ ///
+ /// See: <see cref="Add"/>
+ /// </summary>
+ /// <param name="link">The link to remove. If the link is already removed, nothing will be done.</param>
+ public void Remove (OffMeshLinkSource link) {
+ if (link == null) throw new ArgumentNullException("link");
+ if (link.status == OffMeshLinkStatus.Inactive || (link.status & OffMeshLinkStatus.PendingRemoval) != 0) {
+ return;
+ } else if (link.status == OffMeshLinkStatus.Pending) {
+ link.status = OffMeshLinkStatus.Inactive;
+ pendingAdd.Remove(link);
+ } else {
+ link.status |= OffMeshLinkStatus.Pending | OffMeshLinkStatus.PendingRemoval;
+ tree.Tag(link.treeKey);
+ }
+
+ Assert.IsTrue(link.status == OffMeshLinkStatus.Inactive || (link.status & OffMeshLinkStatus.PendingRemoval) != 0);
+ ScheduleUpdate();
+ }
+
+ NNConstraint cachedNNConstraint = NNConstraint.Walkable;
+
+ bool ClampSegment (Anchor anchor, GraphMask graphMask, float maxSnappingDistance, out Anchor result, List<GraphNode> nodes) {
+ var nn = cachedNNConstraint;
+ nn.distanceMetric = DistanceMetric.Euclidean;
+ nn.graphMask = graphMask;
+ Profiler.BeginSample("GetNearest");
+ var nearest = astar.GetNearest(0.5f*(anchor.point1 + anchor.point2), nn);
+ Profiler.EndSample();
+
+ if (nearest.distanceCostSqr > maxSnappingDistance*maxSnappingDistance) nearest = default;
+
+ if (nearest.node == null) {
+ result = default;
+ return false;
+ }
+
+ if (anchor.width > 0 && nearest.node.Graph is IRaycastableGraph rayGraph) {
+ var offset = 0.5f * (anchor.point2 - anchor.point1);
+ rayGraph.Linecast(nearest.position, nearest.position - offset, nearest.node, out var hit1, nodes);
+ rayGraph.Linecast(nearest.position, nearest.position + offset, nearest.node, out var hit2, nodes);
+ result = new Anchor {
+ center = (hit1.point + hit2.point) * 0.5f,
+ rotation = anchor.rotation,
+ width = (hit1.point - hit2.point).magnitude,
+ };
+
+ // Sort and deduplicate
+ nodes.Sort((a, b) => a.NodeIndex.CompareTo(b.NodeIndex));
+ for (int j = nodes.Count - 1; j >= 0; j--) {
+ var n = nodes[j];
+ for (int k = j - 1; k >= 0; k--) {
+ if (nodes[k] == n) {
+ nodes.RemoveAtSwapBack(j);
+ break;
+ }
+ }
+ }
+ } else {
+ result = new Anchor {
+ center = nearest.position,
+ rotation = anchor.rotation,
+ width = 0f,
+ };
+ nodes.Add(nearest.node);
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Mark links touching the given bounds as dirty.
+ ///
+ /// The bounds should contain the surface of all nodes that have been changed.
+ ///
+ /// This will cause the links to be recalculated as soon as possible.
+ ///
+ /// Note: Since graphs should only be modified during graph updates, this method should also only be called during a graph update.
+ /// </summary>
+ public void DirtyBounds (Bounds bounds) {
+ Profiler.BeginSample("DirtyBounds");
+ tree.Tag(bounds);
+ Profiler.EndSample();
+ // Note: We don't have to call ScheduleUpdate here, because DirtyBounds will only be called during a work item/graph update
+ }
+
+ /// <summary>
+ /// Mark a link as dirty.
+ ///
+ /// This will cause the link to be recalculated as soon as possible.
+ /// </summary>
+ public void Dirty (OffMeshLinkSource link) {
+ DirtyNoSchedule(link);
+ ScheduleUpdate();
+ }
+
+ internal void DirtyNoSchedule (OffMeshLinkSource link) {
+ tree.Tag(link.treeKey);
+ }
+
+ void ScheduleUpdate () {
+ if (!updateScheduled && !astar.isScanning && !astar.IsAnyWorkItemInProgress) {
+ updateScheduled = true;
+ astar.AddWorkItem(() => {});
+ }
+ }
+
+ /// <summary>
+ /// Get the nearest link to a point.
+ ///
+ /// Returns: The nearest link to the point or a default <see cref="OffMeshLinkTracer"/> if no link was found.
+ /// The returned struct contains both the link and information about which side of the link is closest to the point.
+ /// If the end is closer than the start, then a reversed <see cref="OffMeshLinkTracer"/> will be returned.
+ /// </summary>
+ /// <param name="point">Point to search around.</param>
+ /// <param name="maxDistance">Maximum distance to search. Use a small distance for better performance.</param>
+ public OffMeshLinkTracer GetNearest (Vector3 point, float maxDistance) {
+ if (maxDistance < 0) return default;
+ if (!float.IsFinite(maxDistance)) throw new System.ArgumentOutOfRangeException("maxDistance");
+
+ var ls = ListPool<OffMeshLinkCombined>.Claim();
+ tree.Query(new Bounds(point, new Vector3(2*maxDistance, 2*maxDistance, 2*maxDistance)), ls);
+ OffMeshLinkConcrete nearest = null;
+ bool reversed = false;
+ float nearestDist = maxDistance*maxDistance;
+ for (int i = 0; i < ls.Count; i++) {
+ var link = ls[i].concrete;
+ var dist = VectorMath.SqrDistancePointSegment(link.start.point1, link.start.point2, point);
+ if (dist < nearestDist) {
+ nearestDist = dist;
+ nearest = link;
+ reversed = false;
+ }
+ dist = VectorMath.SqrDistancePointSegment(link.end.point1, link.end.point2, point);
+ if (dist < nearestDist) {
+ nearestDist = dist;
+ nearest = link;
+ reversed = true;
+ }
+ }
+ ListPool<OffMeshLinkCombined>.Release(ref ls);
+ return nearest != null ? new OffMeshLinkTracer(nearest, reversed) : default;
+ }
+
+ internal void Refresh () {
+ Profiler.BeginSample("Refresh Off-mesh links");
+ updateScheduled = false;
+
+ var pendingUpdate = ListPool<OffMeshLinkCombined>.Claim();
+ // Find all links that require updates
+ // These have previously been tagged using the DirtyBounds method
+ tree.QueryTagged(pendingUpdate, true);
+
+ // Add all links to the tree which are pending insertion
+ for (int i = 0; i < pendingAdd.Count; i++) {
+ var link = pendingAdd[i];
+ Assert.IsTrue(link.status == OffMeshLinkStatus.Pending);
+ var combined = new OffMeshLinkCombined {
+ source = link,
+ concrete = null,
+ };
+ link.treeKey = tree.Add(link.bounds, combined);
+ pendingUpdate.Add(combined);
+ }
+ pendingAdd.Clear();
+
+ List<GraphNode> startNodes = ListPool<GraphNode>.Claim();
+ List<GraphNode> endNodes = ListPool<GraphNode>.Claim();
+
+ for (int i = 0; i < pendingUpdate.Count; i++) {
+ for (int j = 0; j < i; j++) {
+ if (pendingUpdate[i].source == pendingUpdate[j].source) throw new System.Exception("Duplicate link");
+ }
+ var source = pendingUpdate[i].source;
+
+ var combined = tree[source.treeKey];
+ var prevConcrete = combined.concrete;
+
+ if ((source.status & OffMeshLinkStatus.PendingRemoval) != 0) {
+ if (prevConcrete != null) {
+ prevConcrete.Disconnect();
+ combined.concrete = null;
+ }
+ tree.Remove(source.treeKey);
+ source.treeKey = default;
+ source.status = OffMeshLinkStatus.Inactive;
+ continue;
+ }
+
+ startNodes.Clear();
+ if (!ClampSegment(source.start, source.graphMask, source.maxSnappingDistance, out var concreteStart, startNodes)) {
+ if (prevConcrete != null) {
+ prevConcrete.Disconnect();
+ combined.concrete = null;
+ }
+ source.status = OffMeshLinkStatus.FailedToConnectStart;
+ continue;
+ }
+ endNodes.Clear();
+ if (!ClampSegment(source.end, source.graphMask, source.maxSnappingDistance, out var concreteEnd, endNodes)) {
+ if (prevConcrete != null) {
+ prevConcrete.Disconnect();
+ combined.concrete = null;
+ }
+ source.status = OffMeshLinkStatus.FailedToConnectEnd;
+ continue;
+ }
+
+ var concrete = new OffMeshLinkConcrete {
+ start = concreteStart,
+ end = concreteEnd,
+ startNodes = startNodes.ToArrayFromPool(),
+ endNodes = endNodes.ToArrayFromPool(),
+ source = source,
+ directionality = source.directionality,
+ tag = source.tag,
+ costFactor = source.costFactor,
+ };
+
+ if (prevConcrete != null && !prevConcrete.staleConnections && prevConcrete.Equivalent(concrete)) {
+ // Nothing to do. The link is already connected like it should be.
+ source.status &= ~OffMeshLinkStatus.Pending;
+ Assert.AreNotEqual(OffMeshLinkStatus.Inactive, source.status);
+ } else {
+ // Remove previous connections
+ if (prevConcrete != null) {
+ prevConcrete.Disconnect();
+ ArrayPool<GraphNode>.Release(ref prevConcrete.startNodes);
+ ArrayPool<GraphNode>.Release(ref prevConcrete.endNodes);
+ }
+
+ // Add new connections
+ if (astar.data.linkGraph == null) astar.data.AddGraph<LinkGraph>();
+ concrete.Connect(astar.data.linkGraph, source);
+ combined.concrete = concrete;
+ source.status = OffMeshLinkStatus.Active;
+ }
+ }
+
+ ListPool<OffMeshLinkCombined>.Release(ref pendingUpdate);
+ ListPool<GraphNode>.Release(ref startNodes);
+ ListPool<GraphNode>.Release(ref endNodes);
+ Profiler.EndSample();
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs.meta
new file mode 100644
index 0000000..96fffdd
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/OffMeshLinks.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6ad699a60828e7e4e98c493913084bb4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs
new file mode 100644
index 0000000..88fb499
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs
@@ -0,0 +1,619 @@
+using UnityEngine;
+using System.Collections.Generic;
+using Unity.Mathematics;
+
+namespace Pathfinding.Util {
+ /// <summary>Interpolates along a sequence of points</summary>
+ public class PathInterpolator {
+ /// <summary>
+ /// Represents a single point on the polyline represented by the <see cref="PathInterpolator"/>.
+ /// The cursor is a lightweight structure which can be used to move backwards and forwards along a <see cref="PathInterpolator"/>.
+ ///
+ /// If the <see cref="PathInterpolator"/> changes (e.g. has its path swapped out), then this cursor is invalidated and cannot be used anymore.
+ /// </summary>
+ public struct Cursor {
+ private PathInterpolator interpolator;
+ private int version;
+ private float currentDistance;
+ private float distanceToSegmentStart;
+ private float currentSegmentLength;
+
+ /// <summary>
+ /// Current segment.
+ /// The start and end points of the segment are path[value] and path[value+1].
+ /// </summary>
+ int segmentIndex { get; set; }
+
+ public int segmentCount {
+ get {
+ AssertValid();
+ return interpolator.path.Count - 1;
+ }
+ }
+
+ /// <summary>Last point in the path</summary>
+ public Vector3 endPoint {
+ get {
+ AssertValid();
+ return interpolator.path[interpolator.path.Count-1];
+ }
+ }
+
+ /// <summary>
+ /// Fraction of the way along the current segment.
+ /// 0 is at the start of the segment, 1 is at the end of the segment.
+ /// </summary>
+ public float fractionAlongCurrentSegment {
+ get {
+ return currentSegmentLength > 0 ? (currentDistance - distanceToSegmentStart) / currentSegmentLength : 1f;
+ }
+ set {
+ currentDistance = distanceToSegmentStart + Mathf.Clamp01(value) * currentSegmentLength;
+ }
+ }
+
+ /// <summary>A cursor at the start of the polyline represented by the interpolator</summary>
+ public static Cursor StartOfPath (PathInterpolator interpolator) {
+ if (!interpolator.valid) throw new System.InvalidOperationException("PathInterpolator has no path set");
+ return new Cursor {
+ interpolator = interpolator,
+ version = interpolator.version,
+ segmentIndex = 0,
+ currentDistance = 0,
+ distanceToSegmentStart = 0,
+ currentSegmentLength = (interpolator.path[1] - interpolator.path[0]).magnitude,
+ };
+ }
+
+ /// <summary>
+ /// True if this instance has a path set.
+ /// See: SetPath
+ /// </summary>
+ public bool valid {
+ get {
+ return interpolator != null && interpolator.version == version;
+ }
+ }
+
+ /// <summary>
+ /// Tangent of the curve at the current position.
+ /// Not necessarily normalized.
+ /// </summary>
+ public Vector3 tangent {
+ get {
+ AssertValid();
+ return interpolator.path[segmentIndex+1] - interpolator.path[segmentIndex];
+ }
+ }
+
+ /// <summary>Remaining distance until the end of the path</summary>
+ public float remainingDistance {
+ get {
+ AssertValid();
+ return interpolator.totalDistance - distance;
+ }
+ set {
+ AssertValid();
+ distance = interpolator.totalDistance - value;
+ }
+ }
+
+ /// <summary>Traversed distance from the start of the path</summary>
+ public float distance {
+ get {
+ return currentDistance;
+ }
+ set {
+ AssertValid();
+ currentDistance = value;
+
+ while (currentDistance < distanceToSegmentStart && segmentIndex > 0) PrevSegment();
+ while (currentDistance > distanceToSegmentStart + currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment();
+ }
+ }
+
+ /// <summary>Current position</summary>
+ public Vector3 position {
+ get {
+ AssertValid();
+ float t = currentSegmentLength > 0.0001f ? (currentDistance - distanceToSegmentStart) / currentSegmentLength : 0f;
+ return Vector3.Lerp(interpolator.path[segmentIndex], interpolator.path[segmentIndex+1], t);
+ }
+ }
+
+ /// <summary>Appends the remaining path between <see cref="position"/> and <see cref="endPoint"/> to buffer</summary>
+ public void GetRemainingPath (List<Vector3> buffer) {
+ AssertValid();
+ buffer.Add(position);
+ for (int i = segmentIndex+1; i < interpolator.path.Count; i++) {
+ buffer.Add(interpolator.path[i]);
+ }
+ }
+
+ void AssertValid () {
+ if (!this.valid) throw new System.InvalidOperationException("The cursor has been invalidated because SetPath has been called on the interpolator. Please create a new cursor.");
+ }
+
+ /// <summary>
+ /// The tangent(s) of the curve at the current position.
+ /// Not necessarily normalized.
+ ///
+ /// Will output t1=<see cref="tangent"/>, t2=<see cref="tangent"/> if on a straight line segment.
+ /// Will output the previous and next tangents for the adjacent line segments when on a corner.
+ ///
+ /// This is similar to <see cref="tangent"/> but can output two tangents instead of one when on a corner.
+ /// </summary>
+ public void GetTangents (out Vector3 t1, out Vector3 t2) {
+ AssertValid();
+ var nearStart = currentDistance <= distanceToSegmentStart + 0.001f;
+ var nearEnd = currentDistance >= distanceToSegmentStart + currentSegmentLength - 0.001f;
+ if (nearStart || nearEnd) {
+ int s1, s2;
+ if (nearStart) {
+ s1 = segmentIndex > 0 ? segmentIndex - 1 : segmentIndex;
+ s2 = segmentIndex;
+ } else {
+ s1 = segmentIndex;
+ s2 = segmentIndex < interpolator.path.Count - 2 ? segmentIndex + 1 : segmentIndex;
+ }
+ t1 = interpolator.path[s1+1] - interpolator.path[s1];
+ t2 = interpolator.path[s2+1] - interpolator.path[s2];
+ } else {
+ t1 = tangent;
+ t2 = t1;
+ }
+ }
+
+ /// <summary>
+ /// A vector parallel to the local curvature.
+ ///
+ /// This will be zero on straight line segments, and in the same direction as the rotation axis when on a corner.
+ ///
+ /// Since this interpolator follows a polyline, the curvature is always either 0 or infinite.
+ /// Therefore the magnitude of this vector has no meaning when non-zero. Only the direction matters.
+ /// </summary>
+ public Vector3 curvatureDirection {
+ get {
+ GetTangents(out var t1, out var t2);
+ var up = Vector3.Cross(t1, t2);
+ return up.sqrMagnitude <= 0.000001f ? Vector3.zero : up;
+ }
+ }
+
+ /// <summary>
+ /// Moves the cursor to the next geometric corner in the path.
+ ///
+ /// This is the next geometric corner.
+ /// If the original path contained any zero-length segments, they will be skipped over.
+ /// </summary>
+ public void MoveToNextCorner () {
+ AssertValid();
+ var path = interpolator.path;
+ // Skip zero-length segments
+ while (currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < path.Count - 2) NextSegment();
+ // Skip parallel segements
+ while (segmentIndex < path.Count - 2 && VectorMath.IsColinear(path[segmentIndex], path[segmentIndex+1], path[segmentIndex+2])) NextSegment();
+ // Move to end of current segment
+ currentDistance = distanceToSegmentStart + currentSegmentLength;
+ }
+
+ /// <summary>
+ /// Moves to the closest intersection of the line segment (origin + direction*range.x, origin + direction*range.y).
+ /// The closest intersection as measured by the distance along the path is returned.
+ ///
+ /// If no intersection is found, false will be returned and the cursor remains unchanged.
+ ///
+ /// The intersection is calculated in XZ space.
+ /// </summary>
+ /// <param name="origin">A point on the line</param>
+ /// <param name="direction">The direction of the line. Need not be normalized.</param>
+ /// <param name="range">The range of the line segment along the line. The segment is (origin + direction*range.x, origin + direction*range.y). May be (-inf, +inf) to consider an infinite line.</param>
+ public bool MoveToClosestIntersectionWithLineSegment (Vector3 origin, Vector3 direction, Vector2 range) {
+ AssertValid();
+ var closestIntersection = float.PositiveInfinity;
+ var closestDist = float.PositiveInfinity;
+ var d = 0f;
+ for (int i = 0; i < interpolator.path.Count - 1; i++) {
+ var p1 = interpolator.path[i];
+ var p2 = interpolator.path[i+1];
+ var segmentLength = (p2 - p1).magnitude;
+ if (
+ VectorMath.LineLineIntersectionFactors(((float3)p1).xz, ((float3)(p2 - p1)).xz, ((float3)origin).xz, ((float3)direction).xz, out float t1, out float t2)
+ && t1 >= 0.0f && t1 <= 1.0f
+ && t2 >= range.x && t2 <= range.y
+ ) {
+ var intersection = d + t1 * segmentLength;
+ var dist = Mathf.Abs(intersection - this.currentDistance);
+ if (dist < closestDist) {
+ closestIntersection = intersection;
+ closestDist = dist;
+ }
+ }
+ d += segmentLength;
+ }
+ if (closestDist != float.PositiveInfinity) {
+ this.distance = closestIntersection;
+ return true;
+ }
+ return false;
+ }
+
+ /// <summary>Move to the specified segment and move a fraction of the way to the next segment</summary>
+ void MoveToSegment (int index, float fractionAlongSegment) {
+ AssertValid();
+ if (index < 0 || index >= interpolator.path.Count - 1) throw new System.ArgumentOutOfRangeException("index");
+ while (segmentIndex > index) PrevSegment();
+ while (segmentIndex < index) NextSegment();
+ currentDistance = distanceToSegmentStart + Mathf.Clamp01(fractionAlongSegment) * currentSegmentLength;
+ }
+
+ /// <summary>Move as close as possible to the specified point</summary>
+ public void MoveToClosestPoint (Vector3 point) {
+ AssertValid();
+
+ float bestDist = float.PositiveInfinity;
+ float bestFactor = 0f;
+ int bestIndex = 0;
+
+ var path = interpolator.path;
+
+ for (int i = 0; i < path.Count-1; i++) {
+ float factor = VectorMath.ClosestPointOnLineFactor(path[i], path[i+1], point);
+ Vector3 closest = Vector3.Lerp(path[i], path[i+1], factor);
+ float dist = (point - closest).sqrMagnitude;
+
+ if (dist < bestDist) {
+ bestDist = dist;
+ bestFactor = factor;
+ bestIndex = i;
+ }
+ }
+
+ MoveToSegment(bestIndex, bestFactor);
+ }
+
+ public void MoveToLocallyClosestPoint (Vector3 point, bool allowForwards = true, bool allowBackwards = true) {
+ AssertValid();
+
+ var path = interpolator.path;
+
+ segmentIndex = Mathf.Min(segmentIndex, path.Count - 2);
+ while (true) {
+ int currentSegment = segmentIndex;
+ var factor = VectorMath.ClosestPointOnLineFactor(path[currentSegment], path[currentSegment+1], point);
+ if (factor > 1.0f && allowForwards && segmentIndex < path.Count - 2) {
+ NextSegment();
+ allowBackwards = false;
+ } else if (factor < 0.0f && allowBackwards && segmentIndex > 0) {
+ PrevSegment();
+ allowForwards = false;
+ } else {
+ if (factor > 0.5f && segmentIndex < path.Count - 2) {
+ NextSegment();
+ }
+ break;
+ }
+ }
+
+ // Check the distances to the two segments extending from the vertex path[segmentIndex]
+ // and pick the position on those segments that is closest to the #point parameter.
+ float factor1 = 0, d1 = float.PositiveInfinity;
+
+ if (segmentIndex > 0) {
+ var s1 = segmentIndex - 1;
+ factor1 = VectorMath.ClosestPointOnLineFactor(path[s1], path[s1+1], point);
+ d1 = (Vector3.Lerp(path[s1], path[s1+1], factor1) - point).sqrMagnitude;
+ }
+
+ var factor2 = VectorMath.ClosestPointOnLineFactor(path[segmentIndex], path[segmentIndex+1], point);
+ var d2 = (Vector3.Lerp(path[segmentIndex], path[segmentIndex+1], factor2) - point).sqrMagnitude;
+
+ if (d1 < d2) MoveToSegment(segmentIndex - 1, factor1);
+ else MoveToSegment(segmentIndex, factor2);
+ }
+
+ public void MoveToCircleIntersection2D<T>(Vector3 circleCenter3D, float radius, T transform) where T : IMovementPlane {
+ AssertValid();
+
+ var path = interpolator.path;
+
+ // Move forwards as long as we are getting closer to circleCenter3D
+ while (segmentIndex < path.Count - 2 && VectorMath.ClosestPointOnLineFactor(path[segmentIndex], path[segmentIndex+1], circleCenter3D) > 1) {
+ NextSegment();
+ }
+
+ var circleCenter = transform.ToPlane(circleCenter3D);
+
+ // Move forwards as long as the current segment endpoint is within the circle
+ while (segmentIndex < path.Count - 2 && (transform.ToPlane(path[segmentIndex+1]) - circleCenter).sqrMagnitude <= radius*radius) {
+ NextSegment();
+ }
+
+ // Calculate the intersection with the circle. This involves some math.
+ var factor = VectorMath.LineCircleIntersectionFactor(circleCenter, transform.ToPlane(path[segmentIndex]), transform.ToPlane(path[segmentIndex+1]), radius);
+
+ // Move to the intersection point
+ MoveToSegment(segmentIndex, factor);
+ }
+
+ /// <summary>
+ /// Integrates exp(-|x|/smoothingDistance)/(2*smoothingDistance) from a to b.
+ /// The integral from -inf to +inf is 1.
+ /// </summary>
+ static float IntegrateSmoothingKernel (float a, float b, float smoothingDistance) {
+ if (smoothingDistance <= 0) return a <= 0 && b > 0 ? 1 : 0;
+ var iA = a < 0 ? Mathf.Exp(a / smoothingDistance) : 2.0f - Mathf.Exp(-a / smoothingDistance);
+ var iB = b < 0 ? Mathf.Exp(b / smoothingDistance) : 2.0f - Mathf.Exp(-b / smoothingDistance);
+ return 0.5f * (iB - iA);
+ }
+
+ /// <summary>Integrates (x - a)*exp(-x/smoothingDistance)/(2*smoothingDistance) from a to b.</summary>
+ static float IntegrateSmoothingKernel2 (float a, float b, float smoothingDistance) {
+ if (smoothingDistance <= 0) return 0f;
+ var iA = -Mathf.Exp(-a / smoothingDistance) * (smoothingDistance);
+ var iB = -Mathf.Exp(-b / smoothingDistance) * (smoothingDistance + b - a);
+ return 0.5f * (iB - iA);
+ }
+
+ static Vector3 IntegrateSmoothTangent (Vector3 p1, Vector3 p2, ref Vector3 tangent, ref float distance, float expectedRadius, float smoothingDistance) {
+ var segment = p2 - p1;
+ var segmentLength = segment.magnitude;
+ if (segmentLength <= 0.00001f) return Vector3.zero;
+ var nextTangent = segment * (1.0f / segmentLength);
+ var deltaAngle = Vector3.Angle(tangent, nextTangent) * Mathf.Deg2Rad;
+ var arcLength = expectedRadius*Mathf.Abs(deltaAngle);
+ // We try to approximate
+ // integrate kernel(x) * tangent(x); where * denotes convolution
+
+ var integratedTangent = Vector3.zero;
+ if (arcLength > float.Epsilon) {
+ // Arc
+ // integrate kernel(x) * (tangent + (nextTangent - tangent) * x/arcLength) dx
+ var convolution = tangent * IntegrateSmoothingKernel(distance, distance + arcLength, smoothingDistance) +
+ (nextTangent - tangent) * IntegrateSmoothingKernel2(distance, distance + arcLength, smoothingDistance) / arcLength;
+ integratedTangent += convolution;
+ distance += arcLength;
+ }
+
+ // Straight line
+ // integrate kernel(x) * nextTangent dx = nextTangent * integrate kernel(x) dx
+ integratedTangent += nextTangent * IntegrateSmoothingKernel(distance, distance + segmentLength, smoothingDistance);
+ tangent = nextTangent;
+ distance += segmentLength;
+ return integratedTangent;
+ }
+
+ public Vector3 EstimateSmoothTangent (Vector3 normalizedTangent, float smoothingDistance, float expectedRadius, Vector3 beforePathStartContribution, bool forward = true, bool backward = true) {
+ AssertValid();
+ if (expectedRadius <= float.Epsilon || smoothingDistance <= 0f) return normalizedTangent;
+
+ var path = interpolator.path;
+ var estimatedTangent = Vector3.zero;
+ // Avoid zero-length segments at the start
+ while (currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment();
+ if (forward) {
+ var d = 0f;
+ var prev = position;
+ var prevTangent = normalizedTangent;
+ for (int i = segmentIndex + 1; i < path.Count; i++) {
+ estimatedTangent += IntegrateSmoothTangent(prev, path[i], ref prevTangent, ref d, expectedRadius, smoothingDistance);
+ prev = path[i];
+ }
+ }
+ if (backward) {
+ var d = 0f;
+ var prevTangent = -normalizedTangent;
+ var prev = position;
+ for (int i = segmentIndex; i >= 0; i--) {
+ estimatedTangent -= IntegrateSmoothTangent(prev, path[i], ref prevTangent, ref d, expectedRadius, smoothingDistance);
+ prev = path[i];
+ }
+ estimatedTangent += beforePathStartContribution * IntegrateSmoothingKernel(float.NegativeInfinity, -currentDistance, smoothingDistance);
+ }
+
+ return estimatedTangent;
+ }
+
+ public Vector3 EstimateSmoothCurvature (Vector3 tangent, float smoothingDistance, float expectedRadius) {
+ AssertValid();
+ if (expectedRadius <= float.Epsilon) return Vector3.zero;
+
+ var path = interpolator.path;
+ tangent = tangent.normalized;
+ var curvature = Vector3.zero;
+ // Avoid zero-length segments at the start
+ while (currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment();
+ var d = 0f;
+ var prev = position;
+ var currentTangent = tangent.normalized;
+ for (int i = segmentIndex + 1; i < path.Count; i++) {
+ var segment = path[i] - prev;
+ var t = segment.normalized;
+ var deltaAngle = Vector3.Angle(currentTangent, t) * Mathf.Deg2Rad;
+ var c = Vector3.Cross(currentTangent, t).normalized;
+ var angleDerivative = 1.0f / expectedRadius;
+ var arcLength = expectedRadius*Mathf.Abs(deltaAngle);
+ // d/dx(f * angle(x)) = f * angle'(x); where * denotes convolution
+ var convolutionDerivative = angleDerivative * IntegrateSmoothingKernel(d, d + arcLength, smoothingDistance);
+ curvature -= convolutionDerivative * c;
+ currentTangent = t;
+ d += arcLength;
+ d += segment.magnitude;
+ prev = path[i];
+ }
+ // Do another integral in the backwards direction.
+ // Ensures that if smoothingDistance is 0, the smoothing kernel will only be sampled once at x=0.
+ d = float.Epsilon;
+ currentTangent = -tangent.normalized;
+ prev = position;
+ for (int i = segmentIndex; i >= 0; i--) {
+ var segment = path[i] - prev;
+ if (segment == Vector3.zero) continue;
+
+ var t = segment.normalized;
+ var deltaAngle = Vector3.Angle(currentTangent, t) * Mathf.Deg2Rad;
+ var c = Vector3.Cross(currentTangent, t).normalized;
+ var angleDerivative = 1.0f / expectedRadius;
+ var arcLength = expectedRadius*Mathf.Abs(deltaAngle);
+ // d/dx(f * angle(x)) = f * angle'(x); where * denotes convolution
+ var convolutionDerivative = angleDerivative * IntegrateSmoothingKernel(d, d + arcLength, smoothingDistance);
+ curvature += convolutionDerivative * c;
+ currentTangent = t;
+ d += arcLength;
+ d += segment.magnitude;
+ prev = path[i];
+ }
+ return curvature;
+ }
+
+ /// <summary>
+ /// Moves the agent along the path, stopping to rotate on the spot when the path changes direction.
+ ///
+ /// Note: The cursor state does not include the rotation of the agent. So if an agent stops in the middle of a rotation, the final state of this struct will be as if the agent completed its rotation.
+ /// If you want to preserve the rotation state as well, keep track of the output tangent, and pass it along to the next call to this function.
+ /// </summary>
+ /// <param name="time">The number of seconds to move forwards or backwards (if negative).</param>
+ /// <param name="speed">Speed in meters/second.</param>
+ /// <param name="turningSpeed">Turning speed in radians/second.</param>
+ /// <param name="tangent">The current forwards direction of the agent. May be set to the #tangent property if you have no other needs.
+ /// If set to something other than #tangent, the agent will start by rotating to face the #tangent direction.
+ /// This will be replaced with the forwards direction of the agent after moving.
+ /// It will be smoothly interpolated as the agent rotates from one segment to the next.
+ /// It is more precise than the #tangent property after this call, which does not take rotation into account.
+ /// This value is not necessarily normalized.</param>
+ public void MoveWithTurningSpeed (float time, float speed, float turningSpeed, ref Vector3 tangent) {
+ if (turningSpeed <= 0) throw new System.ArgumentException("turningSpeed must be greater than zero");
+ if (speed <= 0) throw new System.ArgumentException("speed must be greater than zero");
+ AssertValid();
+ var radiansToMeters = speed / turningSpeed;
+ var remainingOffset = time * speed;
+ int its = 0;
+ // Make sure we don't start by rotating unnecessarily
+ while (remainingOffset > 0 && currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment();
+ while (remainingOffset < 0 && currentDistance <= this.distanceToSegmentStart && segmentIndex > 0) PrevSegment();
+ while (remainingOffset != 0f) {
+ its++;
+ if (its > 100) throw new System.Exception("Infinite Loop " + remainingOffset + " " + time);
+ var desiredTangent = this.tangent;
+ if (tangent != desiredTangent && currentSegmentLength > 0) {
+ // Rotate to face the desired tangent
+ var angle = Vector3.Angle(tangent, desiredTangent) * Mathf.Deg2Rad;
+ var arcLength = angle * radiansToMeters;
+ if (Mathf.Abs(remainingOffset) > arcLength) {
+ remainingOffset -= arcLength * Mathf.Sign(remainingOffset);
+ tangent = desiredTangent;
+ } else {
+ tangent = Vector3.Slerp(tangent, desiredTangent, Mathf.Abs(remainingOffset) / arcLength);
+ return;
+ }
+ }
+
+ if (remainingOffset > 0) {
+ // Move forward along the segment
+ var remainingOnCurrentSegment = this.currentSegmentLength - (this.currentDistance - this.distanceToSegmentStart);
+ if (remainingOffset >= remainingOnCurrentSegment) {
+ remainingOffset -= remainingOnCurrentSegment;
+ if (segmentIndex + 1 >= this.interpolator.path.Count - 1) {
+ MoveToSegment(segmentIndex, 1.0f);
+ return;
+ } else {
+ MoveToSegment(segmentIndex + 1, 0.0f);
+ }
+ } else {
+ this.currentDistance += remainingOffset;
+ return;
+ }
+ } else {
+ // Move backward along the segment
+ var remainingOnCurrentSegment = this.currentDistance - this.distanceToSegmentStart;
+ if (-remainingOffset > remainingOnCurrentSegment) {
+ remainingOffset += remainingOnCurrentSegment;
+ if (segmentIndex - 1 < 0) {
+ MoveToSegment(segmentIndex, 0.0f);
+ return;
+ } else {
+ MoveToSegment(segmentIndex - 1, 1.0f);
+ }
+ } else {
+ this.currentDistance += remainingOffset;
+ return;
+ }
+ }
+ }
+ }
+
+ void PrevSegment () {
+ segmentIndex--;
+ currentSegmentLength = (interpolator.path[segmentIndex+1] - interpolator.path[segmentIndex]).magnitude;
+ distanceToSegmentStart -= currentSegmentLength;
+ }
+
+ void NextSegment () {
+ segmentIndex++;
+ distanceToSegmentStart += currentSegmentLength;
+ currentSegmentLength = (interpolator.path[segmentIndex+1] - interpolator.path[segmentIndex]).magnitude;
+ }
+ }
+
+ List<Vector3> path;
+ int version = 1;
+ float totalDistance;
+
+ /// <summary>
+ /// True if this instance has a path set.
+ /// See: SetPath
+ /// </summary>
+ public bool valid {
+ get {
+ return path != null;
+ }
+ }
+
+ public Cursor start {
+ get {
+ return Cursor.StartOfPath(this);
+ }
+ }
+
+ public Cursor AtDistanceFromStart (float distance) {
+ var cursor = start;
+
+ cursor.distance = distance;
+ return cursor;
+ }
+
+ /// <summary>
+ /// Set the path to interpolate along.
+ /// This will invalidate all existing cursors.
+ /// </summary>
+ public void SetPath (List<Vector3> path) {
+ this.version++;
+ if (this.path == null) this.path = new List<Vector3>();
+ this.path.Clear();
+
+ if (path == null) {
+ totalDistance = float.PositiveInfinity;
+ return;
+ }
+
+ if (path.Count < 2) throw new System.ArgumentException("Path must have a length of at least 2");
+
+ var prev = path[0];
+
+ totalDistance = 0;
+ this.path.Capacity = Mathf.Max(this.path.Capacity, path.Count);
+ this.path.Add(path[0]);
+ for (int i = 1; i < path.Count; i++) {
+ var current = path[i];
+ // Avoid degenerate segments
+ if (current != prev) {
+ totalDistance += (current - prev).magnitude;
+ this.path.Add(current);
+ prev = current;
+ }
+ }
+ if (this.path.Count < 2) this.path.Add(path[0]);
+ if (float.IsNaN(totalDistance)) throw new System.ArgumentException("Path contains NaN values");
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs.meta
new file mode 100644
index 0000000..ed9f0c3
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathInterpolator.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 9c0392dbc5e744ee28e7b9ee81aea1e2
+timeCreated: 1490125383
+licenseType: Store
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs
new file mode 100644
index 0000000..d09535c
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs
@@ -0,0 +1,32 @@
+namespace Pathfinding.Util {
+ /// <summary>
+ /// Represents a part of a path, with optional link information.
+ ///
+ /// A path is divided up into parts, where each part is either a sequence of nodes or an off-mesh link.
+ /// If your agent never traverses off-mesh links, the path will always consist of only a single part which is a sequence of nodes.
+ /// </summary>
+ public struct PathPartWithLinkInfo {
+ public PathPartWithLinkInfo(int startIndex, int endIndex, OffMeshLinks.OffMeshLinkTracer linkInfo = default) {
+ this.startIndex = startIndex;
+ this.endIndex = endIndex;
+ this.linkInfo = linkInfo;
+ }
+
+ /// <summary>
+ /// Index of the first point in the path that this part represents.
+ ///
+ /// For off-mesh links, this will refer to the last point in the part before the off-mesh link.
+ /// </summary>
+ public int startIndex;
+ /// <summary>
+ /// Index of the last point in the path that this part represents.
+ ///
+ /// For off-mesh links, this will refer to the first point in the part after the off-mesh link.
+ /// </summary>
+ public int endIndex;
+ /// <summary>The off-mesh link that this part represents. Will contain a null link if this part is not an off-mesh link</summary>
+ public OffMeshLinks.OffMeshLinkTracer linkInfo;
+ /// <summary>Specifies if this is a sequence of nodes, or an off-mesh link</summary>
+ public Funnel.PartType type => linkInfo.link != null ? Funnel.PartType.OffMeshLink : Funnel.PartType.NodeSequence;
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs.meta
new file mode 100644
index 0000000..ac0fcdb
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathPartWithLinkInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ef250ec5543ec034cbf24245e48384bf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs
new file mode 100644
index 0000000..420663a
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs
@@ -0,0 +1,71 @@
+namespace Pathfinding {
+ [System.Serializable]
+ public struct PathRequestSettings {
+ /// <summary>
+ /// Graphs that this agent can use.
+ /// This field determines which graphs will be considered when searching for the start and end nodes of a path.
+ /// It is useful in numerous situations, for example if you want to make one graph for small units and one graph for large units.
+ ///
+ /// This is a bitmask so if you for example want to make the agent only use graph index 3 then you can set this to:
+ /// <code> settings.graphMask = 1 << 3; </code>
+ ///
+ /// See: bitmasks (view in online documentation for working links)
+ ///
+ /// Note that this field only stores which graph indices that are allowed. This means that if the graphs change their ordering
+ /// then this mask may no longer be correct.
+ ///
+ /// If you know the name of the graph you can use the <see cref="Pathfinding.GraphMask.FromGraphName"/> method:
+ /// <code>
+ /// GraphMask mask1 = GraphMask.FromGraphName("My Grid Graph");
+ /// GraphMask mask2 = GraphMask.FromGraphName("My Other Grid Graph");
+ ///
+ /// NNConstraint nn = NNConstraint.Walkable;
+ ///
+ /// nn.graphMask = mask1 | mask2;
+ ///
+ /// // Find the node closest to somePoint which is either in 'My Grid Graph' OR in 'My Other Grid Graph'
+ /// var info = AstarPath.active.GetNearest(somePoint, nn);
+ /// </code>
+ ///
+ /// See: multiple-agent-types (view in online documentation for working links)
+ /// </summary>
+ public GraphMask graphMask;
+
+ /// <summary>
+ /// The penalty for each tag.
+ ///
+ /// If null, all penalties will be treated as zero. Otherwise, the array should always have a length of exactly 32.
+ /// </summary>
+ public int[] tagPenalties;
+
+ /// <summary>
+ /// The tags which this agent can traverse.
+ ///
+ /// Note: This field is a bitmask.
+ /// See: bitmasks (view in online documentation for working links)
+ /// </summary>
+ public int traversableTags;
+
+ /// <summary>
+ /// Filters which nodes the agent can traverse, and can also add penalties to each traversed node.
+ ///
+ /// In most common situations, this is left as null (which implies the default traversal provider: <see cref="DefaultITraversalProvider"/>).
+ /// But if you need custom pathfinding behavior which cannot be done using the <see cref="graphMask"/>, <see cref="tagPenalties"/> and <see cref="traversableTags"/>, then setting an <see cref="ITraversalProvider"/> is a great option.
+ /// It provides you a lot more control over how the pathfinding works.
+ ///
+ /// <code>
+ /// followerEntity.pathfindingSettings.traversalProvider = new MyCustomTraversalProvider();
+ /// </code>
+ ///
+ /// See: traversal_provider (view in online documentation for working links)
+ /// </summary>
+ public ITraversalProvider traversalProvider;
+
+ public static PathRequestSettings Default => new PathRequestSettings {
+ graphMask = GraphMask.everything,
+ tagPenalties = new int[32],
+ traversableTags = -1,
+ traversalProvider = null,
+ };
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs.meta
new file mode 100644
index 0000000..9264f08
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathRequestSettings.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 159402c3a3fd6434cad55fd1055058e7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs
new file mode 100644
index 0000000..0bd4da2
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs
@@ -0,0 +1,47 @@
+using Pathfinding;
+
+namespace Pathfinding {
+ /// <summary>
+ /// Represents a single pathfinding tag.
+ ///
+ /// Note: The tag refers to a pathfinding tag, not a unity tag that is applied to GameObjects, or any other kind of tag.
+ ///
+ /// See: tags (view in online documentation for working links)
+ /// </summary>
+ [System.Serializable]
+ public struct PathfindingTag {
+ /// <summary>
+ /// Underlaying tag value.
+ /// Should always be between 0 and <see cref="GraphNode.MaxTagIndex"/> (inclusive).
+ /// </summary>
+ public uint value;
+
+ public PathfindingTag(uint value) {
+ this.value = value;
+ }
+
+ public static implicit operator uint (PathfindingTag tag) {
+ return tag.value;
+ }
+
+ public static implicit operator PathfindingTag(uint tag) {
+ return new PathfindingTag(tag);
+ }
+
+ /// <summary>Get the value of the PathfindingTag with the given name</summary>
+ public static PathfindingTag FromName (string tagName) {
+ AstarPath.FindAstarPath();
+ if (AstarPath.active == null) throw new System.InvalidOperationException("There's no AstarPath component in the scene. Cannot get tag names.");
+
+ var tagNames = AstarPath.active.GetTagNames();
+ var tag = System.Array.IndexOf(tagNames, tagName);
+ if (tag == -1) throw new System.ArgumentException("There's no pathfinding tag with the name '" + tagName + "'");
+
+ return new PathfindingTag((uint)tag);
+ }
+
+ public override string ToString () {
+ return value.ToString();
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs.meta
new file mode 100644
index 0000000..4deed42
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/PathfindingTag.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2eeb70ede92586d3e9834dd8f17a8097
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs
new file mode 100644
index 0000000..4fa7b59
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs
@@ -0,0 +1,315 @@
+using UnityEngine;
+using Unity.Collections;
+using Unity.Mathematics;
+using Unity.Burst;
+
+namespace Pathfinding.RVO {
+ /// <summary>
+ /// Controls if the agent slows down to a stop if the area around the destination is crowded.
+ /// The main idea for this script is to
+ /// - Reduce the local avoidance priority for agents that have reached their destination once.
+ /// - Make agents stop if there is a high density of units around its destination.
+ ///
+ /// 'High density' is defined as:
+ /// Take the circle with the center at the AI's destination and a radius such that the AI's current position
+ /// is touching its border. Let 'A' be the area of that circle. Further let 'a' be the total area of all
+ /// individual agents inside that circle.
+ /// The agent should stop if a > A*0.6 or something like that. I.e if the agents inside the circle cover
+ /// over 60% of the surface of the circle. The 60% figure can be modified (see <see cref="densityThreshold)"/>.
+ ///
+ /// This script was inspired by how Starcraft 2 does its local avoidance.
+ ///
+ /// See: <see cref="Pathfinding.AIBase.rvoDensityBehavior"/>
+ /// </summary>
+ [System.Serializable]
+ public struct RVODestinationCrowdedBehavior {
+ /// <summary>Enables or disables this module</summary>
+ public bool enabled;
+
+ /// <summary>
+ /// The threshold for when to stop.
+ /// See the class description for more info.
+ /// </summary>
+ [Range(0, 1)]
+ public float densityThreshold;
+
+ /// <summary>
+ /// If true, the agent will start to move to the destination again if it determines that it is now less crowded.
+ /// If false and the destination becomes less crowded (or if the agent is pushed away from the destination in some way), then the agent will still stay put.
+ /// </summary>
+ public bool returnAfterBeingPushedAway;
+
+ public float progressAverage;
+ bool wasEnabled;
+ float timer1;
+ float shouldStopDelayTimer;
+ bool lastShouldStopResult;
+ Vector3 lastShouldStopDestination;
+ Vector3 reachedDestinationPoint;
+ public bool lastJobDensityResult;
+
+ /// <summary>See https://en.wikipedia.org/wiki/Circle_packing</summary>
+ const float MaximumCirclePackingDensity = 0.9069f;
+
+ [BurstCompile(CompileSynchronously = false, FloatMode = FloatMode.Fast)]
+ public struct JobDensityCheck : Pathfinding.Jobs.IJobParallelForBatched {
+ [ReadOnly]
+ RVOQuadtreeBurst quadtree;
+ [ReadOnly]
+ public NativeArray<QueryData> data;
+ [ReadOnly]
+ public NativeArray<float3> agentPosition;
+ [ReadOnly]
+ NativeArray<float3> agentTargetPoint;
+ [ReadOnly]
+ NativeArray<float> agentRadius;
+ [ReadOnly]
+ NativeArray<float> agentDesiredSpeed;
+ [ReadOnly]
+ NativeArray<float3> agentOutputTargetPoint;
+ [ReadOnly]
+ NativeArray<float> agentOutputSpeed;
+ [WriteOnly]
+ public NativeArray<bool> outThresholdResult;
+ public NativeArray<float> progressAverage;
+
+ public float deltaTime;
+
+ public bool allowBoundsChecks => false;
+
+ public struct QueryData {
+ public float3 agentDestination;
+ public int agentIndex;
+ public float densityThreshold;
+ }
+
+ public JobDensityCheck(int size, float deltaTime) {
+ var simulator = RVOSimulator.active.GetSimulator() as SimulatorBurst;
+
+ agentPosition = simulator.simulationData.position;
+ agentTargetPoint = simulator.simulationData.targetPoint;
+ agentRadius = simulator.simulationData.radius;
+ agentDesiredSpeed = simulator.simulationData.desiredSpeed;
+ agentOutputTargetPoint = simulator.outputData.targetPoint;
+ agentOutputSpeed = simulator.outputData.speed;
+ quadtree = simulator.quadtree;
+ data = new NativeArray<QueryData>(size, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ outThresholdResult = new NativeArray<bool>(size, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ progressAverage = new NativeArray<float>(size, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ this.deltaTime = deltaTime;
+ }
+
+ public void Dispose () {
+ data.Dispose();
+ outThresholdResult.Dispose();
+ progressAverage.Dispose();
+ }
+
+ public void Set (int index, int rvoAgentIndex, float3 destination, float densityThreshold, float progressAverage) {
+ data[index] = new QueryData {
+ agentDestination = destination,
+ densityThreshold = densityThreshold,
+ agentIndex = rvoAgentIndex,
+ };
+ this.progressAverage[index] = progressAverage;
+ }
+
+ void Pathfinding.Jobs.IJobParallelForBatched.Execute (int start, int count) {
+ for (int i = start; i < start + count; i++) {
+ Execute(i);
+ }
+ }
+
+ float AgentDensityInCircle (float3 position, float radius) {
+ return quadtree.QueryArea(position, radius) / (radius * radius * math.PI);
+ }
+
+ void Execute (int i) {
+ var query = data[i];
+
+ var position = agentPosition[query.agentIndex];
+ var radius = agentRadius[query.agentIndex];
+
+ var desiredDirection = math.normalizesafe(agentTargetPoint[query.agentIndex] - position);
+ float delta;
+
+ if (agentDesiredSpeed[query.agentIndex] > 0.01f) {
+ // How quickly the agent can move
+ var speedTowardsTarget = math.dot(desiredDirection, math.normalizesafe(agentOutputTargetPoint[query.agentIndex] - position) * agentOutputSpeed[query.agentIndex]);
+ // Make it relative to how quickly it wants to move
+ // So 0.0 means it is stuck
+ // 1.0 means it is moving as quickly as it wants
+ // Cap the desired speed by the agent's radius. This avoids making agents that want to move very quickly
+ // but are slowed down to a still reasonable speed get a very low progressAverage.
+ delta = speedTowardsTarget / math.max(0.001f, math.min(agentDesiredSpeed[query.agentIndex], agentRadius[query.agentIndex]));
+ // Clamp between -1 and 1
+ delta = math.clamp(delta, -1.0f, 1.0f);
+ } else {
+ // If the agent doesn't want to move anywhere then it has 100% progress
+ delta = 1.0f;
+ }
+
+ // Exponentially decaying average of the deltas (in seconds^-1)
+ const float FilterConvergenceSpeed = 2.0f;
+ progressAverage[i] = math.lerp(progressAverage[i], delta, FilterConvergenceSpeed * deltaTime);
+
+ // If no destination has been set, then always stop
+ if (math.any(math.isinf(query.agentDestination))) {
+ outThresholdResult[i] = true;
+ return;
+ }
+
+ var checkRadius = math.length(query.agentDestination - position);
+ var checkRadius2 = radius*5;
+
+ if (checkRadius > checkRadius2) {
+ // TODO: Change center to be slightly biased towards agent position
+ // If the agent is far away from the destination then do a faster check around the destination first.
+ if (AgentDensityInCircle(query.agentDestination, checkRadius2) < MaximumCirclePackingDensity*query.densityThreshold) {
+ outThresholdResult[i] = false;
+ return;
+ }
+ }
+
+ outThresholdResult[i] = AgentDensityInCircle(query.agentDestination, checkRadius) > MaximumCirclePackingDensity*query.densityThreshold;
+ }
+ }
+
+ public void ReadJobResult (ref JobDensityCheck jobResult, int index) {
+ bool shouldStop = jobResult.outThresholdResult[index];
+
+ progressAverage = jobResult.progressAverage[index];
+
+ lastJobDensityResult = shouldStop;
+ shouldStopDelayTimer = Mathf.Lerp(shouldStopDelayTimer, shouldStop ? 1 : 0, Time.deltaTime);
+ shouldStop = shouldStop && shouldStopDelayTimer > 0.1f;
+ lastShouldStopResult = shouldStop;
+ lastShouldStopDestination = jobResult.data[index].agentDestination;
+ }
+
+ public RVODestinationCrowdedBehavior (bool enabled, float densityFraction, bool returnAfterBeingPushedAway) {
+ this.enabled = wasEnabled = enabled;
+ this.densityThreshold = densityFraction;
+ this.returnAfterBeingPushedAway = returnAfterBeingPushedAway;
+ this.lastJobDensityResult = false;
+ this.progressAverage = 0;
+ this.wasStopped = false;
+ this.lastShouldStopDestination = new Vector3(float.NaN, float.NaN, float.NaN);
+ this.reachedDestinationPoint = new Vector3(float.NaN, float.NaN, float.NaN);
+ timer1 = 0;
+ shouldStopDelayTimer = 0;
+ reachedDestination = false;
+ lastShouldStopResult = false;
+ }
+
+ /// <summary>
+ /// Marks the destination as no longer being reached.
+ ///
+ /// If the agent had stopped because the destination was crowded, this will make it immediately try again
+ /// to move forwards if it can. If the destination is still crowded it will soon stop again.
+ ///
+ /// This is useful to call when a user gave an agent an explicit order to ensure it doesn't
+ /// just stay in the same location without even trying to move forwards.
+ /// </summary>
+ public void ClearDestinationReached () {
+ wasStopped = false;
+ progressAverage = 1.0f;
+ reachedDestination = false;
+ }
+
+ public void OnDestinationChanged (Vector3 newDestination, bool reachedDestination) {
+ timer1 = float.PositiveInfinity;
+ // TODO: Check previous ShouldStop result. Check how much the circles overlap.
+ // With significant overlap we may want to keep reachedCurrentDestination = true
+ this.reachedDestination = reachedDestination; // (ideal: || ShouldStop(ai, rvo))
+ }
+
+ /// <summary>
+ /// True if the agent has reached its destination.
+ /// If the agents destination changes this may return false until the next frame.
+ /// Note that changing the destination every frame may cause this value to never return true.
+ ///
+ /// True will be returned if the agent has stopped due to being close enough to the destination.
+ /// This may be quite some distance away if there are many other agents around the destination.
+ ///
+ /// See: <see cref="Pathfinding.IAstarAI.destination"/>
+ /// </summary>
+ public bool reachedDestination { get; private set; }
+
+ bool wasStopped;
+
+ const float DefaultPriority = 1.0f;
+ const float StoppedPriority = 0.1f;
+ const float MoveBackPriority = 0.5f;
+
+ public void Update (bool rvoControllerEnabled, bool reachedDestination, ref bool isStopped, ref float rvoPriorityMultiplier, ref float rvoFlowFollowingStrength, Vector3 agentPosition) {
+ if (!(enabled && rvoControllerEnabled)) {
+ if (wasEnabled) {
+ wasEnabled = false;
+ // Reset to default values
+ rvoPriorityMultiplier = DefaultPriority;
+ rvoFlowFollowingStrength = 0;
+ timer1 = float.PositiveInfinity;
+ progressAverage = 1.0f;
+ }
+ return;
+ }
+ wasEnabled = true;
+
+ if (reachedDestination) {
+ var validRange = (agentPosition - this.reachedDestinationPoint).sqrMagnitude;
+ if ((lastShouldStopDestination - this.reachedDestinationPoint).sqrMagnitude > validRange) {
+ // The reachedDestination bool is no longer valid.
+ // The destination has moved significantly from the last point where we detected that it was crowded.
+ // It may end up being set to true immediately afterwards though if
+ // the parameter reachedDestination (not this.reachedDestination) is true.
+ this.reachedDestination = false;
+ }
+ }
+
+ if (reachedDestination || lastShouldStopResult) {
+ // We have reached the destination the destination is crowded enough that we should stop here anyway
+ timer1 = 0f;
+ this.reachedDestination = true;
+ this.reachedDestinationPoint = this.lastShouldStopDestination;
+ rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, StoppedPriority, Time.deltaTime * 2);
+ rvoFlowFollowingStrength = Mathf.Lerp(rvoFlowFollowingStrength, 1.0f, Time.deltaTime * 4);
+ wasStopped |= math.abs(progressAverage) < 0.1f;
+ isStopped |= wasStopped; // false && rvoPriorityMultiplier > 0.9f;
+ } else if (isStopped) {
+ // We have not reached the destination, but a separate script is telling is to stop
+ timer1 = 0f;
+ this.reachedDestination = false;
+ rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, StoppedPriority, Time.deltaTime * 2);
+ rvoFlowFollowingStrength = Mathf.Lerp(rvoFlowFollowingStrength, 1.0f, Time.deltaTime * 4);
+ wasStopped |= math.abs(progressAverage) < 0.1f;
+ } else {
+ // Check if we had reached the current destination previously (but it is not reached any longer)
+ // TODO: Rename variable, confusing
+ if (this.reachedDestination) {
+ timer1 += Time.deltaTime;
+ if (timer1 > 3 && returnAfterBeingPushedAway) {
+ // Make the agent try to move back to the destination
+ // Use a slightly higher priority than agents that are just standing still, but lower than regular agents
+ rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, MoveBackPriority, Time.deltaTime * 2);
+ rvoFlowFollowingStrength = 0;
+ isStopped = false;
+ wasStopped = false;
+ } else {
+ rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, StoppedPriority, Time.deltaTime * 2);
+ rvoFlowFollowingStrength = Mathf.Lerp(rvoFlowFollowingStrength, 1.0f, Time.deltaTime * 4);
+ wasStopped |= math.abs(progressAverage) < 0.1f;
+ isStopped = wasStopped;
+ //isStopped = false && rvoPriorityMultiplier > 0.9f;
+ }
+ } else {
+ // This is the common case: the agent is just on its way to the destination
+ rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, DefaultPriority, Time.deltaTime * 4);
+ rvoFlowFollowingStrength = 0f;
+ isStopped = false;
+ wasStopped = false;
+ }
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs.meta
new file mode 100644
index 0000000..1957e7c
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/RVODestinationCrowdedBehavior.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 3346d0269f43f49adbf879577f758cd0
+timeCreated: 1503053987
+licenseType: Store
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs
new file mode 100644
index 0000000..6fdd1e6
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs
@@ -0,0 +1,38 @@
+using UnityEngine;
+
+namespace Pathfinding.Util {
+ /// <summary>Compatibility class for Unity APIs that are not available in all Unity versions</summary>
+ public static class UnityCompatibility {
+ public static T[] FindObjectsByTypeSorted<T>() where T : Object {
+#if UNITY_2021_3_OR_NEWER && !(UNITY_2022_1_OR_NEWER && !UNITY_2022_2_OR_NEWER)
+ return Object.FindObjectsByType<T>(FindObjectsSortMode.InstanceID);
+#else
+ return Object.FindObjectsOfType<T>();
+#endif
+ }
+
+ public static T[] FindObjectsByTypeUnsorted<T>() where T : Object {
+#if UNITY_2021_3_OR_NEWER && !(UNITY_2022_1_OR_NEWER && !UNITY_2022_2_OR_NEWER)
+ return Object.FindObjectsByType<T>(FindObjectsSortMode.None);
+#else
+ return Object.FindObjectsOfType<T>();
+#endif
+ }
+
+ public static T[] FindObjectsByTypeUnsortedWithInactive<T>() where T : Object {
+#if UNITY_2021_3_OR_NEWER && !(UNITY_2022_1_OR_NEWER && !UNITY_2022_2_OR_NEWER)
+ return Object.FindObjectsByType<T>(FindObjectsInactive.Include, FindObjectsSortMode.None);
+#else
+ return Object.FindObjectsOfType<T>(true);
+#endif
+ }
+
+ public static T FindAnyObjectByType<T>() where T : Object {
+#if UNITY_2021_3_OR_NEWER && !(UNITY_2022_1_OR_NEWER && !UNITY_2022_2_OR_NEWER)
+ return Object.FindAnyObjectByType<T>();
+#else
+ return Object.FindObjectOfType<T>();
+#endif
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs.meta
new file mode 100644
index 0000000..abff2d5
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/UnityCompatibility.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e62cc47f530e57d4294dbb1baba8ffc5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs
new file mode 100644
index 0000000..fabfbae
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs
@@ -0,0 +1,140 @@
+#if NETFX_CORE
+using System.Threading;
+using System.Threading.Tasks;
+using System.Reflection;
+using System.IO;
+using TP = System.Reflection.TypeInfo;
+#else
+using TP = System.Type;
+#endif
+
+namespace Pathfinding.WindowsStore {
+ public static class WindowsStoreCompatibility {
+ public static TP GetTypeInfo (System.Type type) {
+#if NETFX_CORE
+ return type.GetTypeInfo();
+#else
+ return type;
+#endif
+ }
+
+#if NETFX_CORE
+ public static void Close (this BinaryWriter stream) {
+ stream.Dispose();
+ }
+
+ public static void Close (this BinaryReader stream) {
+ stream.Dispose();
+ }
+
+ public static void Close (this StreamWriter stream) {
+ stream.Dispose();
+ }
+#endif
+ }
+
+#if NETFX_CORE
+ public delegate void ParameterizedThreadStart(System.Object ob);
+ public delegate void ThreadStart();
+
+ public class Thread {
+ //
+ // Fields
+ //
+ private Pathfinding.WindowsStore.ParameterizedThreadStart _paramThreadStart;
+
+ private CancellationTokenSource _taskCancellationTokenSource;
+
+ private Task _task = null;
+
+ private Pathfinding.WindowsStore.ThreadStart _threadStart;
+
+ private static ManualResetEvent SleepEvent = new ManualResetEvent(false);
+
+ //
+ // Properties
+ //
+ public bool IsAlive {
+ get {
+ return this._task != null && !this._task.IsCompleted;
+ }
+ set {
+ throw new System.NotImplementedException();
+ }
+ }
+
+ public bool IsBackground {
+ get {
+ return false;
+ }
+ set {
+ }
+ }
+
+ public string Name {
+ get;
+ set;
+ }
+
+ //
+ // Constructors
+ //
+ public Thread (Pathfinding.WindowsStore.ParameterizedThreadStart start) {
+ this._taskCancellationTokenSource = new CancellationTokenSource();
+ this._paramThreadStart = start;
+ }
+
+ public Thread (Pathfinding.WindowsStore.ThreadStart start) {
+ this._taskCancellationTokenSource = new CancellationTokenSource();
+ this._threadStart = start;
+ }
+
+ //
+ // Static Methods
+ //
+ public static void Sleep (int ms) {
+ SleepEvent.WaitOne(ms);
+ }
+
+ //
+ // Methods
+ //
+ public void Abort () {
+ if (this._taskCancellationTokenSource != null) {
+ this._taskCancellationTokenSource.Cancel();
+ }
+ }
+
+ private void EnsureTask (object paramThreadStartParam = null) {
+ if (this._task == null) {
+ if (this._paramThreadStart != null) {
+ this._task = new Task(delegate {
+ this._paramThreadStart(paramThreadStartParam);
+ }, this._taskCancellationTokenSource.Token);
+ } else {
+ if (this._threadStart != null) {
+ this._task = new Task(delegate {
+ this._threadStart();
+ }, this._taskCancellationTokenSource.Token);
+ }
+ }
+ }
+ }
+
+ public bool Join (int ms) {
+ this.EnsureTask();
+ return this._task.Wait(ms, this._taskCancellationTokenSource.Token);
+ }
+
+ public void Start () {
+ this.EnsureTask();
+ this._task.Start(TaskScheduler.Default);
+ }
+
+ public void Start (object param) {
+ this.EnsureTask(param);
+ this._task.Start(TaskScheduler.Default);
+ }
+ }
+#endif
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs.meta
new file mode 100644
index 0000000..d99c0c7
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WindowsStoreCompatibility.cs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 810a4e97f5ccb4b5184c4c3206492974
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs
new file mode 100644
index 0000000..6e8c615
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs
@@ -0,0 +1,419 @@
+using UnityEngine;
+using UnityEngine.Profiling;
+using Unity.Jobs;
+using UnityEngine.Assertions;
+
+namespace Pathfinding {
+ /// <summary>
+ /// An item of work that can be executed when graphs are safe to update.
+ /// See: <see cref="AstarPath.UpdateGraphs"/>
+ /// See: <see cref="AstarPath.AddWorkItem"/>
+ /// </summary>
+ public struct AstarWorkItem {
+ /// <summary>
+ /// Init function.
+ /// May be null if no initialization is needed.
+ /// Will be called once, right before the first call to <see cref="update"/> or <see cref="updateWithContext"/>.
+ /// </summary>
+ public System.Action init;
+
+ /// <summary>
+ /// Init function.
+ /// May be null if no initialization is needed.
+ /// Will be called once, right before the first call to <see cref="update"/> or <see cref="updateWithContext"/>.
+ ///
+ /// A context object is sent as a parameter. This can be used
+ /// to for example queue a flood fill that will be executed either
+ /// when a work item calls EnsureValidFloodFill or all work items have
+ /// been completed. If multiple work items are updating nodes
+ /// so that they need a flood fill afterwards, using the QueueFloodFill
+ /// method is preferred since then only a single flood fill needs
+ /// to be performed for all of the work items instead of one
+ /// per work item.
+ /// </summary>
+ public System.Action<IWorkItemContext> initWithContext;
+
+ /// <summary>
+ /// Update function, called once per frame when the work item executes.
+ /// Takes a param force. If that is true, the work item should try to complete the whole item in one go instead
+ /// of spreading it out over multiple frames.
+ ///
+ /// Warning: If you make modifications to the graphs, they must only be made during the last time the <see cref="update"/> method is called.
+ /// Earlier invocations, as well as the <see cref="init"/>/<see cref="initWithContext"/> mehods, are only for pre-calculating information required for the update.
+ ///
+ /// Returns: True when the work item is completed.
+ /// </summary>
+ public System.Func<bool, bool> update;
+
+ /// <summary>
+ /// Update function, called once per frame when the work item executes.
+ /// Takes a param force. If that is true, the work item should try to complete the whole item in one go instead
+ /// of spreading it out over multiple frames.
+ /// Returns: True when the work item is completed.
+ ///
+ /// Warning: If you make modifications to the graphs, they must only be made during the last time the <see cref="update"/> method is called.
+ /// Earlier invocations, as well as the <see cref="init"/>/<see cref="initWithContext"/> mehods, are only for pre-calculating information required for the update.
+ ///
+ /// A context object is sent as a parameter. This can be used
+ /// to for example queue a flood fill that will be executed either
+ /// when a work item calls EnsureValidFloodFill or all work items have
+ /// been completed. If multiple work items are updating nodes
+ /// so that they need a flood fill afterwards, using the QueueFloodFill
+ /// method is preferred since then only a single flood fill needs
+ /// to be performed for all of the work items instead of one
+ /// per work item.
+ /// </summary>
+ public System.Func<IWorkItemContext, bool, bool> updateWithContext;
+
+ /// <summary>Creates a work item which will call the specified functions when executed.</summary>
+ /// <param name="update">Will be called once per frame when the work item executes. See #update for details.</param>
+ public AstarWorkItem (System.Func<bool, bool> update) {
+ this.init = null;
+ this.initWithContext = null;
+ this.updateWithContext = null;
+ this.update = update;
+ }
+
+ /// <summary>Creates a work item which will call the specified functions when executed.</summary>
+ /// <param name="update">Will be called once per frame when the work item executes. See #updateWithContext for details.</param>
+ public AstarWorkItem (System.Func<IWorkItemContext, bool, bool> update) {
+ this.init = null;
+ this.initWithContext = null;
+ this.updateWithContext = update;
+ this.update = null;
+ }
+
+ /// <summary>Creates a work item which will call the specified functions when executed.</summary>
+ /// <param name="init">Will be called once, right before the first call to update. See #init for details.</param>
+ /// <param name="update">Will be called once per frame when the work item executes. See #update for details.</param>
+ public AstarWorkItem (System.Action init, System.Func<bool, bool> update = null) {
+ this.init = init;
+ this.initWithContext = null;
+ this.update = update;
+ this.updateWithContext = null;
+ }
+
+ /// <summary>Creates a work item which will call the specified functions when executed.</summary>
+ /// <param name="init">Will be called once, right before the first call to update. See #initWithContext for details.</param>
+ /// <param name="update">Will be called once per frame when the work item executes. See #updateWithContext for details.</param>
+ public AstarWorkItem (System.Action<IWorkItemContext> init, System.Func<IWorkItemContext, bool, bool> update = null) {
+ this.init = null;
+ this.initWithContext = init;
+ this.update = null;
+ this.updateWithContext = update;
+ }
+ }
+
+ /// <summary>Interface to expose a subset of the WorkItemProcessor functionality</summary>
+ public interface IWorkItemContext : IGraphUpdateContext {
+ /// <summary>
+ /// Call during work items to queue a flood fill.
+ /// An instant flood fill can be done via FloodFill()
+ /// but this method can be used to batch several updates into one
+ /// to increase performance.
+ /// WorkItems which require a valid Flood Fill in their execution can call EnsureValidFloodFill
+ /// to ensure that a flood fill is done if any earlier work items queued one.
+ ///
+ /// Once a flood fill is queued it will be done after all WorkItems have been executed.
+ ///
+ /// Deprecated: Avoid using. This will force a full recalculation of the connected components. In most cases the HierarchicalGraph class takes care of things automatically behind the scenes now. In pretty much all cases you should be able to remove the call to this function.
+ /// </summary>
+ [System.Obsolete("Avoid using. This will force a full recalculation of the connected components. In most cases the HierarchicalGraph class takes care of things automatically behind the scenes now. In pretty much all cases you should be able to remove the call to this function.")]
+ void QueueFloodFill();
+
+ /// <summary>
+ /// If a WorkItem needs to have a valid area information during execution, call this method to ensure there are no pending flood fills.
+ /// If you are using the <see cref="Pathfinding.GraphNode.Area"/> property or the <see cref="Pathfinding.PathUtilities.IsPathPossible"/> method in your work items, then you might want to call this method before you use them
+ /// to ensure that the data is up to date.
+ ///
+ /// See: <see cref="Pathfinding.HierarchicalGraph"/>
+ ///
+ /// <code>
+ /// AstarPath.active.AddWorkItem(new AstarWorkItem((IWorkItemContext ctx) => {
+ /// ctx.EnsureValidFloodFill();
+ ///
+ /// // The above call guarantees that this method has up to date information about the graph
+ /// if (PathUtilities.IsPathPossible(someNode, someOtherNode)) {
+ /// // Do something
+ /// }
+ /// }));
+ /// </code>
+ /// </summary>
+ void EnsureValidFloodFill();
+
+ /// <summary>
+ /// Call to send a GraphModifier.EventType.PreUpdate event to all graph modifiers.
+ /// The difference between this and GraphModifier.TriggerEvent(GraphModifier.EventType.PreUpdate) is that using this method
+ /// ensures that multiple PreUpdate events will not be issued during a single update.
+ ///
+ /// Once an event has been sent no more events will be sent until all work items are complete and a PostUpdate or PostScan event is sent.
+ ///
+ /// When scanning a graph PreUpdate events are never sent. However a PreScan event is always sent before a scan begins.
+ /// </summary>
+ void PreUpdate();
+
+ /// <summary>
+ /// Trigger a graph modification event.
+ /// This will cause a <see cref="GraphModifier.EventType.PostUpdate"/> event to be issued after all graph updates have finished.
+ /// Some scripts listen for this event. For example off-mesh links listen to it and will recalculate which nodes they are connected to when it it sent.
+ /// If a graph is dirtied multiple times, or even if multiple graphs are dirtied, the event will only be sent once.
+ /// </summary>
+ // TODO: Deprecate?
+ void SetGraphDirty(NavGraph graph);
+ }
+
+ class WorkItemProcessor : IWorkItemContext {
+ public event System.Action OnGraphsUpdated;
+
+ /// <summary>Used to prevent waiting for work items to complete inside other work items as that will cause the program to hang</summary>
+ public bool workItemsInProgressRightNow { get; private set; }
+
+ readonly AstarPath astar;
+ readonly IndexedQueue<AstarWorkItem> workItems = new IndexedQueue<AstarWorkItem>();
+
+
+ /// <summary>True if any work items are queued right now</summary>
+ public bool anyQueued {
+ get { return workItems.Count > 0; }
+ }
+
+ bool anyGraphsDirty = true;
+ bool preUpdateEventSent = false;
+
+ /// <summary>
+ /// True while a batch of work items are being processed.
+ /// Set to true when a work item is started to be processed, reset to false when all work items are complete.
+ ///
+ /// Work item updates are often spread out over several frames, this flag will be true during the whole time the
+ /// updates are in progress.
+ /// </summary>
+ public bool workItemsInProgress { get; private set; }
+
+ /// <summary>Similar to Queue<T> but allows random access</summary>
+ // TODO: Replace with CircularBuffer?
+ class IndexedQueue<T> {
+ T[] buffer = new T[4];
+ int start;
+
+ public T this[int index] {
+ get {
+ if (index < 0 || index >= Count) throw new System.IndexOutOfRangeException();
+ return buffer[(start + index) % buffer.Length];
+ }
+ set {
+ if (index < 0 || index >= Count) throw new System.IndexOutOfRangeException();
+ buffer[(start + index) % buffer.Length] = value;
+ }
+ }
+
+ public int Count { get; private set; }
+
+ public void Enqueue (T item) {
+ if (Count == buffer.Length) {
+ var newBuffer = new T[buffer.Length*2];
+ for (int i = 0; i < Count; i++) {
+ newBuffer[i] = this[i];
+ }
+ buffer = newBuffer;
+ start = 0;
+ }
+
+ buffer[(start + Count) % buffer.Length] = item;
+ Count++;
+ }
+
+ public T Dequeue () {
+ if (Count == 0) throw new System.InvalidOperationException();
+ var item = buffer[start];
+ start = (start + 1) % buffer.Length;
+ Count--;
+ return item;
+ }
+ }
+
+ /// <summary>
+ /// Call during work items to queue a flood fill.
+ /// An instant flood fill can be done via FloodFill()
+ /// but this method can be used to batch several updates into one
+ /// to increase performance.
+ /// WorkItems which require a valid Flood Fill in their execution can call EnsureValidFloodFill
+ /// to ensure that a flood fill is done if any earlier work items queued one.
+ ///
+ /// Once a flood fill is queued it will be done after all WorkItems have been executed.
+ ///
+ /// Deprecated: This method no longer does anything.
+ /// </summary>
+ void IWorkItemContext.QueueFloodFill () {
+ }
+
+ void IWorkItemContext.PreUpdate () {
+ if (!preUpdateEventSent && !astar.isScanning) {
+ preUpdateEventSent = true;
+ GraphModifier.TriggerEvent(GraphModifier.EventType.PreUpdate);
+ }
+ }
+
+ // This will also call DirtyGraphs
+ void IWorkItemContext.SetGraphDirty(NavGraph graph) => astar.DirtyBounds(graph.bounds);
+
+ // This will also call DirtyGraphs
+ void IGraphUpdateContext.DirtyBounds(Bounds bounds) => astar.DirtyBounds(bounds);
+
+ internal void DirtyGraphs () {
+ anyGraphsDirty = true;
+ }
+
+ /// <summary>If a WorkItem needs to have a valid area information during execution, call this method to ensure there are no pending flood fills</summary>
+ public void EnsureValidFloodFill () {
+ astar.hierarchicalGraph.RecalculateIfNecessary();
+ }
+
+ public WorkItemProcessor (AstarPath astar) {
+ this.astar = astar;
+ }
+
+ /// <summary>
+ /// Add a work item to be processed when pathfinding is paused.
+ ///
+ /// See: ProcessWorkItems
+ /// </summary>
+ public void AddWorkItem (AstarWorkItem item) {
+ workItems.Enqueue(item);
+ }
+
+ bool ProcessWorkItems (bool force, bool sendEvents) {
+ if (workItemsInProgressRightNow) throw new System.Exception("Processing work items recursively. Please do not wait for other work items to be completed inside work items. " +
+ "If you think this is not caused by any of your scripts, this might be a bug.");
+
+ // Work items may update graph data arbitrarily
+ // So we need to hold a write lock here so that for example
+ // ECS jobs don't try to read the graph data while it is being updated
+ var lockObj = astar.LockGraphDataForWritingSync();
+ astar.data.LockGraphStructure(true);
+
+ // Make sure the physics engine data is up to date.
+ // Graph updates may use physics methods and it is very confusing if they
+ // do not always pick up the latest changes made to the scene.
+ UnityEngine.Physics.SyncTransforms();
+ UnityEngine.Physics2D.SyncTransforms();
+
+ workItemsInProgressRightNow = true;
+
+ try {
+ bool workRemaining = false;
+ bool anyFinished = false;
+ while (workItems.Count > 0) {
+ // Working on a new batch
+ if (!workItemsInProgress) {
+ workItemsInProgress = true;
+ }
+
+ // Peek at first item in the queue
+ AstarWorkItem itm = workItems[0];
+ bool status;
+
+ try {
+ // Call init the first time the item is seen
+ if (itm.init != null) {
+ itm.init();
+ itm.init = null;
+ }
+
+ if (itm.initWithContext != null) {
+ itm.initWithContext(this);
+ itm.initWithContext = null;
+ }
+
+ // Make sure the item in the queue is up to date
+ workItems[0] = itm;
+
+ if (itm.update != null) {
+ status = itm.update(force);
+ } else if (itm.updateWithContext != null) {
+ status = itm.updateWithContext(this, force);
+ } else {
+ status = true;
+ }
+ } catch {
+ workItems.Dequeue();
+ throw;
+ }
+
+ if (!status) {
+ if (force) {
+ Debug.LogError("Misbehaving WorkItem. 'force'=true but the work item did not complete.\nIf force=true is passed to a WorkItem it should always return true.");
+ }
+
+ // There's more work to do on this work item
+ workRemaining = true;
+ break;
+ } else {
+ workItems.Dequeue();
+ anyFinished = true;
+ }
+ }
+
+ if (sendEvents && anyFinished) {
+ Profiler.BeginSample("PostUpdate");
+ if (anyGraphsDirty) GraphModifier.TriggerEvent(GraphModifier.EventType.PostUpdateBeforeAreaRecalculation);
+ Profiler.EndSample();
+ astar.offMeshLinks.Refresh();
+
+ EnsureValidFloodFill();
+
+ Profiler.BeginSample("PostUpdate");
+ if (anyGraphsDirty) {
+ GraphModifier.TriggerEvent(GraphModifier.EventType.PostUpdate);
+ if (OnGraphsUpdated != null) OnGraphsUpdated();
+ }
+ Profiler.EndSample();
+ }
+ if (workRemaining) return false;
+ } finally {
+ lockObj.Unlock();
+ astar.data.UnlockGraphStructure();
+ workItemsInProgressRightNow = false;
+ }
+
+ // Reset flags at the end
+ anyGraphsDirty = false;
+ preUpdateEventSent = false;
+
+ workItemsInProgress = false;
+ return true;
+ }
+
+ /// <summary>
+ /// Process graph updating work items.
+ /// Process all queued work items, e.g graph updates and the likes.
+ ///
+ /// Returns:
+ /// - false if there are still items to be processed.
+ /// - true if the last work items was processed and pathfinding threads are ready to be resumed.
+ ///
+ /// This will not call <see cref="EnsureValidFloodFill"/> in contrast to <see cref="ProcessWorkItemsForUpdate"/>.
+ ///
+ /// See: <see cref="AstarPath.AddWorkItem"/>
+ /// </summary>
+ public bool ProcessWorkItemsForScan (bool force) {
+ return ProcessWorkItems(force, false);
+ }
+
+ /// <summary>
+ /// Process graph updating work items.
+ /// Process all queued work items, e.g graph updates and the likes.
+ ///
+ /// Returns:
+ /// - false if there are still items to be processed.
+ /// - true if the last work items was processed and pathfinding threads are ready to be resumed.
+ ///
+ /// See: <see cref="AstarPath.AddWorkItem"/>
+ ///
+ /// This method also calls GraphModifier.TriggerEvent(PostUpdate) if any graphs were dirtied.
+ /// It also calls <see cref="EnsureValidFloodFill"/> after the work items are done
+ /// </summary>
+ public bool ProcessWorkItemsForUpdate (bool force) {
+ return ProcessWorkItems(force, true);
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs.meta
new file mode 100644
index 0000000..e7cf323
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/WorkItemProcessor.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4236764d0fc1041abaac13b858be5118
+timeCreated: 1443114816
+licenseType: Store
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: