summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Utilities/DynamicGridObstacle.cs
blob: e6be96d49f51571eceda4e5417fdab436d3f7aac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
using UnityEngine;
using System.Collections.Generic;

namespace Pathfinding {
	/// <summary>
	/// Attach this script to any obstacle with a collider to enable dynamic updates of the graphs around it.
	/// When the object has moved or rotated at least <see cref="updateError"/> world units
	/// then it will call AstarPath.UpdateGraphs and update the graph around it.
	///
	/// Make sure that any children colliders do not extend beyond the bounds of the collider attached to the
	/// GameObject that the DynamicGridObstacle component is attached to since this script only updates the graph
	/// around the bounds of the collider on the same GameObject.
	///
	/// An update will be triggered whenever the bounding box of the attached collider has changed (moved/expanded/etc.) by at least <see cref="updateError"/> world units or if
	/// the GameObject has rotated enough so that the outmost point of the object has moved at least <see cref="updateError"/> world units.
	///
	/// This script works with both 2D colliders and normal 3D colliders.
	///
	/// Note: This script works with a GridGraph, PointGraph, LayerGridGraph or RecastGraph.
	/// However, for recast graphs, you can often use the <see cref="NavmeshCut"/> instead, for simple obstacles. The <see cref="NavmeshCut"/> can be faster, but it's not quite as flexible.
	///
	/// See: AstarPath.UpdateGraphs
	/// See: graph-updates (view in online documentation for working links)
	/// See: navmeshcutting (view in online documentation for working links)
	/// </summary>
	[AddComponentMenu("Pathfinding/Dynamic Grid Obstacle")]
	[HelpURL("https://arongranberg.com/astar/documentation/stable/dynamicgridobstacle.html")]
	public class DynamicGridObstacle : GraphModifier {
		/// <summary>Collider to get bounds information from</summary>
		Collider coll;

		/// <summary>2D Collider to get bounds information from</summary>
		Collider2D coll2D;

		/// <summary>Cached transform component</summary>
		Transform tr;

		/// <summary>The minimum change in world units along one of the axis of the bounding box of the collider to trigger a graph update</summary>
		public float updateError = 1;

		/// <summary>
		/// Time in seconds between bounding box checks.
		/// If AstarPath.batchGraphUpdates is enabled, it is not beneficial to have a checkTime much lower
		/// than AstarPath.graphUpdateBatchingInterval because that will just add extra unnecessary graph updates.
		///
		/// In real time seconds (based on Time.realtimeSinceStartup).
		/// </summary>
		public float checkTime = 0.2F;

		/// <summary>Bounds of the collider the last time the graphs were updated</summary>
		Bounds prevBounds;

		/// <summary>Rotation of the collider the last time the graphs were updated</summary>
		Quaternion prevRotation;

		/// <summary>True if the collider was enabled last time the graphs were updated</summary>
		bool prevEnabled;

		float lastCheckTime = -9999;
		Queue<GraphUpdateObject> pendingGraphUpdates = new Queue<GraphUpdateObject>();

		Bounds bounds {
			get {
				if (coll != null) {
					return coll.bounds;
				} else if (coll2D != null) {
					var b = coll2D.bounds;
					// Make sure the bounding box stretches close to infinitely along the Z axis (which is the axis perpendicular to the 2D plane).
					// We don't want any change along the Z axis to make a difference.
					b.extents += new Vector3(0, 0, 10000);
					return b;
				} else {
					return default;
				}
			}
		}

		bool colliderEnabled {
			get {
				return coll != null ? coll.enabled : coll2D.enabled;
			}
		}

		protected override void Awake () {
			base.Awake();

			coll = GetComponent<Collider>();
			coll2D = GetComponent<Collider2D>();
			tr = transform;
			if (coll == null && coll2D == null && Application.isPlaying) {
				throw new System.Exception("A collider or 2D collider must be attached to the GameObject(" + gameObject.name + ") for the DynamicGridObstacle to work");
			}

			prevBounds = bounds;
			prevRotation = tr.rotation;
			// Make sure we update the graph as soon as we find that the collider is enabled
			prevEnabled = false;
		}

		public override void OnPostScan () {
			// Make sure we find the collider
			// AstarPath.Awake may run before Awake on this component
			if (coll == null) Awake();

			// In case the object was in the scene from the start and the graphs
			// were scanned then we ignore the first update since it is unnecessary.
			if (coll != null) prevEnabled = colliderEnabled;
		}

