using UnityEngine;
using System.Collections.Generic;
using Pathfinding;
namespace Assets.Scripts.Server.Pathing
{
///
/// 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/documentation/beta/class_pathfinding_1_1_dynamic_grid_obstacle.php")]
public class DynamicGridPenalty : GraphModifier
{
private CharacterController _characterController;
/// Cached transform component
private 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;
public float centerYOffset = 0F;
public int penalty = 1000;
/// Bounds of the collider the last time the graphs were updated
private Bounds _prevBounds;
/// Rotation of the collider the last time the graphs were updated
private Quaternion _prevRotation;
/// True if the collider was enabled last time the graphs were updated
private bool _prevEnabled;
private float _lastCheckTime = -9999;
private readonly Queue _pendingGraphUpdates = new Queue();
private Bounds Bounds => _characterController.bounds;
///
/// these are the bounds after we've adjust the center to be shifted by
///
private Bounds ShiftedBounds => new Bounds(new Vector3(Bounds.center.x, Bounds.center.y + centerYOffset, Bounds.center.z), Bounds.size);
private bool ColliderEnabled => _characterController.enabled;
protected override void Awake()
{
base.Awake();
_characterController = GetComponentInParent() ?? GetComponentInChildren();
_tr = transform;
if (_characterController == null && Application.isPlaying)
{
throw new System.Exception("A CharacterController must be attached to the GameObject(" + gameObject.name + ") for the DynamicGridObstacle to work");
}
_prevBounds = ShiftedBounds;
_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 (_characterController == 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 (_characterController != null) _prevEnabled = ColliderEnabled;
}
void Update()
{
if (!Application.isPlaying) return;
if (_characterController == null)
{
Debug.LogError("No CharacterController 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 = ShiftedBounds;
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
/// after the call to this method.
///
public void DoUpdateGraphs()
{
if (_characterController == 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 = CreateGraphUpdateObject(_prevBounds);
_pendingGraphUpdates.Enqueue(guo);
AstarPath.active.UpdateGraphs(guo);
}
else
{
Bounds newBounds = ShiftedBounds;
Bounds merged = newBounds;
merged.Encapsulate(_prevBounds);
_oldMergedBounds = merged;
// 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 = CreateGraphUpdateObject(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 = CreateGraphUpdateObject(_prevBounds);
var guo2 = CreateGraphUpdateObject(newBounds);
_pendingGraphUpdates.Enqueue(guo1);
_pendingGraphUpdates.Enqueue(guo2);
AstarPath.active.UpdateGraphs(guo1);
AstarPath.active.UpdateGraphs(guo2);
}
_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;
}
private Bounds _oldMergedBounds;
private void OnDrawGizmos()
{
Gizmos.DrawWireCube(_oldMergedBounds.center, _oldMergedBounds.size);
}
private GraphUpdateObject CreateGraphUpdateObject(Bounds bounds)
{
return new GraphUpdateObject(bounds) { addPenalty = penalty, resetPenaltyOnPhysics = true, updateErosion = true, updatePhysics = true };
}
/// 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);
}
}
}