using UnityEngine; using System.Collections.Generic; namespace Pathfinding { /// /// 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 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 world units or if /// the GameObject has rotated enough so that the outmost point of the object has moved at least world units. /// /// This script works with both 2D colliders and normal 3D colliders. /// /// Note: This script works best with a GridGraph, PointGraph or LayerGridGraph /// You can use this with recast graphs as well. However since recast graph updates are much slower it is recommended to use the component if at all possible. /// /// See: AstarPath.UpdateGraphs /// See: graph-updates (view in online documentation for working links) /// See: navmeshcutting (view in online documentation for working links) /// [HelpURL("http://arongranberg.com/astar/docs/class_pathfinding_1_1_dynamic_grid_obstacle.php")] public class DynamicGridObstacle : GraphModifier { /// Collider to get bounds information from Collider coll; /// 2D Collider to get bounds information from Collider2D coll2D; /// Cached transform component Transform tr; /// The minimum change in world units along one of the axis of the bounding box of the collider to trigger a graph update public float updateError = 1; /// /// 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). /// public float checkTime = 0.2F; /// Bounds of the collider the last time the graphs were updated Bounds prevBounds; /// Rotation of the collider the last time the graphs were updated Quaternion prevRotation; /// True if the collider was enabled last time the graphs were updated bool prevEnabled; float lastCheckTime = -9999; Queue pendingGraphUpdates = new Queue(); Bounds bounds { get { if (coll != null) { return coll.bounds; } else { 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; } } } bool colliderEnabled { get { return coll != null ? coll.enabled : coll2D.enabled; } } protected override void Awake () { base.Awake(); coll = GetComponent(); coll2D = GetComponent(); 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("Removed collider from DynamicGridObstacle", 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(); } } } /// /// 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 /// 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(); } /// /// 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 AstarPath.active.FlushGraphUpdates() /// after the call to this method. /// 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; } /// Volume of a Bounds object. X*Y*Z static float BoundsVolume (Bounds b) { return System.Math.Abs(b.size.x * b.size.y * b.size.z); } } }