		void Update () {
			if (!Application.isPlaying) return;

			if (coll == null && coll2D == null) {
				Debug.LogError("No collider attached to this GameObject. The DynamicGridObstacle component requires a collider.", this);
				enabled = false;
				return;
			}

			// Check if the previous graph updates have been completed yet.
			// We don't want to update the graph again until the last graph updates are done.
			// This is particularly important for recast graphs for which graph updates can take a long time.
			while (pendingGraphUpdates.Count > 0 && pendingGraphUpdates.Peek().stage != GraphUpdateStage.Pending) {
				pendingGraphUpdates.Dequeue();
			}

			if (AstarPath.active == null || AstarPath.active.isScanning || Time.realtimeSinceStartup - lastCheckTime < checkTime || !Application.isPlaying || pendingGraphUpdates.Count > 0) {
				return;
			}

			lastCheckTime = Time.realtimeSinceStartup;
			if (colliderEnabled) {
				// The current bounds of the collider
				Bounds newBounds = bounds;
				var newRotation = tr.rotation;

				Vector3 minDiff = prevBounds.min - newBounds.min;
				Vector3 maxDiff = prevBounds.max - newBounds.max;

				var extents = newBounds.extents.magnitude;
				// This is the distance that a point furthest out on the bounding box
				// would have moved due to the changed rotation of the object
				var errorFromRotation = extents*Quaternion.Angle(prevRotation, newRotation)*Mathf.Deg2Rad;

				// If the difference between the previous bounds and the new bounds is greater than some value, update the graphs
				if (minDiff.sqrMagnitude > updateError*updateError || maxDiff.sqrMagnitude > updateError*updateError ||
					errorFromRotation > updateError || !prevEnabled) {
					// Update the graphs as soon as possible
					DoUpdateGraphs();
				}
			} else {
				// Collider has just been disabled
				if (prevEnabled) {
					DoUpdateGraphs();
				}
			}
		}

		/// <summary>
		/// Revert graphs when disabled.
		/// When the DynamicObstacle is disabled or destroyed, a last graph update should be done to revert nodes to their original state
		/// </summary>
		protected override void OnDisable () {
			base.OnDisable();
			if (AstarPath.active != null && Application.isPlaying) {
				var guo = new GraphUpdateObject(prevBounds);
				pendingGraphUpdates.Enqueue(guo);
				AstarPath.active.UpdateGraphs(guo);
				prevEnabled = false;
			}

			// Stop caring about pending graph updates if this object is disabled.
			// This avoids a memory leak since `Update` will never be called again to remove pending updates
			// that have been completed.
			pendingGraphUpdates.Clear();
		}

		/// <summary>
		/// Update the graphs around this object.
		/// Note: The graphs will not be updated immediately since the pathfinding threads need to be paused first.
		/// If you want to guarantee that the graphs have been updated then call <see cref="AstarPath.FlushGraphUpdates"/>
		/// after the call to this method.
		/// </summary>
		public void DoUpdateGraphs () {
			if (coll == null && coll2D == null) return;

			// Required to ensure we get the most up to date bounding box from the physics engine
			UnityEngine.Physics.SyncTransforms();
			UnityEngine.Physics2D.SyncTransforms();

			if (!colliderEnabled) {
				// If the collider is not enabled, then col.bounds will empty
				// so just update prevBounds
				var guo = new GraphUpdateObject(prevBounds);
				pendingGraphUpdates.Enqueue(guo);
				AstarPath.active.UpdateGraphs(guo);
			} else {
				Bounds newBounds = bounds;

				Bounds merged = newBounds;
				merged.Encapsulate(prevBounds);

				// Check what seems to be fastest, to update the union of prevBounds and newBounds in a single request
				// or to update them separately, the smallest volume is usually the fastest
				if (BoundsVolume(merged) < BoundsVolume(newBounds) + BoundsVolume(prevBounds)) {
					// Send an update request to update the nodes inside the 'merged' volume
					var guo = new GraphUpdateObject(merged);
					pendingGraphUpdates.Enqueue(guo);
					AstarPath.active.UpdateGraphs(guo);
				} else {
					// Send two update request to update the nodes inside the 'prevBounds' and 'newBounds' volumes
					var guo1 = new GraphUpdateObject(prevBounds);
					var guo2 = new GraphUpdateObject(newBounds);
					pendingGraphUpdates.Enqueue(guo1);
					pendingGraphUpdates.Enqueue(guo2);
					AstarPath.active.UpdateGraphs(guo1);
					AstarPath.active.UpdateGraphs(guo2);
				}

#if ASTARDEBUG
				Debug.DrawLine(prevBounds.min, prevBounds.max, Color.yellow);
				Debug.DrawLine(newBounds.min, newBounds.max, Color.red);
#endif
				prevBounds = newBounds;
			}

			prevEnabled = colliderEnabled;
			prevRotation = tr.rotation;

			// Set this here as well since the DoUpdateGraphs method can be called from other scripts
			lastCheckTime = Time.realtimeSinceStartup;
		}

		/// <summary>Volume of a Bounds object. X*Y*Z</summary>
		static float BoundsVolume (Bounds b) {
			return System.Math.Abs(b.size.x * b.size.y * b.size.z);
		}
	}
}