diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core')
253 files changed, 39607 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI.meta new file mode 100644 index 0000000..38e138b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5ab9be352d07b44e68ad7c1a03eef3a5 diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIBase.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIBase.cs new file mode 100644 index 0000000..7fe54e1 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIBase.cs @@ -0,0 +1,930 @@ +using UnityEngine; +using System.Collections; +using UnityEngine.Serialization; +using Unity.Jobs; + +namespace Pathfinding { + using Pathfinding.RVO; + using Pathfinding.Util; + using Pathfinding.Jobs; + using Pathfinding.Drawing; + using UnityEngine.Jobs; + + /// <summary> + /// Base class for AIPath and RichAI. + /// This class holds various methods and fields that are common to both AIPath and RichAI. + /// + /// See: <see cref="Pathfinding.AIPath"/> + /// See: <see cref="Pathfinding.RichAI"/> + /// See: <see cref="Pathfinding.IAstarAI"/> (all movement scripts implement this interface) + /// </summary> + [RequireComponent(typeof(Seeker))] + public abstract class AIBase : VersionedMonoBehaviour { + /// <summary>\copydocref{IAstarAI.radius}</summary> + public float radius = 0.5f; + + /// <summary>\copydocref{IAstarAI.height}</summary> + public float height = 2; + + /// <summary> + /// Determines how often the agent will search for new paths (in seconds). + /// The agent will plan a new path to the target every N seconds. + /// + /// If you have fast moving targets or AIs, you might want to set it to a lower value. + /// + /// See: <see cref="shouldRecalculatePath"/> + /// See: <see cref="SearchPath"/> + /// + /// Deprecated: This has been renamed to <see cref="autoRepath.period"/>. + /// See: <see cref="AutoRepathPolicy"/> + /// </summary> + public float repathRate { + get { + return this.autoRepath.period; + } + set { + this.autoRepath.period = value; + } + } + + /// <summary> + /// \copydocref{IAstarAI::canSearch} + /// Deprecated: This has been superseded by <see cref="autoRepath.mode"/>. + /// </summary> + public bool canSearch { + get { + return this.autoRepath.mode != AutoRepathPolicy.Mode.Never; + } + set { + if (value) { + if (this.autoRepath.mode == AutoRepathPolicy.Mode.Never) { + this.autoRepath.mode = AutoRepathPolicy.Mode.EveryNSeconds; + } + } else { + this.autoRepath.mode = AutoRepathPolicy.Mode.Never; + } + } + } + + /// <summary>\copydocref{IAstarAI.canMove}</summary> + public bool canMove = true; + + /// <summary>Max speed in world units per second</summary> + [UnityEngine.Serialization.FormerlySerializedAs("speed")] + public float maxSpeed = 1; + + /// <summary> + /// Gravity to use. + /// If set to (NaN,NaN,NaN) then Physics.Gravity (configured in the Unity project settings) will be used. + /// If set to (0,0,0) then no gravity will be used and no raycast to check for ground penetration will be performed. + /// </summary> + public Vector3 gravity = new Vector3(float.NaN, float.NaN, float.NaN); + + /// <summary> + /// Layer mask to use for ground placement. + /// Make sure this does not include the layer of any colliders attached to this gameobject. + /// + /// See: <see cref="gravity"/> + /// See: https://docs.unity3d.com/Manual/Layers.html + /// </summary> + public LayerMask groundMask = -1; + + /// <summary> + /// Distance to the end point to consider the end of path to be reached. + /// + /// When the end of the path is within this distance then <see cref="IAstarAI.reachedEndOfPath"/> will return true. + /// When the <see cref="destination"/> is within this distance then <see cref="IAstarAI.reachedDestination"/> will return true. + /// + /// Note that the <see cref="destination"/> may not be reached just because the end of the path was reached. The <see cref="destination"/> may not be reachable at all. + /// + /// See: <see cref="IAstarAI.reachedEndOfPath"/> + /// See: <see cref="IAstarAI.reachedDestination"/> + /// </summary> + public float endReachedDistance = 0.2f; + + /// <summary> + /// What to do when within <see cref="endReachedDistance"/> units from the destination. + /// The character can either stop immediately when it comes within that distance, which is useful for e.g archers + /// or other ranged units that want to fire on a target. Or the character can continue to try to reach the exact + /// destination point and come to a full stop there. This is useful if you want the character to reach the exact + /// point that you specified. + /// + /// Note: <see cref="IAstarAI.reachedEndOfPath"/> will become true when the character is within <see cref="endReachedDistance"/> units from the destination + /// regardless of what this field is set to. + /// </summary> + public CloseToDestinationMode whenCloseToDestination = CloseToDestinationMode.Stop; + + /// <summary> + /// Controls if the agent slows down to a stop if the area around the destination is crowded. + /// + /// Using this module requires that local avoidance is used: i.e. that an RVOController is attached to the GameObject. + /// + /// See: <see cref="Pathfinding.RVO.RVODestinationCrowdedBehavior"/> + /// See: local-avoidance (view in online documentation for working links) + /// </summary> + public RVODestinationCrowdedBehavior rvoDensityBehavior = new RVODestinationCrowdedBehavior(true, 0.5f, false); + + /// <summary> + /// Offset along the Y coordinate for the ground raycast start position. + /// Normally the pivot of the character is at the character's feet, but you usually want to fire the raycast + /// from the character's center, so this value should be half of the character's height. + /// + /// A green gizmo line will be drawn upwards from the pivot point of the character to indicate where the raycast will start. + /// + /// See: <see cref="gravity"/> + /// Deprecated: Use the <see cref="height"/> property instead (2x this value) + /// </summary> + [System.Obsolete("Use the height property instead (2x this value)")] + public float centerOffset { + get { return height * 0.5f; } set { height = value * 2; } + } + + [SerializeField] + [HideInInspector] + [FormerlySerializedAs("centerOffset")] + float centerOffsetCompatibility = float.NaN; + + [SerializeField] + [HideInInspector] + [UnityEngine.Serialization.FormerlySerializedAs("repathRate")] + float repathRateCompatibility = float.NaN; + + [SerializeField] + [HideInInspector] + [UnityEngine.Serialization.FormerlySerializedAs("canSearch")] + [UnityEngine.Serialization.FormerlySerializedAs("repeatedlySearchPaths")] + bool canSearchCompability = false; + + /// <summary> + /// Determines which direction the agent moves in. + /// For 3D games you most likely want the ZAxisIsForward option as that is the convention for 3D games. + /// For 2D games you most likely want the YAxisIsForward option as that is the convention for 2D games. + /// + /// Using the YAxisForward option will also allow the agent to assume that the movement will happen in the 2D (XY) plane instead of the XZ plane + /// if it does not know. This is important only for the point graph which does not have a well defined up direction. The other built-in graphs (e.g the grid graph) + /// will all tell the agent which movement plane it is supposed to use. + /// + /// [Open online documentation to see images] + /// </summary> + [UnityEngine.Serialization.FormerlySerializedAs("rotationIn2D")] + public OrientationMode orientation = OrientationMode.ZAxisForward; + + /// <summary> + /// If true, the forward axis of the character will be along the Y axis instead of the Z axis. + /// + /// Deprecated: Use <see cref="orientation"/> instead + /// </summary> + [System.Obsolete("Use orientation instead")] + public bool rotationIn2D { + get { return orientation == OrientationMode.YAxisForward; } + set { orientation = value ? OrientationMode.YAxisForward : OrientationMode.ZAxisForward; } + } + + /// <summary> + /// If true, the AI will rotate to face the movement direction. + /// See: <see cref="orientation"/> + /// </summary> + public bool enableRotation = true; + + /// <summary> + /// Position of the agent. + /// If <see cref="updatePosition"/> is true then this value will be synchronized every frame with Transform.position. + /// </summary> + protected Vector3 simulatedPosition; + + /// <summary> + /// Rotation of the agent. + /// If <see cref="updateRotation"/> is true then this value will be synchronized every frame with Transform.rotation. + /// </summary> + protected Quaternion simulatedRotation; + + /// <summary> + /// Position of the agent. + /// In world space. + /// If <see cref="updatePosition"/> is true then this value is idential to transform.position. + /// See: <see cref="Teleport"/> + /// See: <see cref="Move"/> + /// </summary> + public Vector3 position { get { return updatePosition ? tr.position : simulatedPosition; } } + + /// <summary> + /// Rotation of the agent. + /// If <see cref="updateRotation"/> is true then this value is identical to transform.rotation. + /// </summary> + public virtual Quaternion rotation { + get { return updateRotation ? tr.rotation : simulatedRotation; } + set { + if (updateRotation) { + tr.rotation = value; + } else { + simulatedRotation = value; + } + } + } + + /// <summary>Accumulated movement deltas from the <see cref="Move"/> method</summary> + protected Vector3 accumulatedMovementDelta = Vector3.zero; + + /// <summary> + /// Current desired velocity of the agent (does not include local avoidance and physics). + /// Lies in the movement plane. + /// </summary> + protected Vector2 velocity2D; + + /// <summary> + /// Velocity due to gravity. + /// Perpendicular to the movement plane. + /// + /// When the agent is grounded this may not accurately reflect the velocity of the agent. + /// It may be non-zero even though the agent is not moving. + /// </summary> + protected float verticalVelocity; + + /// <summary>Cached Seeker component</summary> + protected Seeker seeker; + + /// <summary>Cached Transform component</summary> + protected Transform tr; + + /// <summary>Cached Rigidbody component</summary> + protected Rigidbody rigid; + + /// <summary>Cached Rigidbody component</summary> + protected Rigidbody2D rigid2D; + + /// <summary>Cached CharacterController component</summary> + protected CharacterController controller; + + /// <summary>Cached RVOController component</summary> + protected RVOController rvoController; + + /// <summary> + /// Plane which this agent is moving in. + /// This is used to convert between world space and a movement plane to make it possible to use this script in + /// both 2D games and 3D games. + /// </summary> + public SimpleMovementPlane movementPlane = new SimpleMovementPlane(Quaternion.identity); + + /// <summary> + /// Determines if the character's position should be coupled to the Transform's position. + /// If false then all movement calculations will happen as usual, but the object that this component is attached to will not move + /// instead only the <see cref="position"/> property will change. + /// + /// This is useful if you want to control the movement of the character using some other means such + /// as for example root motion but still want the AI to move freely. + /// See: Combined with calling <see cref="MovementUpdate"/> from a separate script instead of it being called automatically one can take a similar approach to what is documented here: https://docs.unity3d.com/Manual/nav-CouplingAnimationAndNavigation.html + /// + /// See: <see cref="canMove"/> which in contrast to this field will disable all movement calculations. + /// See: <see cref="updateRotation"/> + /// </summary> + [System.NonSerialized] + public bool updatePosition = true; + + /// <summary> + /// Determines if the character's rotation should be coupled to the Transform's rotation. + /// If false then all movement calculations will happen as usual, but the object that this component is attached to will not rotate + /// instead only the <see cref="rotation"/> property will change. + /// + /// See: <see cref="updatePosition"/> + /// </summary> + [System.NonSerialized] + public bool updateRotation = true; + + /// <summary> + /// Determines how the agent recalculates its path automatically. + /// This corresponds to the settings under the "Recalculate Paths Automatically" field in the inspector. + /// </summary> + public AutoRepathPolicy autoRepath = new AutoRepathPolicy(); + + /// <summary>Indicates if gravity is used during this frame</summary> + protected bool usingGravity { get; set; } + + /// <summary>Delta time used for movement during the last frame</summary> + protected float lastDeltaTime; + + /// <summary>Position of the character at the end of the last frame</summary> + protected Vector3 prevPosition1; + + /// <summary>Position of the character at the end of the frame before the last frame</summary> + protected Vector3 prevPosition2; + + /// <summary>Amount which the character wants or tried to move with during the last frame</summary> + protected Vector2 lastDeltaPosition; + + /// <summary>Only when the previous path has been calculated should the script consider searching for a new path</summary> + protected bool waitingForPathCalculation = false; + + /// <summary>Time when the last path request was started</summary> + protected float lastRepath = float.NegativeInfinity; + + [UnityEngine.Serialization.FormerlySerializedAs("target")][SerializeField][HideInInspector] + Transform targetCompatibility; + + /// <summary> + /// True if the Start method has been executed. + /// Used to test if coroutines should be started in OnEnable to prevent calculating paths + /// in the awake stage (or rather before start on frame 0). + /// </summary> + protected bool startHasRun = false; + + /// <summary> + /// Target to move towards. + /// The AI will try to follow/move towards this target. + /// It can be a point on the ground where the player has clicked in an RTS for example, or it can be the player object in a zombie game. + /// + /// Deprecated: In 4.1 this will automatically add a <see cref="Pathfinding.AIDestinationSetter"/> component and set the target on that component. + /// Try instead to use the <see cref="destination"/> property which does not require a transform to be created as the target or use + /// the AIDestinationSetter component directly. + /// </summary> + [System.Obsolete("Use the destination property or the AIDestinationSetter component instead")] + public Transform target { + get { + return TryGetComponent(out AIDestinationSetter setter) ? setter.target : null; + } + set { + targetCompatibility = null; + if (!TryGetComponent(out AIDestinationSetter setter)) setter = gameObject.AddComponent<AIDestinationSetter>(); + setter.target = value; + destination = value != null ? value.position : new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + } + } + + /// <summary>Backing field for <see cref="destination"/></summary> + Vector3 destinationBackingField = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + + /// <summary>\copydocref{IAstarAI.destination}</summary> + public Vector3 destination { + get { return destinationBackingField; } + set { + // Note: vector3 equality operator will return false if both are (inf,inf,inf). So do the extra check to see if both are infinity. + if (rvoDensityBehavior.enabled && !(value == destinationBackingField || (float.IsPositiveInfinity(value.x) && float.IsPositiveInfinity(destinationBackingField.x)))) { + destinationBackingField = value; + rvoDensityBehavior.OnDestinationChanged(value, reachedDestination); + } else { + destinationBackingField = value; + } + } + } + + /// <summary>\copydocref{IAstarAI.velocity}</summary> + public Vector3 velocity { + get { + return lastDeltaTime > 0.000001f ? (prevPosition1 - prevPosition2) / lastDeltaTime : Vector3.zero; + } + } + + /// <summary>\copydocref{IAstarAI.desiredVelocity}</summary> + public Vector3 desiredVelocity { + get { return lastDeltaTime > 0.00001f ? movementPlane.ToWorld(lastDeltaPosition / lastDeltaTime, verticalVelocity) : Vector3.zero; } + } + + /// <summary>\copydocref{IAstarAI.desiredVelocityWithoutLocalAvoidance}</summary> + public Vector3 desiredVelocityWithoutLocalAvoidance { + get { return movementPlane.ToWorld(velocity2D, verticalVelocity); } + set { velocity2D = movementPlane.ToPlane(value, out verticalVelocity); } + } + + /// <summary>\copydocref{IAstarAI.endOfPath}</summary> + public abstract Vector3 endOfPath { get; } + + /// <summary>\copydocref{IAstarAI.reachedDestination}</summary> + public abstract bool reachedDestination { get; } + + /// <summary>\copydocref{IAstarAI.isStopped}</summary> + public bool isStopped { get; set; } + + /// <summary>\copydocref{IAstarAI.onSearchPath}</summary> + public System.Action onSearchPath { get; set; } + + /// <summary> + /// Cached delegate for the <see cref="OnPathComplete"/> method. + /// + /// Caching this avoids allocating a new one every time a path is calculated, which reduces GC pressure. + /// </summary> + protected OnPathDelegate onPathComplete; + + /// <summary>True if the path should be automatically recalculated as soon as possible</summary> + protected virtual bool shouldRecalculatePath { + get { + return !waitingForPathCalculation && autoRepath.ShouldRecalculatePath(position, radius, destination, Time.time); + } + } + + /// <summary> + /// Looks for any attached components like RVOController and CharacterController etc. + /// + /// This is done during <see cref="OnEnable"/>. If you are adding/removing components during runtime you may want to call this function + /// to make sure that this script finds them. It is unfortunately prohibitive from a performance standpoint to look for components every frame. + /// </summary> + public virtual void FindComponents () { + tr = transform; + // GetComponent is a bit slow, so only call it if we don't know about the component already. + // This is important when selecting a lot of objects in the editor as OnDrawGizmos will call + // this method every frame when outside of play mode. + if (!seeker) TryGetComponent(out seeker); + if (!rvoController) TryGetComponent(out rvoController); + // Find attached movement components + if (!controller) TryGetComponent(out controller); + if (!rigid) TryGetComponent(out rigid); + if (!rigid2D) TryGetComponent(out rigid2D); + } + + /// <summary>Called when the component is enabled</summary> + protected virtual void OnEnable () { + FindComponents(); + onPathComplete = OnPathComplete; + Init(); + + // When using rigidbodies all movement is done inside FixedUpdate instead of Update + bool fixedUpdate = rigid != null || rigid2D != null; + BatchedEvents.Add(this, fixedUpdate ? BatchedEvents.Event.FixedUpdate : BatchedEvents.Event.Update, OnUpdate); + } + + /// <summary> + /// Called every frame. + /// This may be called during FixedUpdate or Update depending on if a rigidbody is attached to the GameObject. + /// </summary> + static void OnUpdate (AIBase[] components, int count, TransformAccessArray transforms, BatchedEvents.Event ev) { + // Sync transforms to ensure raycasts will hit the correct colliders + Physics.SyncTransforms(); + Physics2D.SyncTransforms(); + + float dt = ev == BatchedEvents.Event.FixedUpdate ? Time.fixedDeltaTime : Time.deltaTime; + + var simulator = RVOSimulator.active?.GetSimulator(); + if (simulator != null) { + int agentsWithRVOControllers = 0; + for (int i = 0; i < count; i++) agentsWithRVOControllers += (components[i].rvoController != null && components[i].rvoController.enabled ? 1 : 0); + RVODestinationCrowdedBehavior.JobDensityCheck densityJobData = new RVODestinationCrowdedBehavior.JobDensityCheck(agentsWithRVOControllers, dt); + + for (int i = 0, j = 0; i < count; i++) { + var agent = components[i]; + if (agent.rvoController != null && agent.rvoController.enabled) { + densityJobData.Set(j, agent.rvoController.rvoAgent.AgentIndex, agent.endOfPath, agent.rvoDensityBehavior.densityThreshold, agent.rvoDensityBehavior.progressAverage); + j++; + } + } + var densityJob = densityJobData.ScheduleBatch(agentsWithRVOControllers, agentsWithRVOControllers / 16, simulator.lastJob); + densityJob.Complete(); + + for (int i = 0, j = 0; i < count; i++) { + var agent = components[i]; + if (agent.rvoController != null && agent.rvoController.enabled) { + agent.rvoDensityBehavior.ReadJobResult(ref densityJobData, j); + j++; + } + } + + densityJobData.Dispose(); + } + + for (int i = 0; i < count; i++) { + var agent = components[i]; + UnityEngine.Profiling.Profiler.BeginSample("OnUpdate"); + agent.OnUpdate(dt); + UnityEngine.Profiling.Profiler.EndSample(); + } + + if (count > 0 && components[0] is AIPathAlignedToSurface) { + AIPathAlignedToSurface.UpdateMovementPlanes(components as AIPathAlignedToSurface[], count); + } + + Physics.SyncTransforms(); + Physics2D.SyncTransforms(); + } + + /// <summary>Called every frame</summary> + protected virtual void OnUpdate (float dt) { + // If gravity is used depends on a lot of things. + // For example when a non-kinematic rigidbody is used then the rigidbody will apply the gravity itself + // Note that the gravity can contain NaN's, which is why the comparison uses !(a==b) instead of just a!=b. + usingGravity = !(gravity == Vector3.zero) && (!updatePosition || ((rigid == null || rigid.isKinematic) && (rigid2D == null || rigid2D.isKinematic))); + + if (shouldRecalculatePath) SearchPath(); + + if (canMove) { + MovementUpdate(dt, out var nextPosition, out var nextRotation); + UnityEngine.Profiling.Profiler.BeginSample("Finalize"); + FinalizeMovement(nextPosition, nextRotation); + UnityEngine.Profiling.Profiler.EndSample(); + } + } + + /// <summary> + /// Starts searching for paths. + /// If you override this method you should in most cases call base.Start () at the start of it. + /// See: <see cref="Init"/> + /// </summary> + protected virtual void Start () { + startHasRun = true; + Init(); + } + + void Init () { + if (startHasRun) { + // Clamp the agent to the navmesh (which is what the Teleport call will do essentially. Though only some movement scripts require this, like RichAI). + // The Teleport call will also make sure some variables are properly initialized (like #prevPosition1 and #prevPosition2) + if (canMove) Teleport(position, false); + autoRepath.Reset(); + if (shouldRecalculatePath) SearchPath(); + } + } + + /// <summary>\copydocref{IAstarAI.Teleport}</summary> + public virtual void Teleport (Vector3 newPosition, bool clearPath = true) { + if (clearPath) ClearPath(); + prevPosition1 = prevPosition2 = simulatedPosition = newPosition; + if (updatePosition) tr.position = newPosition; + if (rvoController != null) rvoController.Move(Vector3.zero); + if (clearPath) SearchPath(); + } + + protected void CancelCurrentPathRequest () { + waitingForPathCalculation = false; + // Abort calculation of the current path + if (seeker != null) seeker.CancelCurrentPathRequest(); + } + + protected virtual void OnDisable () { + BatchedEvents.Remove(this); + ClearPath(); + + velocity2D = Vector3.zero; + accumulatedMovementDelta = Vector3.zero; + verticalVelocity = 0f; + lastDeltaTime = 0; + } + + /// <summary>\copydocref{IAstarAI.MovementUpdate}</summary> + public void MovementUpdate (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) { + lastDeltaTime = deltaTime; + MovementUpdateInternal(deltaTime, out nextPosition, out nextRotation); + } + + /// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary> + protected abstract void MovementUpdateInternal(float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation); + + /// <summary> + /// Outputs the start point and end point of the next automatic path request. + /// This is a separate method to make it easy for subclasses to swap out the endpoints + /// of path requests. For example the <see cref="LocalSpaceRichAI"/> script which requires the endpoints + /// to be transformed to graph space first. + /// </summary> + protected virtual void CalculatePathRequestEndpoints (out Vector3 start, out Vector3 end) { + start = GetFeetPosition(); + end = destination; + } + + /// <summary>\copydocref{IAstarAI.SearchPath}</summary> + public virtual void SearchPath () { + if (float.IsPositiveInfinity(destination.x)) return; + if (onSearchPath != null) onSearchPath(); + + // Find out where we are and where we are going + Vector3 start, end; + CalculatePathRequestEndpoints(out start, out end); + + // Request a path to be calculated from our current position to the destination + ABPath p = ABPath.Construct(start, end, null); + SetPath(p, false); + } + + /// <summary> + /// Position of the base of the character. + /// This is used for pathfinding as the character's pivot point is sometimes placed + /// at the center of the character instead of near the feet. In a building with multiple floors + /// the center of a character may in some scenarios be closer to the navmesh on the floor above + /// than to the floor below which could cause an incorrect path to be calculated. + /// To solve this the start point of the requested paths is always at the base of the character. + /// </summary> + public virtual Vector3 GetFeetPosition () { + return position; + } + + /// <summary>Called when a requested path has been calculated</summary> + protected abstract void OnPathComplete(Path newPath); + + /// <summary> + /// Clears the current path of the agent. + /// + /// Usually invoked using SetPath(null). + /// + /// See: <see cref="SetPath"/> + /// See: <see cref="isStopped"/> + /// </summary> + protected abstract void ClearPath(); + + /// <summary>\copydocref{IAstarAI.SetPath}</summary> + public void SetPath (Path path, bool updateDestinationFromPath = true) { + if (updateDestinationFromPath && path is ABPath abPath && abPath.endPointKnownBeforeCalculation) { + this.destination = abPath.originalEndPoint; + } + + if (path == null) { + CancelCurrentPathRequest(); + ClearPath(); + } else if (path.PipelineState == PathState.Created) { + // Path has not started calculation yet + waitingForPathCalculation = true; + seeker.CancelCurrentPathRequest(); + seeker.StartPath(path, onPathComplete); + autoRepath.DidRecalculatePath(destination, Time.time); + } else if (path.PipelineState >= PathState.Returning) { + // Path has already been calculated + + // We might be calculating another path at the same time, and we don't want that path to override this one. So cancel it. + if (seeker.GetCurrentPath() != path) seeker.CancelCurrentPathRequest(); + + OnPathComplete(path); + } else { + // Path calculation has been started, but it is not yet complete. Cannot really handle this. + throw new System.ArgumentException("You must call the SetPath method with a path that either has been completely calculated or one whose path calculation has not been started at all. It looks like the path calculation for the path you tried to use has been started, but is not yet finished."); + } + } + + /// <summary> + /// Accelerates the agent downwards. + /// See: <see cref="verticalVelocity"/> + /// See: <see cref="gravity"/> + /// </summary> + protected virtual void ApplyGravity (float deltaTime) { + // Apply gravity + if (usingGravity) { + float verticalGravity; + velocity2D += movementPlane.ToPlane(deltaTime * (float.IsNaN(gravity.x) ? Physics.gravity : gravity), out verticalGravity); + verticalVelocity += verticalGravity; + } else { + verticalVelocity = 0; + } + } + + /// <summary>Calculates how far to move during a single frame</summary> + protected Vector2 CalculateDeltaToMoveThisFrame (Vector3 position, float distanceToEndOfPath, float deltaTime) { + if (rvoController != null && rvoController.enabled) { + // Use RVOController to get a processed delta position + // such that collisions will be avoided if possible + return movementPlane.ToPlane(rvoController.CalculateMovementDelta(position, deltaTime)); + } + // Direction and distance to move during this frame + return Vector2.ClampMagnitude(velocity2D * deltaTime, distanceToEndOfPath); + } + + /// <summary> + /// Simulates rotating the agent towards the specified direction and returns the new rotation. + /// + /// Note that this only calculates a new rotation, it does not change the actual rotation of the agent. + /// Useful when you are handling movement externally using <see cref="FinalizeMovement"/> but you want to use the built-in rotation code. + /// + /// See: <see cref="orientation"/> + /// </summary> + /// <param name="direction">Direction in world space to rotate towards.</param> + /// <param name="maxDegrees">Maximum number of degrees to rotate this frame.</param> + public Quaternion SimulateRotationTowards (Vector3 direction, float maxDegrees) { + return SimulateRotationTowards(movementPlane.ToPlane(direction), maxDegrees, maxDegrees); + } + + /// <summary> + /// Simulates rotating the agent towards the specified direction and returns the new rotation. + /// + /// Note that this only calculates a new rotation, it does not change the actual rotation of the agent. + /// + /// See: <see cref="orientation"/> + /// See: <see cref="movementPlane"/> + /// </summary> + /// <param name="direction">Direction in the movement plane to rotate towards.</param> + /// <param name="maxDegreesMainAxis">Maximum number of degrees to rotate this frame around the character's main axis. This is rotating left and right as a character normally does.</param> + /// <param name="maxDegreesOffAxis">Maximum number of degrees to rotate this frame around other axes. This is used to ensure the character's up direction is correct. + /// It is only used for non-planar worlds where the up direction changes depending on the position of the character. + /// More precisely a faster code path which ignores this parameter is used whenever the current #movementPlane is exactly the XZ or XY plane. + /// This must be at least as large as maxDegreesMainAxis.</param> + protected Quaternion SimulateRotationTowards (Vector2 direction, float maxDegreesMainAxis, float maxDegreesOffAxis = float.PositiveInfinity) { + Quaternion targetRotation; + + if (movementPlane.isXY || movementPlane.isXZ) { + if (direction == Vector2.zero) return simulatedRotation; + + // Common fast path. + // A standard XY or XZ movement plane indicates that the character is moving in a normal planar world. + // We will use a much faster code path for this case since we don't have to deal with changing the 'up' direction of the character all the time. + // This code path mostly works for non-planar worlds too, but it will fail in some cases. + // In particular it will not be able to adjust the up direction of the character while it is standing still (because then a zero maxDegreesMainAxis is usually passed). + // That case may be important, especially when the character has just been spawned and does not have a destination yet. + targetRotation = Quaternion.LookRotation(movementPlane.ToWorld(direction, 0), movementPlane.ToWorld(Vector2.zero, 1)); + maxDegreesOffAxis = maxDegreesMainAxis; + } else { + // Decompose the rotation into two parts: a rotation around the main axis of the character, and a rotation around the other axes. + // Then limit the rotation speed along those two components separately. + var forwardInPlane = movementPlane.ToPlane(rotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward)); + + // Can happen if the character is perpendicular to the plane + if (forwardInPlane == Vector2.zero) forwardInPlane = Vector2.right; + + var rotationVectorAroundMainAxis = VectorMath.ComplexMultiplyConjugate(direction, forwardInPlane); + + // Note: If the direction is zero, then angle will also be zero since atan2(0,0) = 0 + var angle = Mathf.Atan2(rotationVectorAroundMainAxis.y, rotationVectorAroundMainAxis.x) * Mathf.Rad2Deg; + var rotationAroundMainAxis = Quaternion.AngleAxis(-Mathf.Min(Mathf.Abs(angle), maxDegreesMainAxis) * Mathf.Sign(angle), Vector3.up); + + targetRotation = Quaternion.LookRotation(movementPlane.ToWorld(forwardInPlane, 0), movementPlane.ToWorld(Vector2.zero, 1)); + targetRotation = targetRotation * rotationAroundMainAxis; + } + + // This causes the character to only rotate around the Z axis + if (orientation == OrientationMode.YAxisForward) targetRotation *= Quaternion.Euler(90, 0, 0); + return Quaternion.RotateTowards(simulatedRotation, targetRotation, maxDegreesOffAxis); + } + + /// <summary>\copydocref{IAstarAI.Move}</summary> + public virtual void Move (Vector3 deltaPosition) { + accumulatedMovementDelta += deltaPosition; + } + + /// <summary> + /// Moves the agent to a position. + /// + /// This is used if you want to override how the agent moves. For example if you are using + /// root motion with Mecanim. + /// + /// This will use a CharacterController, Rigidbody, Rigidbody2D or the Transform component depending on what options + /// are available. + /// + /// The agent will be clamped to the navmesh after the movement (if such information is available, generally this is only done by the RichAI component). + /// + /// See: <see cref="MovementUpdate"/> for some example code. + /// See: <see cref="controller"/>, <see cref="rigid"/>, <see cref="rigid2D"/> + /// </summary> + /// <param name="nextPosition">New position of the agent.</param> + /// <param name="nextRotation">New rotation of the agent. If #enableRotation is false then this parameter will be ignored.</param> + public virtual void FinalizeMovement (Vector3 nextPosition, Quaternion nextRotation) { + if (enableRotation) FinalizeRotation(nextRotation); + FinalizePosition(nextPosition); + } + + void FinalizeRotation (Quaternion nextRotation) { + simulatedRotation = nextRotation; + if (updateRotation) { + if (rigid != null) rigid.MoveRotation(nextRotation); + else if (rigid2D != null) rigid2D.MoveRotation(nextRotation.eulerAngles.z); + else tr.rotation = nextRotation; + } + } + + void FinalizePosition (Vector3 nextPosition) { + // Use a local variable, it is significantly faster + Vector3 currentPosition = simulatedPosition; + bool positionDirty1 = false; + + if (controller != null && controller.enabled && updatePosition) { + // Use CharacterController + // The Transform may not be at #position if it was outside the navmesh and had to be moved to the closest valid position + tr.position = currentPosition; + controller.Move((nextPosition - currentPosition) + accumulatedMovementDelta); + // Grab the position after the movement to be able to take physics into account + // TODO: Add this into the clampedPosition calculation below to make RVO better respond to physics + currentPosition = tr.position; + if (controller.isGrounded) verticalVelocity = 0; + } else { + // Use Transform, Rigidbody, Rigidbody2D or nothing at all (if updatePosition = false) + float lastElevation; + movementPlane.ToPlane(currentPosition, out lastElevation); + currentPosition = nextPosition + accumulatedMovementDelta; + + // Position the character on the ground + if (usingGravity) currentPosition = RaycastPosition(currentPosition, lastElevation); + positionDirty1 = true; + } + + // Clamp the position to the navmesh after movement is done + bool positionDirty2 = false; + currentPosition = ClampToNavmesh(currentPosition, out positionDirty2); + + // Assign the final position to the character if we haven't already set it (mostly for performance, setting the position can be slow) + if ((positionDirty1 || positionDirty2) && updatePosition) { + // Note that rigid.MovePosition may or may not move the character immediately. + // Check the Unity documentation for the special cases. + if (rigid != null) rigid.MovePosition(currentPosition); + else if (rigid2D != null) rigid2D.MovePosition(currentPosition); + else tr.position = currentPosition; + } + + accumulatedMovementDelta = Vector3.zero; + simulatedPosition = currentPosition; + UpdateVelocity(); + } + + protected void UpdateVelocity () { + prevPosition2 = prevPosition1; + prevPosition1 = position; + } + + /// <summary> + /// Constrains the character's position to lie on the navmesh. + /// Not all movement scripts have support for this. + /// + /// Returns: New position of the character that has been clamped to the navmesh. + /// </summary> + /// <param name="position">Current position of the character.</param> + /// <param name="positionChanged">True if the character's position was modified by this method.</param> + protected virtual Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) { + positionChanged = false; + return position; + } + + /// <summary> + /// Hit info from the last raycast done for ground placement. + /// Will not update unless gravity is used (if no gravity is used, then raycasts are disabled). + /// + /// See: <see cref="RaycastPosition"/> + /// </summary> + protected RaycastHit lastRaycastHit; + + /// <summary> + /// Checks if the character is grounded and prevents ground penetration. + /// + /// Sets <see cref="verticalVelocity"/> to zero if the character is grounded. + /// + /// Returns: The new position of the character. + /// </summary> + /// <param name="position">Position of the character in the world.</param> + /// <param name="lastElevation">Elevation coordinate before the agent was moved. This is along the 'up' axis of the #movementPlane.</param> + protected Vector3 RaycastPosition (Vector3 position, float lastElevation) { + float elevation; + + movementPlane.ToPlane(position, out elevation); + float rayLength = tr.localScale.y * height * 0.5f + Mathf.Max(0, lastElevation-elevation); + Vector3 rayOffset = movementPlane.ToWorld(Vector2.zero, rayLength); + + if (Physics.Raycast(position + rayOffset, -rayOffset, out lastRaycastHit, rayLength, groundMask, QueryTriggerInteraction.Ignore)) { + // Grounded + // Make the vertical velocity fall off exponentially. This is reasonable from a physical standpoint as characters + // are not completely stiff and touching the ground will not immediately negate all velocity downwards. The AI will + // stop moving completely due to the raycast penetration test but it will still *try* to move downwards. This helps + // significantly when moving down along slopes as if the vertical velocity would be set to zero when the character + // was grounded it would lead to a kind of 'bouncing' behavior (try it, it's hard to explain). Ideally this should + // use a more physically correct formula but this is a good approximation and is much more performant. The constant + // '5' in the expression below determines how quickly it converges but high values can lead to too much noise. + verticalVelocity *= System.Math.Max(0, 1 - 5 * lastDeltaTime); + return lastRaycastHit.point; + } + return position; + } + + protected virtual void OnDrawGizmosSelected () { + // When selected in the Unity inspector it's nice to make the component react instantly if + // any other components are attached/detached or enabled/disabled. + // We don't want to do this normally every frame because that would be expensive. + if (Application.isPlaying) FindComponents(); + } + + public static readonly Color ShapeGizmoColor = new Color(240/255f, 213/255f, 30/255f); + + public override void DrawGizmos () { + if (!Application.isPlaying || !enabled || tr == null) FindComponents(); + + var color = ShapeGizmoColor; + if (rvoController != null && rvoController.locked) color *= 0.5f; + if (orientation == OrientationMode.YAxisForward) { + Draw.WireCylinder(position, Vector3.forward, 0, radius * tr.localScale.x, color); + } else { + Draw.WireCylinder(position, rotation * Vector3.up, tr.localScale.y * height, radius * tr.localScale.x, color); + } + + if (!float.IsPositiveInfinity(destination.x) && Application.isPlaying) Draw.Circle(destination, movementPlane.rotation * Vector3.up, 0.2f, Color.blue); + + autoRepath.DrawGizmos(Draw.editor, position, radius, new NativeMovementPlane(movementPlane.rotation)); + } + + protected override void Reset () { + ResetShape(); + base.Reset(); + } + + void ResetShape () { + if (TryGetComponent(out CharacterController cc)) { + radius = cc.radius; + height = Mathf.Max(radius*2, cc.height); + } + } + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + if (migrations.TryMigrateFromLegacyFormat(out var legacyVersion)) { + // Version == 5 is the 4.2 branch. + if (legacyVersion <= 2 || legacyVersion == 5) rvoDensityBehavior.enabled = false; + + if (legacyVersion <= 3) { + repathRate = repathRateCompatibility; + canSearch = canSearchCompability; + } + } + if (unityThread && !float.IsNaN(centerOffsetCompatibility)) { + height = centerOffsetCompatibility*2; + ResetShape(); + if (TryGetComponent(out RVOController rvo)) radius = rvo.radiusBackingField; + centerOffsetCompatibility = float.NaN; + } + #pragma warning disable 618 + if (unityThread && targetCompatibility != null) target = targetCompatibility; + #pragma warning restore 618 + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIBase.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIBase.cs.meta new file mode 100644 index 0000000..b9f18be --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIBase.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: a08f67bbe580e4ddfaebd06363c9cc97 +timeCreated: 1496932372 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 100 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AILerp.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AILerp.cs new file mode 100644 index 0000000..681420d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AILerp.cs @@ -0,0 +1,770 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; + +namespace Pathfinding { + using Pathfinding.Util; + + /// <summary> + /// Linearly interpolating movement script. + /// + /// This movement script will follow the path exactly, it uses linear interpolation to move between the waypoints in the path. + /// This is desirable for some types of games. + /// It also works in 2D. + /// + /// See: You can see an example of this script in action in the example scene called Example15_2D. + /// + /// \section rec Configuration + /// \subsection rec-snapped Recommended setup for movement along connections + /// + /// This depends on what type of movement you are aiming for. + /// If you are aiming for movement where the unit follows the path exactly and move only along the graph connections on a grid/point graph. + /// I recommend that you adjust the StartEndModifier on the Seeker component: set the 'Start Point Snapping' field to 'NodeConnection' and the 'End Point Snapping' field to 'SnapToNode'. + /// [Open online documentation to see images] + /// [Open online documentation to see images] + /// + /// \subsection rec-smooth Recommended setup for smooth movement + /// If you on the other hand want smoother movement I recommend setting 'Start Point Snapping' and 'End Point Snapping' to 'ClosestOnNode' and to add the Simple Smooth Modifier to the GameObject as well. + /// Alternatively you can use the <see cref="Pathfinding.FunnelModifier Funnel"/> which works better on navmesh/recast graphs or the <see cref="Pathfinding.RaycastModifier"/>. + /// + /// You should not combine the Simple Smooth Modifier or the Funnel Modifier with the NodeConnection snapping mode. This may lead to very odd behavior. + /// + /// [Open online documentation to see images] + /// [Open online documentation to see images] + /// You may also want to tweak the <see cref="rotationSpeed"/>. + /// </summary> + [RequireComponent(typeof(Seeker))] + [AddComponentMenu("Pathfinding/AI/AILerp (2D,3D)")] + [UniqueComponent(tag = "ai")] + [HelpURL("https://arongranberg.com/astar/documentation/stable/ailerp.html")] + public class AILerp : VersionedMonoBehaviour, IAstarAI { + /// <summary> + /// Determines how often it will search for new paths. + /// If you have fast moving targets or AIs, you might want to set it to a lower value. + /// The value is in seconds between path requests. + /// + /// Deprecated: This has been renamed to <see cref="autoRepath.period"/>. + /// See: <see cref="AutoRepathPolicy"/> + /// </summary> + public float repathRate { + get { + return this.autoRepath.period; + } + set { + this.autoRepath.period = value; + } + } + + /// <summary> + /// \copydoc Pathfinding::IAstarAI::canSearch + /// Deprecated: This has been superseded by <see cref="autoRepath.mode"/>. + /// </summary> + public bool canSearch { + get { + return this.autoRepath.mode != AutoRepathPolicy.Mode.Never; + } + set { + this.autoRepath.mode = value ? AutoRepathPolicy.Mode.EveryNSeconds : AutoRepathPolicy.Mode.Never; + } + } + + /// <summary> + /// Determines how the agent recalculates its path automatically. + /// This corresponds to the settings under the "Recalculate Paths Automatically" field in the inspector. + /// </summary> + public AutoRepathPolicy autoRepath = new AutoRepathPolicy(); + + /// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary> + public bool canMove = true; + + /// <summary>Speed in world units</summary> + public float speed = 3; + + /// <summary> + /// Determines which direction the agent moves in. + /// For 3D games you most likely want the ZAxisIsForward option as that is the convention for 3D games. + /// For 2D games you most likely want the YAxisIsForward option as that is the convention for 2D games. + /// + /// Using the YAxisForward option will also allow the agent to assume that the movement will happen in the 2D (XY) plane instead of the XZ plane + /// if it does not know. This is important only for the point graph which does not have a well defined up direction. The other built-in graphs (e.g the grid graph) + /// will all tell the agent which movement plane it is supposed to use. + /// + /// [Open online documentation to see images] + /// </summary> + [UnityEngine.Serialization.FormerlySerializedAs("rotationIn2D")] + public OrientationMode orientation = OrientationMode.ZAxisForward; + + /// <summary> + /// If true, the forward axis of the character will be along the Y axis instead of the Z axis. + /// + /// Deprecated: Use <see cref="orientation"/> instead + /// </summary> + [System.Obsolete("Use orientation instead")] + public bool rotationIn2D { + get { return orientation == OrientationMode.YAxisForward; } + set { orientation = value ? OrientationMode.YAxisForward : OrientationMode.ZAxisForward; } + } + + /// <summary> + /// If true, the AI will rotate to face the movement direction. + /// See: <see cref="orientation"/> + /// </summary> + public bool enableRotation = true; + + /// <summary>How quickly to rotate</summary> + public float rotationSpeed = 10; + + /// <summary> + /// If true, some interpolation will be done when a new path has been calculated. + /// This is used to avoid short distance teleportation. + /// See: <see cref="switchPathInterpolationSpeed"/> + /// </summary> + public bool interpolatePathSwitches = true; + + /// <summary> + /// How quickly to interpolate to the new path. + /// See: <see cref="interpolatePathSwitches"/> + /// </summary> + public float switchPathInterpolationSpeed = 5; + + /// <summary>True if the end of the current path has been reached</summary> + public bool reachedEndOfPath { get; private set; } + + /// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary> + public bool reachedDestination { + get { + if (!reachedEndOfPath || !interpolator.valid) return false; + // Note: distanceToSteeringTarget is the distance to the end of the path when approachingPathEndpoint is true + var dir = destination - interpolator.endPoint; + // Ignore either the y or z coordinate depending on if we are using 2D mode or not + if (orientation == OrientationMode.YAxisForward) dir.z = 0; + else dir.y = 0; + + // Check against using a very small margin + // In theory a check against 0 should be done, but this will be a bit more resilient against targets that move slowly or maybe jitter around due to floating point errors. + if (remainingDistance + dir.magnitude >= 0.05f) return false; + + return true; + } + } + + public Vector3 destination { get; set; } + + /// <summary>\copydoc Pathfinding::IAstarAI::movementPlane</summary> + public NativeMovementPlane movementPlane { + get { + if (path != null && path.path.Count > 0) { + var graph = path.path[0].Graph; + if (graph is NavmeshBase navmeshBase) { + return new NativeMovementPlane(navmeshBase.transform.ToSimpleMovementPlane()); + } else if (graph is GridGraph gg) { + return new NativeMovementPlane(gg.transform.ToSimpleMovementPlane()); + } + } + return new NativeMovementPlane(Unity.Mathematics.quaternion.identity); + } + } + + /// <summary> + /// Determines if the character's position should be coupled to the Transform's position. + /// If false then all movement calculations will happen as usual, but the object that this component is attached to will not move + /// instead only the <see cref="position"/> property will change. + /// + /// See: <see cref="canMove"/> which in contrast to this field will disable all movement calculations. + /// See: <see cref="updateRotation"/> + /// </summary> + [System.NonSerialized] + public bool updatePosition = true; + + /// <summary> + /// Determines if the character's rotation should be coupled to the Transform's rotation. + /// If false then all movement calculations will happen as usual, but the object that this component is attached to will not rotate + /// instead only the <see cref="rotation"/> property will change. + /// + /// See: <see cref="updatePosition"/> + /// </summary> + [System.NonSerialized] + public bool updateRotation = true; + + /// <summary> + /// Cached delegate for the <see cref="OnPathComplete"/> method. + /// + /// Caching this avoids allocating a new one every time a path is calculated, which reduces GC pressure. + /// </summary> + protected OnPathDelegate onPathComplete; + + /// <summary> + /// Target to move towards. + /// The AI will try to follow/move towards this target. + /// It can be a point on the ground where the player has clicked in an RTS for example, or it can be the player object in a zombie game. + /// + /// Deprecated: In 4.0 this will automatically add a <see cref="Pathfinding.AIDestinationSetter"/> component and set the target on that component. + /// Try instead to use the <see cref="destination"/> property which does not require a transform to be created as the target or use + /// the AIDestinationSetter component directly. + /// </summary> + [System.Obsolete("Use the destination property or the AIDestinationSetter component instead")] + public Transform target { + get { + var setter = GetComponent<AIDestinationSetter>(); + return setter != null ? setter.target : null; + } + set { + targetCompatibility = null; + var setter = GetComponent<AIDestinationSetter>(); + if (setter == null) setter = gameObject.AddComponent<AIDestinationSetter>(); + setter.target = value; + destination = value != null ? value.position : new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::position</summary> + public Vector3 position { get { return updatePosition ? tr.position : simulatedPosition; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::rotation</summary> + public Quaternion rotation { + get { return updateRotation ? tr.rotation : simulatedRotation; } + set { + if (updateRotation) { + tr.rotation = value; + } else { + simulatedRotation = value; + } + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::endOfPath</summary> + public Vector3 endOfPath { + get { + if (interpolator.valid) return interpolator.endPoint; + if (float.IsFinite(destination.x)) return destination; + return position; + } + } + + #region IAstarAI implementation + + /// <summary>\copydoc Pathfinding::IAstarAI::Move</summary> + void IAstarAI.Move (Vector3 deltaPosition) { + // This script does not know the concept of being away from the path that it is following + // so this call will be ignored (as is also mentioned in the documentation). + } + + /// <summary>\copydoc Pathfinding::IAstarAI::radius</summary> + float IAstarAI.radius { get { return 0; } set {} } + + /// <summary>\copydoc Pathfinding::IAstarAI::height</summary> + float IAstarAI.height { get { return 0; } set {} } + + /// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary> + float IAstarAI.maxSpeed { get { return speed; } set { speed = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary> + bool IAstarAI.canSearch { get { return canSearch; } set { canSearch = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary> + bool IAstarAI.canMove { get { return canMove; } set { canMove = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::velocity</summary> + public Vector3 velocity { + get { + return Time.deltaTime > 0.00001f ? (previousPosition1 - previousPosition2) / Time.deltaTime : Vector3.zero; + } + } + + Vector3 IAstarAI.desiredVelocity { + get { + // The AILerp script sets the position every frame. It does not take into account physics + // or other things. So the velocity should always be the same as the desired velocity. + return (this as IAstarAI).velocity; + } + } + + Vector3 IAstarAI.desiredVelocityWithoutLocalAvoidance { + get { + // The AILerp script sets the position every frame. It does not take into account physics + // or other things. So the velocity should always be the same as the desired velocity. + return (this as IAstarAI).velocity; + } + set { + throw new System.InvalidOperationException("The AILerp component does not support setting the desiredVelocityWithoutLocalAvoidance property since it does not make sense for how its movement works."); + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary> + Vector3 IAstarAI.steeringTarget { + get { + // AILerp doesn't use steering at all, so we will just return a point ahead of the agent in the direction it is moving. + return interpolator.valid ? interpolator.position + interpolator.tangent : simulatedPosition; + } + } + + #endregion + + /// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary> + public float remainingDistance { + get { + return interpolator.valid ? Mathf.Max(interpolator.remainingDistance, 0) : float.PositiveInfinity; + } + set { + if (!interpolator.valid) throw new System.InvalidOperationException("Cannot set the remaining distance on the AILerp component because it doesn't have a path to follow."); + interpolator.remainingDistance = Mathf.Max(value, 0); + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary> + public bool hasPath { + get { + return interpolator.valid; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary> + public bool pathPending { + get { + return !canSearchAgain; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::isStopped</summary> + public bool isStopped { get; set; } + + /// <summary>\copydoc Pathfinding::IAstarAI::onSearchPath</summary> + public System.Action onSearchPath { get; set; } + + /// <summary>Cached Seeker component</summary> + protected Seeker seeker; + + /// <summary>Cached Transform component</summary> + protected Transform tr; + + /// <summary>Current path which is followed</summary> + protected ABPath path; + + /// <summary>Only when the previous path has been returned should a search for a new path be done</summary> + protected bool canSearchAgain = true; + + /// <summary> + /// When a new path was returned, the AI was moving along this ray. + /// Used to smoothly interpolate between the previous movement and the movement along the new path. + /// The speed is equal to movement direction. + /// </summary> + protected Vector3 previousMovementOrigin; + protected Vector3 previousMovementDirection; + + /// <summary> + /// Time since the path was replaced by a new path. + /// See: <see cref="interpolatePathSwitches"/> + /// </summary> + protected float pathSwitchInterpolationTime = 0; + + protected PathInterpolator.Cursor interpolator; + protected PathInterpolator interpolatorPath = new PathInterpolator(); + + + /// <summary> + /// Holds if the Start function has been run. + /// Used to test if coroutines should be started in OnEnable to prevent calculating paths + /// in the awake stage (or rather before start on frame 0). + /// </summary> + bool startHasRun = false; + + Vector3 previousPosition1, previousPosition2, simulatedPosition; + Quaternion simulatedRotation; + + /// <summary>Required for serialization backward compatibility</summary> + [UnityEngine.Serialization.FormerlySerializedAs("target")][SerializeField][HideInInspector] + Transform targetCompatibility; + + [SerializeField] + [HideInInspector] + [UnityEngine.Serialization.FormerlySerializedAs("repathRate")] + float repathRateCompatibility = float.NaN; + + [SerializeField] + [HideInInspector] + [UnityEngine.Serialization.FormerlySerializedAs("canSearch")] + bool canSearchCompability = false; + + protected AILerp () { + // Note that this needs to be set here in the constructor and not in e.g Awake + // because it is possible that other code runs and sets the destination property + // before the Awake method on this script runs. + destination = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + } + + /// <summary> + /// Initializes reference variables. + /// If you override this function you should in most cases call base.Awake () at the start of it. + /// </summary> + protected override void Awake () { + base.Awake(); + //This is a simple optimization, cache the transform component lookup + tr = transform; + + seeker = GetComponent<Seeker>(); + + // Tell the StartEndModifier to ask for our exact position when post processing the path This + // is important if we are using prediction and requesting a path from some point slightly ahead + // of us since then the start point in the path request may be far from our position when the + // path has been calculated. This is also good because if a long path is requested, it may take + // a few frames for it to be calculated so we could have moved some distance during that time + seeker.startEndModifier.adjustStartPoint = () => simulatedPosition; + } + + /// <summary> + /// Starts searching for paths. + /// If you override this function you should in most cases call base.Start () at the start of it. + /// See: <see cref="Init"/> + /// </summary> + protected virtual void Start () { + startHasRun = true; + Init(); + } + + /// <summary>Called when the component is enabled</summary> + protected virtual void OnEnable () { + onPathComplete = OnPathComplete; + Init(); + } + + void Init () { + if (startHasRun) { + // The Teleport call will make sure some variables are properly initialized (like #prevPosition1 and #prevPosition2) + Teleport(position, false); + autoRepath.Reset(); + if (shouldRecalculatePath) SearchPath(); + } + } + + public void OnDisable () { + ClearPath(); + } + + /// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,bool)}</summary> + public void GetRemainingPath (List<Vector3> buffer, out bool stale) { + buffer.Clear(); + if (!interpolator.valid) { + buffer.Add(position); + stale = true; + return; + } + + stale = false; + interpolator.GetRemainingPath(buffer); + // The agent is almost always at interpolation.position (which is buffer[0]) + // but sometimes - in particular when interpolating between two paths - the agent might at a slightly different position. + // So we replace the first point with the actual position of the agent. + buffer[0] = position; + } + + /// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,List<PathPartWithLinkInfo>,bool)}</summary> + public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale) { + GetRemainingPath(buffer, out stale); + // This movement script doesn't keep track of path parts, so we just add the whole path as a single part + if (partsBuffer != null) { + partsBuffer.Clear(); + partsBuffer.Add(new PathPartWithLinkInfo { startIndex = 0, endIndex = buffer.Count - 1 }); + } + } + + public void Teleport (Vector3 position, bool clearPath = true) { + if (clearPath) ClearPath(); + simulatedPosition = previousPosition1 = previousPosition2 = position; + if (updatePosition) tr.position = position; + reachedEndOfPath = false; + if (clearPath) SearchPath(); + } + + /// <summary>True if the path should be automatically recalculated as soon as possible</summary> + protected virtual bool shouldRecalculatePath { + get { + return canSearchAgain && autoRepath.ShouldRecalculatePath(position, 0.0f, destination, Time.time); + } + } + + /// <summary> + /// Requests a path to the target. + /// Deprecated: Use <see cref="SearchPath"/> instead. + /// </summary> + [System.Obsolete("Use SearchPath instead")] + public virtual void ForceSearchPath () { + SearchPath(); + } + + /// <summary>Requests a path to the target.</summary> + public virtual void SearchPath () { + if (float.IsPositiveInfinity(destination.x)) return; + if (onSearchPath != null) onSearchPath(); + + // This is where the path should start to search from + var currentPosition = GetFeetPosition(); + + // If we are following a path, start searching from the node we will + // reach next this can prevent odd turns right at the start of the path + /*if (interpolator.valid) { + var prevDist = interpolator.distance; + // Move to the end of the current segment + interpolator.MoveToSegment(interpolator.segmentIndex, 1); + currentPosition = interpolator.position; + // Move back to the original position + interpolator.distance = prevDist; + }*/ + + canSearchAgain = false; + + // Create a new path request + // The OnPathComplete method will later be called with the result + SetPath(ABPath.Construct(currentPosition, destination, null), false); + } + + /// <summary> + /// The end of the path has been reached. + /// If you want custom logic for when the AI has reached it's destination + /// add it here. + /// You can also create a new script which inherits from this one + /// and override the function in that script. + /// + /// Deprecated: Avoid overriding this method. Instead poll the <see cref="reachedDestination"/> or <see cref="reachedEndOfPath"/> properties. + /// </summary> + public virtual void OnTargetReached () { + } + + /// <summary> + /// Called when a requested path has finished calculation. + /// A path is first requested by <see cref="SearchPath"/>, it is then calculated, probably in the same or the next frame. + /// Finally it is returned to the seeker which forwards it to this function. + /// </summary> + protected virtual void OnPathComplete (Path _p) { + ABPath p = _p as ABPath; + + if (p == null) throw new System.Exception("This function only handles ABPaths, do not use special path types"); + + canSearchAgain = true; + + // Increase the reference count on the path. + // This is used for path pooling + p.Claim(this); + + // Path couldn't be calculated of some reason. + // More info in p.errorLog (debug string) + if (p.error) { + p.Release(this); + return; + } + + if (interpolatePathSwitches) { + ConfigurePathSwitchInterpolation(); + } + + + // Replace the old path + var oldPath = path; + + path = p; + reachedEndOfPath = false; + + // The RandomPath and MultiTargetPath do not have a well defined destination that could have been + // set before the paths were calculated. So we instead set the destination here so that some properties + // like #reachedDestination and #remainingDistance work correctly. + if (path is RandomPath rpath) { + destination = rpath.originalEndPoint; + } else if (path is MultiTargetPath mpath) { + destination = mpath.originalEndPoint; + } + + // Just for the rest of the code to work, if there + // is only one waypoint in the path add another one + if (path.vectorPath != null && path.vectorPath.Count == 1) { + path.vectorPath.Insert(0, GetFeetPosition()); + } + + // Reset some variables + ConfigureNewPath(); + + // Release the previous path + // This is used for path pooling. + // This is done after the interpolator has been configured in the ConfigureNewPath method + // as this method would otherwise invalidate the interpolator + // since the vectorPath list (which the interpolator uses) will be pooled. + if (oldPath != null) oldPath.Release(this); + + if (interpolator.remainingDistance < 0.0001f && !reachedEndOfPath) { + reachedEndOfPath = true; + OnTargetReached(); + } + } + + /// <summary> + /// Clears the current path of the agent. + /// + /// Usually invoked using <see cref="SetPath"/>(null) + /// + /// See: <see cref="SetPath"/> + /// See: <see cref="isStopped"/> + /// </summary> + protected virtual void ClearPath () { + // Abort any calculations in progress + if (seeker != null) seeker.CancelCurrentPathRequest(); + canSearchAgain = true; + reachedEndOfPath = false; + + // Release current path so that it can be pooled + if (path != null) path.Release(this); + path = null; + interpolatorPath.SetPath(null); + } + + /// <summary>\copydoc Pathfinding::IAstarAI::SetPath</summary> + public void SetPath (Path path, bool updateDestinationFromPath = true) { + if (updateDestinationFromPath && path is ABPath abPath && !(path is RandomPath)) { + this.destination = abPath.originalEndPoint; + } + + if (path == null) { + ClearPath(); + } else if (path.PipelineState == PathState.Created) { + // Path has not started calculation yet + canSearchAgain = false; + seeker.CancelCurrentPathRequest(); + seeker.StartPath(path, onPathComplete); + autoRepath.DidRecalculatePath(destination, Time.time); + } else if (path.PipelineState >= PathState.Returning) { + // Path has already been calculated + + // We might be calculating another path at the same time, and we don't want that path to override this one. So cancel it. + if (seeker.GetCurrentPath() != path) seeker.CancelCurrentPathRequest(); + + OnPathComplete(path); + } else { + // Path calculation has been started, but it is not yet complete. Cannot really handle this. + throw new System.ArgumentException("You must call the SetPath method with a path that either has been completely calculated or one whose path calculation has not been started at all. It looks like the path calculation for the path you tried to use has been started, but is not yet finished."); + } + } + + protected virtual void ConfigurePathSwitchInterpolation () { + bool reachedEndOfPreviousPath = interpolator.valid && interpolator.remainingDistance < 0.0001f; + + if (interpolator.valid && !reachedEndOfPreviousPath) { + previousMovementOrigin = interpolator.position; + previousMovementDirection = interpolator.tangent.normalized * interpolator.remainingDistance; + pathSwitchInterpolationTime = 0; + } else { + previousMovementOrigin = Vector3.zero; + previousMovementDirection = Vector3.zero; + pathSwitchInterpolationTime = float.PositiveInfinity; + } + } + + public virtual Vector3 GetFeetPosition () { + return position; + } + + /// <summary>Finds the closest point on the current path and configures the <see cref="interpolator"/></summary> + protected virtual void ConfigureNewPath () { + var hadValidPath = interpolator.valid; + var prevTangent = hadValidPath ? interpolator.tangent : Vector3.zero; + + interpolatorPath.SetPath(path.vectorPath); + interpolator = interpolatorPath.start; + interpolator.MoveToClosestPoint(GetFeetPosition()); + + if (interpolatePathSwitches && switchPathInterpolationSpeed > 0.01f && hadValidPath) { + var correctionFactor = Mathf.Max(-Vector3.Dot(prevTangent.normalized, interpolator.tangent.normalized), 0); + interpolator.distance -= speed*correctionFactor*(1f/switchPathInterpolationSpeed); + } + } + + protected virtual void Update () { + if (shouldRecalculatePath) SearchPath(); + if (canMove) { + Vector3 nextPosition; + Quaternion nextRotation; + MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation); + FinalizeMovement(nextPosition, nextRotation); + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::MovementUpdate</summary> + public void MovementUpdate (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) { + if (updatePosition) simulatedPosition = tr.position; + if (updateRotation) simulatedRotation = tr.rotation; + + Vector3 direction; + + nextPosition = CalculateNextPosition(out direction, isStopped ? 0f : deltaTime); + + if (enableRotation) nextRotation = SimulateRotationTowards(direction, deltaTime); + else nextRotation = simulatedRotation; + } + + /// <summary>\copydoc Pathfinding::IAstarAI::FinalizeMovement</summary> + public void FinalizeMovement (Vector3 nextPosition, Quaternion nextRotation) { + previousPosition2 = previousPosition1; + previousPosition1 = simulatedPosition = nextPosition; + simulatedRotation = nextRotation; + if (updatePosition) tr.position = nextPosition; + if (updateRotation) tr.rotation = nextRotation; + } + + Quaternion SimulateRotationTowards (Vector3 direction, float deltaTime) { + // Rotate unless we are really close to the target + if (direction != Vector3.zero) { + Quaternion targetRotation = Quaternion.LookRotation(direction, orientation == OrientationMode.YAxisForward ? Vector3.back : Vector3.up); + // This causes the character to only rotate around the Z axis + if (orientation == OrientationMode.YAxisForward) targetRotation *= Quaternion.Euler(90, 0, 0); + return Quaternion.Slerp(simulatedRotation, targetRotation, deltaTime * rotationSpeed); + } + return simulatedRotation; + } + + /// <summary>Calculate the AI's next position (one frame in the future).</summary> + /// <param name="direction">The tangent of the segment the AI is currently traversing. Not normalized.</param> + /// <param name="deltaTime">The time to simulate into the future.</param> + protected virtual Vector3 CalculateNextPosition (out Vector3 direction, float deltaTime) { + if (!interpolator.valid) { + direction = Vector3.zero; + return simulatedPosition; + } + + interpolator.distance += deltaTime * speed; + + if (interpolator.remainingDistance < 0.0001f && !reachedEndOfPath) { + reachedEndOfPath = true; + OnTargetReached(); + } + + direction = interpolator.tangent; + pathSwitchInterpolationTime += deltaTime; + var alpha = switchPathInterpolationSpeed * pathSwitchInterpolationTime; + + if (interpolatePathSwitches && alpha < 1f) { + // Find the approximate position we would be at if we + // would have continued to follow the previous path + Vector3 positionAlongPreviousPath = previousMovementOrigin + Vector3.ClampMagnitude(previousMovementDirection, speed * pathSwitchInterpolationTime); + + // Interpolate between the position on the current path and the position + // we would have had if we would have continued along the previous path. + return Vector3.Lerp(positionAlongPreviousPath, interpolator.position, alpha); + } else { + return interpolator.position; + } + } + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + if (migrations.TryMigrateFromLegacyFormat(out var legacyVersion)) { + if (legacyVersion <= 3) { + repathRate = repathRateCompatibility; + canSearch = canSearchCompability; + } + } + #pragma warning disable 618 + if (unityThread && targetCompatibility != null) target = targetCompatibility; + #pragma warning restore 618 + } + + public override void DrawGizmos () { + tr = transform; + autoRepath.DrawGizmos(Pathfinding.Drawing.Draw.editor, this.position, 0.0f, new NativeMovementPlane(orientation == OrientationMode.YAxisForward ? Quaternion.Euler(-90, 0, 0) : Quaternion.identity)); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AILerp.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AILerp.cs.meta new file mode 100644 index 0000000..f22788b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AILerp.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 847a14d4dc9cc43679ab34fc78e0182f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIPath.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIPath.cs new file mode 100644 index 0000000..49b9648 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIPath.cs @@ -0,0 +1,542 @@ +using UnityEngine; +using System.Collections.Generic; + +namespace Pathfinding { + using Pathfinding.Util; + using Pathfinding.Drawing; + + /// <summary> + /// AI for following paths. + /// + /// This AI is the default movement script which comes with the A* Pathfinding Project. + /// It is in no way required by the rest of the system, so feel free to write your own. But I hope this script will make it easier + /// to set up movement for the characters in your game. + /// This script works well for many types of units, but if you need the highest performance (for example if you are moving hundreds of characters) you + /// may want to customize this script or write a custom movement script to be able to optimize it specifically for your game. + /// + /// This script will try to move to a given <see cref="destination"/>. At <see cref="repathRate regular"/>, the path to the destination will be recalculated. + /// If you want to make the AI to follow a particular object you can attach the <see cref="Pathfinding.AIDestinationSetter"/> component. + /// Take a look at the getstarted (view in online documentation for working links) tutorial for more instructions on how to configure this script. + /// + /// Here is a video of this script being used move an agent around (technically it uses the <see cref="Pathfinding.Examples.MineBotAI"/> script that inherits from this one but adds a bit of animation support for the example scenes): + /// [Open online documentation to see videos] + /// + /// \section variables Quick overview of the variables + /// In the inspector in Unity, you will see a bunch of variables. You can view detailed information further down, but here's a quick overview. + /// + /// The <see cref="repathRate"/> determines how often it will search for new paths, if you have fast moving targets, you might want to set it to a lower value. + /// The <see cref="destination"/> field is where the AI will try to move, it can be a point on the ground where the player has clicked in an RTS for example. + /// Or it can be the player object in a zombie game. + /// The <see cref="maxSpeed"/> is self-explanatory, as is <see cref="rotationSpeed"/>. however <see cref="slowdownDistance"/> might require some explanation: + /// It is the approximate distance from the target where the AI will start to slow down. Setting it to a large value will make the AI slow down very gradually. + /// <see cref="pickNextWaypointDist"/> determines the distance to the point the AI will move to (see image below). + /// + /// Below is an image illustrating several variables that are exposed by this class (<see cref="pickNextWaypointDist"/>, <see cref="steeringTarget"/>, <see cref="desiredVelocity)"/> + /// [Open online documentation to see images] + /// + /// This script has many movement fallbacks. + /// If it finds an RVOController attached to the same GameObject as this component, it will use that. If it finds a character controller it will also use that. + /// If it finds a rigidbody it will use that. Lastly it will fall back to simply modifying Transform.position which is guaranteed to always work and is also the most performant option. + /// + /// \section how-aipath-works How it works + /// In this section I'm going to go over how this script is structured and how information flows. + /// This is useful if you want to make changes to this script or if you just want to understand how it works a bit more deeply. + /// However you do not need to read this section if you are just going to use the script as-is. + /// + /// This script inherits from the <see cref="AIBase"/> class. The movement happens either in Unity's standard Update or FixedUpdate method. + /// They are both defined in the AIBase class. Which one is actually used depends on if a rigidbody is used for movement or not. + /// Rigidbody movement has to be done inside the FixedUpdate method while otherwise it is better to do it in Update. + /// + /// From there a call is made to the <see cref="MovementUpdate"/> method (which in turn calls <see cref="MovementUpdateInternal)"/>. + /// This method contains the main bulk of the code and calculates how the AI *wants* to move. However it doesn't do any movement itself. + /// Instead it returns the position and rotation it wants the AI to move to have at the end of the frame. + /// The Update (or FixedUpdate) method then passes these values to the <see cref="FinalizeMovement"/> method which is responsible for actually moving the character. + /// That method also handles things like making sure the AI doesn't fall through the ground using raycasting. + /// + /// The AI recalculates its path regularly. This happens in the Update method which checks <see cref="shouldRecalculatePath"/>, and if that returns true it will call <see cref="SearchPath"/>. + /// The <see cref="SearchPath"/> method will prepare a path request and send it to the <see cref="Seeker"/> component, which should be attached to the same GameObject as this script. + /// </summary> + [AddComponentMenu("Pathfinding/AI/AIPath (2D,3D)")] + [UniqueComponent(tag = "ai")] + public partial class AIPath : AIBase, IAstarAI { + /// <summary> + /// How quickly the agent accelerates. + /// Positive values represent an acceleration in world units per second squared. + /// Negative values are interpreted as an inverse time of how long it should take for the agent to reach its max speed. + /// For example if it should take roughly 0.4 seconds for the agent to reach its max speed then this field should be set to -1/0.4 = -2.5. + /// For a negative value the final acceleration will be: -acceleration*maxSpeed. + /// This behaviour exists mostly for compatibility reasons. + /// + /// In the Unity inspector there are two modes: Default and Custom. In the Default mode this field is set to -2.5 which means that it takes about 0.4 seconds for the agent to reach its top speed. + /// In the Custom mode you can set the acceleration to any positive value. + /// </summary> + public float maxAcceleration = -2.5f; + + /// <summary> + /// Rotation speed in degrees per second. + /// Rotation is calculated using Quaternion.RotateTowards. This variable represents the rotation speed in degrees per second. + /// The higher it is, the faster the character will be able to rotate. + /// </summary> + [UnityEngine.Serialization.FormerlySerializedAs("turningSpeed")] + public float rotationSpeed = 360; + + /// <summary>Distance from the end of the path where the AI will start to slow down</summary> + public float slowdownDistance = 0.6F; + + /// <summary> + /// How far the AI looks ahead along the path to determine the point it moves to. + /// In world units. + /// If you enable the <see cref="alwaysDrawGizmos"/> toggle this value will be visualized in the scene view as a blue circle around the agent. + /// [Open online documentation to see images] + /// + /// Here are a few example videos showing some typical outcomes with good values as well as how it looks when this value is too low and too high. + /// <table> + /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-danger">Too low</span><br/></verbatim>\endxmlonly A too low value and a too low acceleration will result in the agent overshooting a lot and not managing to follow the path well.</td></tr> + /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-warning">Ok</span><br/></verbatim>\endxmlonly A low value but a high acceleration works decently to make the AI follow the path more closely. Note that the <see cref="Pathfinding.AILerp"/> component is better suited if you want the agent to follow the path without any deviations.</td></tr> + /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-success">Ok</span><br/></verbatim>\endxmlonly A reasonable value in this example.</td></tr> + /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-success">Ok</span><br/></verbatim>\endxmlonly A reasonable value in this example, but the path is followed slightly more loosely than in the previous video.</td></tr> + /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-danger">Too high</span><br/></verbatim>\endxmlonly A too high value will make the agent follow the path too loosely and may cause it to try to move through obstacles.</td></tr> + /// </table> + /// </summary> + public float pickNextWaypointDist = 2; + + /// <summary>Draws detailed gizmos constantly in the scene view instead of only when the agent is selected and settings are being modified</summary> + public bool alwaysDrawGizmos; + + /// <summary> + /// Slow down when not facing the target direction. + /// Incurs at a small performance overhead. + /// + /// This setting only has an effect if <see cref="enableRotation"/> is enabled. + /// </summary> + public bool slowWhenNotFacingTarget = true; + + /// <summary> + /// Prevent the velocity from being too far away from the forward direction of the character. + /// If the character is ordered to move in the opposite direction from where it is facing + /// then enabling this will cause it to make a small loop instead of turning on the spot. + /// + /// This setting only has an effect if <see cref="slowWhenNotFacingTarget"/> is enabled. + /// </summary> + public bool preventMovingBackwards = false; + + /// <summary> + /// Ensure that the character is always on the traversable surface of the navmesh. + /// When this option is enabled a <see cref="AstarPath.GetNearest"/> query will be done every frame to find the closest node that the agent can walk on + /// and if the agent is not inside that node, then the agent will be moved to it. + /// + /// This is especially useful together with local avoidance in order to avoid agents pushing each other into walls. + /// See: local-avoidance (view in online documentation for working links) for more info about this. + /// + /// This option also integrates with local avoidance so that if the agent is say forced into a wall by other agents the local avoidance + /// system will be informed about that wall and can take that into account. + /// + /// Enabling this has some performance impact depending on the graph type (pretty fast for grid graphs, slightly slower for navmesh/recast graphs). + /// If you are using a navmesh/recast graph you may want to switch to the <see cref="Pathfinding.RichAI"/> movement script which is specifically written for navmesh/recast graphs and + /// does this kind of clamping out of the box. In many cases it can also follow the path more smoothly around sharp bends in the path. + /// + /// It is not recommended that you use this option together with the funnel modifier on grid graphs because the funnel modifier will make the path + /// go very close to the border of the graph and this script has a tendency to try to cut corners a bit. This may cause it to try to go slightly outside the + /// traversable surface near corners and that will look bad if this option is enabled. + /// + /// Warning: This option makes no sense to use on point graphs because point graphs do not have a surface. + /// Enabling this option when using a point graph will lead to the agent being snapped to the closest node every frame which is likely not what you want. + /// + /// Below you can see an image where several agents using local avoidance were ordered to go to the same point in a corner. + /// When not constraining the agents to the graph they are easily pushed inside obstacles. + /// [Open online documentation to see images] + /// </summary> + public bool constrainInsideGraph = false; + + /// <summary>Current path which is followed</summary> + protected Path path; + + /// <summary>Represents the current steering target for the agent</summary> + protected PathInterpolator.Cursor interpolator; + /// <summary>Helper which calculates points along the current path</summary> + protected PathInterpolator interpolatorPath = new PathInterpolator(); + + #region IAstarAI implementation + + /// <summary>\copydoc Pathfinding::IAstarAI::Teleport</summary> + public override void Teleport (Vector3 newPosition, bool clearPath = true) { + reachedEndOfPath = false; + base.Teleport(newPosition, clearPath); + } + + /// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary> + public float remainingDistance => interpolator.valid ? interpolator.remainingDistance + movementPlane.ToPlane(interpolator.position - position).magnitude : float.PositiveInfinity; + + /// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary> + public override bool reachedDestination { + get { + if (!reachedEndOfPath) return false; + if (!interpolator.valid || remainingDistance + movementPlane.ToPlane(destination - interpolator.endPoint).magnitude > endReachedDistance) return false; + + // Don't do height checks in 2D mode + if (orientation != OrientationMode.YAxisForward) { + // Check if the destination is above the head of the character or far below the feet of it + movementPlane.ToPlane(destination - position, out float yDifference); + var h = tr.localScale.y * height; + if (yDifference > h || yDifference < -h*0.5) return false; + } + + return true; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::reachedEndOfPath</summary> + public bool reachedEndOfPath { get; protected set; } + + /// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary> + public bool hasPath => interpolator.valid; + + /// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary> + public bool pathPending => waitingForPathCalculation; + + /// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary> + public Vector3 steeringTarget => interpolator.valid ? interpolator.position : position; + + /// <summary>\copydoc Pathfinding::IAstarAI::endOfPath</summary> + public override Vector3 endOfPath { + get { + if (interpolator.valid) return interpolator.endPoint; + if (float.IsFinite(destination.x)) return destination; + return position; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::radius</summary> + float IAstarAI.radius { get => radius; set => radius = value; } + + /// <summary>\copydoc Pathfinding::IAstarAI::height</summary> + float IAstarAI.height { get => height; set => height = value; } + + /// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary> + float IAstarAI.maxSpeed { get => maxSpeed; set => maxSpeed = value; } + + /// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary> + bool IAstarAI.canSearch { get => canSearch; set => canSearch = value; } + + /// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary> + bool IAstarAI.canMove { get => canMove; set => canMove = value; } + + /// <summary>\copydoc Pathfinding::IAstarAI::movementPlane</summary> + NativeMovementPlane IAstarAI.movementPlane => new NativeMovementPlane(movementPlane); + + #endregion + + /// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,bool)}</summary> + public void GetRemainingPath (List<Vector3> buffer, out bool stale) { + buffer.Clear(); + buffer.Add(position); + if (!interpolator.valid) { + stale = true; + return; + } + + stale = false; + interpolator.GetRemainingPath(buffer); + } + + /// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,List<PathPartWithLinkInfo>,bool)}</summary> + public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale) { + GetRemainingPath(buffer, out stale); + // This movement script doesn't keep track of path parts, so we just add the whole path as a single part + if (partsBuffer != null) { + partsBuffer.Clear(); + partsBuffer.Add(new PathPartWithLinkInfo { startIndex = 0, endIndex = buffer.Count - 1 }); + } + } + + protected override void OnDisable () { + // This will, among other things call ClearPath + base.OnDisable(); + rotationFilterState = Vector2.zero; + rotationFilterState2 = Vector2.zero; + } + + /// <summary> + /// The end of the path has been reached. + /// If you want custom logic for when the AI has reached it's destination add it here. You can + /// also create a new script which inherits from this one and override the function in that script. + /// + /// This method will be called again if a new path is calculated as the destination may have changed. + /// So when the agent is close to the destination this method will typically be called every <see cref="repathRate"/> seconds. + /// + /// Deprecated: Avoid overriding this method. Instead poll the <see cref="reachedDestination"/> or <see cref="reachedEndOfPath"/> properties. + /// </summary> + public virtual void OnTargetReached () { + } + + protected virtual void UpdateMovementPlane () { + if (path.path == null || path.path.Count == 0) return; + var graph = AstarData.GetGraph(path.path[0]) as ITransformedGraph; + IMovementPlane graphTransform = graph != null ? graph.transform : (orientation == OrientationMode.YAxisForward ? new GraphTransform(Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(-90, 270, 90), Vector3.one)) : GraphTransform.identityTransform); + + movementPlane = graphTransform.ToSimpleMovementPlane(); + } + + /// <summary> + /// Called when a requested path has been calculated. + /// A path is first requested by <see cref="SearchPath"/>, it is then calculated, probably in the same or the next frame. + /// Finally it is returned to the seeker which forwards it to this function. + /// </summary> + protected override void OnPathComplete (Path newPath) { + ABPath p = newPath as ABPath; + + if (p == null) throw new System.Exception("This function only handles ABPaths, do not use special path types"); + + waitingForPathCalculation = false; + + // Increase the reference count on the new path. + // This is used for object pooling to reduce allocations. + p.Claim(this); + + // Path couldn't be calculated of some reason. + // More info in p.errorLog (debug string) + if (p.error) { + p.Release(this); + SetPath(null); + return; + } + + // Release the previous path. + if (path != null) path.Release(this); + + // Replace the old path + path = p; + + // The RandomPath and MultiTargetPath do not have a well defined destination that could have been + // set before the paths were calculated. So we instead set the destination here so that some properties + // like #reachedDestination and #remainingDistance work correctly. + if (!p.endPointKnownBeforeCalculation) { + destination = p.originalEndPoint; + } + + // Make sure the path contains at least 2 points + if (path.vectorPath.Count == 1) path.vectorPath.Add(path.vectorPath[0]); + interpolatorPath.SetPath(path.vectorPath); + interpolator = interpolatorPath.start; + + UpdateMovementPlane(); + + // Reset some variables + reachedEndOfPath = false; + + // Simulate movement from the point where the path was requested + // to where we are right now. This reduces the risk that the agent + // gets confused because the first point in the path is far away + // from the current position (possibly behind it which could cause + // the agent to turn around, and that looks pretty bad). + interpolator.MoveToLocallyClosestPoint((GetFeetPosition() + p.originalStartPoint) * 0.5f); + interpolator.MoveToLocallyClosestPoint(GetFeetPosition()); + + // Update which point we are moving towards. + // Note that we need to do this here because otherwise the remainingDistance field might be incorrect for 1 frame. + // (due to interpolator.remainingDistance being incorrect). + interpolator.MoveToCircleIntersection2D(position, pickNextWaypointDist, movementPlane); + + var distanceToEnd = remainingDistance; + + if (distanceToEnd <= endReachedDistance) { + reachedEndOfPath = true; + OnTargetReached(); + } + } + + protected override void ClearPath () { + CancelCurrentPathRequest(); + // Release current path so that it can be pooled + if (path != null) path.Release(this); + path = null; + interpolatorPath.SetPath(null); + reachedEndOfPath = false; + } + + /// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary> + protected override void MovementUpdateInternal (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) { + float currentAcceleration = maxAcceleration; + + // If negative, calculate the acceleration from the max speed + if (currentAcceleration < 0) currentAcceleration *= -maxSpeed; + + if (updatePosition) { + // Get our current position. We read from transform.position as few times as possible as it is relatively slow + // (at least compared to a local variable) + simulatedPosition = tr.position; + } + if (updateRotation) simulatedRotation = tr.rotation; + + var currentPosition = simulatedPosition; + + // Normalized direction of where the agent is looking + var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward)); + + // Check if we have a valid path to follow and some other script has not stopped the character + bool stopped = isStopped || (reachedDestination && whenCloseToDestination == CloseToDestinationMode.Stop); + + if (rvoController != null) rvoDensityBehavior.Update(rvoController.enabled, reachedDestination, ref stopped, ref rvoController.priorityMultiplier, ref rvoController.flowFollowingStrength, currentPosition); + + float speedLimitFactor = 0; + float distanceToEnd; + // Check if we have a path to follow + if (interpolator.valid) { + // Update which point we are moving towards + interpolator.MoveToCircleIntersection2D(currentPosition, pickNextWaypointDist, movementPlane); + var dir = movementPlane.ToPlane(steeringTarget - currentPosition); + + // Calculate the distance to the end of the path + distanceToEnd = dir.magnitude + Mathf.Max(0, interpolator.remainingDistance); + + // Check if we have reached the target + var prevTargetReached = reachedEndOfPath; + reachedEndOfPath = distanceToEnd <= endReachedDistance; + if (!prevTargetReached && reachedEndOfPath) OnTargetReached(); + + if (!stopped) { + // How fast to move depending on the distance to the destination. + // Move slower as the character gets closer to the destination. + // This is always a value between 0 and 1. + speedLimitFactor = distanceToEnd < slowdownDistance? Mathf.Sqrt(distanceToEnd / slowdownDistance) : 1; + velocity2D += MovementUtilities.CalculateAccelerationToReachPoint(dir, dir.normalized*maxSpeed, velocity2D, currentAcceleration, rotationSpeed, maxSpeed, forwards) * deltaTime; + } + } else { + reachedEndOfPath = false; + distanceToEnd = float.PositiveInfinity; + } + + if (!interpolator.valid || stopped) { + // Slow down as quickly as possible + velocity2D -= Vector2.ClampMagnitude(velocity2D, currentAcceleration * deltaTime); + // We are already slowing down as quickly as possible. Avoid limiting the speed in other ways. + speedLimitFactor = 1; + } + + velocity2D = MovementUtilities.ClampVelocity(velocity2D, maxSpeed, speedLimitFactor, slowWhenNotFacingTarget && enableRotation, preventMovingBackwards, forwards); + + ApplyGravity(deltaTime); + bool avoidingOtherAgents = false; + + if (rvoController != null && rvoController.enabled) { + // Send a message to the RVOController that we want to move + // with this velocity. In the next simulation step, this + // velocity will be processed and it will be fed back to the + // rvo controller and finally it will be used by this script + // when calling the CalculateMovementDelta method below + + // Make sure that we don't move further than to the end point + // of the path. If the RVO simulation FPS is low and we did + // not do this, the agent might overshoot the target a lot. + var rvoTarget = currentPosition + movementPlane.ToWorld(Vector2.ClampMagnitude(velocity2D, distanceToEnd), 0f); + rvoController.SetTarget(rvoTarget, velocity2D.magnitude, maxSpeed, endOfPath); + avoidingOtherAgents = rvoController.AvoidingAnyAgents; + } + + // Set how much the agent wants to move during this frame + var delta2D = lastDeltaPosition = CalculateDeltaToMoveThisFrame(currentPosition, distanceToEnd, deltaTime); + nextPosition = currentPosition + movementPlane.ToWorld(delta2D, verticalVelocity * deltaTime); + CalculateNextRotation(speedLimitFactor, avoidingOtherAgents, out nextRotation); + } + + Vector2 rotationFilterState, rotationFilterState2; + + protected virtual void CalculateNextRotation (float slowdown, bool avoidingOtherAgents, out Quaternion nextRotation) { + if (lastDeltaTime > 0.00001f && enableRotation) { + // Rotate towards the direction we are moving in + // Filter out noise in the movement direction + // This is especially important when the agent is almost standing still and when using local avoidance + float noiseThreshold = radius * tr.localScale.x * 0.2f; + float rotationSpeedFactor = MovementUtilities.FilterRotationDirection(ref rotationFilterState, ref rotationFilterState2, lastDeltaPosition, noiseThreshold, lastDeltaTime, avoidingOtherAgents); + nextRotation = SimulateRotationTowards(rotationFilterState, rotationSpeed * lastDeltaTime * rotationSpeedFactor, rotationSpeed * lastDeltaTime); + } else { + // TODO: simulatedRotation + nextRotation = rotation; + } + } + + static NNConstraint cachedNNConstraint = NNConstraint.Walkable; + protected override Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) { + if (constrainInsideGraph) { + cachedNNConstraint.tags = seeker.traversableTags; + cachedNNConstraint.graphMask = seeker.graphMask; + cachedNNConstraint.distanceMetric = DistanceMetric.ClosestAsSeenFromAboveSoft(); + // Note: We don't want to set nn.constrainDistance = false (i.e. allow finding nodes arbitrarily far away), because that can lead to harsh + // performance cliffs if agents for example fall through the ground or get thrown off the map, or something like that (it's bound to happen in some games). + var nearestOnNavmesh = AstarPath.active.GetNearest(position, cachedNNConstraint); + + if (nearestOnNavmesh.node == null) { + // Found no valid node to constrain to. This can happen if there are no valid nodes close enough to the agent. + positionChanged = false; + return position; + } + + var clampedPosition = nearestOnNavmesh.position; + + if (rvoController != null && rvoController.enabled) { + // Inform the RVO system about the edges of the navmesh which will allow + // it to better keep inside the navmesh in the first place. + rvoController.SetObstacleQuery(nearestOnNavmesh.node); + } + + // We cannot simply check for equality because some precision may be lost + // if any coordinate transformations are used. + var difference = movementPlane.ToPlane(clampedPosition - position); + float sqrDifference = difference.sqrMagnitude; + if (sqrDifference > 0.001f*0.001f) { + // The agent was outside the navmesh. Remove that component of the velocity + // so that the velocity only goes along the direction of the wall, not into it + velocity2D -= difference * Vector2.Dot(difference, velocity2D) / sqrDifference; + + positionChanged = true; + // Return the new position, but ignore any changes in the y coordinate from the ClampToNavmesh method as the y coordinates in the navmesh are rarely very accurate + return position + movementPlane.ToWorld(difference); + } + } + + positionChanged = false; + return position; + } + +#if UNITY_EDITOR + [System.NonSerialized] + int gizmoHash = 0; + + [System.NonSerialized] + float lastChangedTime = float.NegativeInfinity; + + protected static readonly Color GizmoColor = new Color(46.0f/255, 104.0f/255, 201.0f/255); + + public override void DrawGizmos () { + base.DrawGizmos(); + + // If alwaysDrawGizmos is false, gizmos are only visible for a short while after the user changes any settings on this component + var newGizmoHash = pickNextWaypointDist.GetHashCode() ^ slowdownDistance.GetHashCode() ^ endReachedDistance.GetHashCode(); + + if (newGizmoHash != gizmoHash && gizmoHash != 0) lastChangedTime = Time.realtimeSinceStartup; + gizmoHash = newGizmoHash; + float alpha = alwaysDrawGizmos ? 1 : Mathf.SmoothStep(1, 0, (Time.realtimeSinceStartup - lastChangedTime - 5f)/0.5f) * (GizmoContext.selectionSize == 1 ? 1 : 0); + + if (alpha > 0) { + // Make sure the scene view is repainted while the gizmos are visible + if (!alwaysDrawGizmos) UnityEditor.SceneView.RepaintAll(); + Draw.Line(position, steeringTarget, GizmoColor * new Color(1, 1, 1, alpha)); + using (Draw.WithMatrix(Matrix4x4.TRS(position, transform.rotation * (orientation == OrientationMode.YAxisForward ? Quaternion.Euler(-90, 0, 0) : Quaternion.identity), Vector3.one))) { + Draw.xz.Circle(Vector3.zero, pickNextWaypointDist, GizmoColor * new Color(1, 1, 1, alpha)); + Draw.xz.Circle(Vector3.zero, slowdownDistance, Color.Lerp(GizmoColor, Color.red, 0.5f) * new Color(1, 1, 1, alpha)); + Draw.xz.Circle(Vector3.zero, endReachedDistance, Color.Lerp(GizmoColor, Color.red, 0.8f) * new Color(1, 1, 1, alpha)); + } + } + } +#endif + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + if (migrations.IsLegacyFormat) { + // Approximately convert from a damping value to a degrees per second value. + if (migrations.LegacyVersion < 1) rotationSpeed *= 90; + // The base call will migrate the legacy format further + } + base.OnUpgradeSerializedData(ref migrations, unityThread); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIPath.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIPath.cs.meta new file mode 100644 index 0000000..435260d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/AIPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6eb1402c17e84a9282a7f0f62eb584f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs new file mode 100644 index 0000000..a262873 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs @@ -0,0 +1,1826 @@ +#pragma warning disable CS0282 // "There is no defined ordering between fields in multiple declarations of partial struct" +#if MODULE_ENTITIES +using UnityEngine; +using Unity.Mathematics; +using System.Collections.Generic; +using Unity.Collections; +using UnityEngine.Profiling; +using Unity.Entities; +using Unity.Transforms; + +namespace Pathfinding { + using Pathfinding.Drawing; + using Pathfinding.Util; + using Palette = Pathfinding.Drawing.Palette.Colorbrewer.Set1; + using System; + using Pathfinding.PID; + using Pathfinding.ECS.RVO; + using Pathfinding.ECS; + using UnityEngine.Assertions; + + /// <summary> + /// Movement script that uses ECS. + /// + /// Warning: This script is still in beta and may change in the future. It aims to be much more robust than AIPath/RichAI, but there may still be rough edges. + /// + /// This script is a replacement for the <see cref="AIPath"/> and <see cref="RichAI"/> scripts. + /// + /// This script is a movement script. It takes care of moving an agent along a path, updating the path, and so on. + /// + /// The intended way to use this script is to use these two components: + /// - <see cref="FollowerEntity"/> + /// - <see cref="AIDestinationSetter"/> (optional, you can instead set the <see cref="destination"/> property manually) + /// + /// Of note is that this component shouldn't be used with a <see cref="Seeker"/> component. + /// It instead has its own settings for pathfinding, which are stored in the <see cref="pathfindingSettings"/> field. + /// + /// <b>Features</b> + /// + /// - Uses Unity's ECS (Entity Component System) to move the agent. This means it is highly-performant and is able to utilize multiple threads. + /// - Supports local avoidance (see local-avoidance) (view in online documentation for working links). + /// - Supports movement in both 2D and 3D games. + /// - Supports movement on spherical on non-planar worlds (see spherical) (view in online documentation for working links). + /// - Supports movement on grid graphs as well as navmesh/recast graphs. + /// - Does <b>not</b> support movement on point graphs at the moment. This may be added in a future update. + /// - Supports time-scales greater than 1. The agent will automatically run multiple simulation steps per frame if the time-scale is greater than 1, to ensure stability. + /// - Supports off-mesh links. Subscribe to the <see cref="onTraverseOffMeshLink"/> event to handle this. + /// - Knows which node it is traversing at all times (see <see cref="currentNode)"/>. + /// - Automatically stops when trying to reach a crowded destination when using local avoidance. + /// - Clamps the agent to the navmesh at all times. + /// - Follows paths very smoothly. + /// - Can keep a desired distance to walls. + /// - Can approach its destination with a desired facing direction. + /// + /// <b>%ECS</b> + /// + /// This script uses Unity's ECS (Entity Component System) to move the agent. This means it is highly-performant and is able to utilize multiple threads. + /// Internally, an entity is created for the agent with the following components: + /// + /// - LocalTransform + /// - <see cref="MovementState"/> + /// - <see cref="MovementSettings"/> + /// - <see cref="MovementControl"/> + /// - <see cref="ManagedState"/> + /// - <see cref="SearchState"/> + /// - <see cref="MovementStatistics"/> + /// - <see cref="AgentCylinderShape"/> + /// - <see cref="ResolvedMovement"/> + /// - <see cref="GravityState"/> + /// - <see cref="DestinationPoint"/> + /// - <see cref="AgentMovementPlane"/> + /// - <see cref="SimulateMovement"/> - tag component (if <see cref="canMove"/> is enabled) + /// - <see cref="SimulateMovementRepair"/> - tag component + /// - <see cref="SimulateMovementControl"/> - tag component + /// - <see cref="SimulateMovementFinalize"/> - tag component + /// - <see cref="SyncPositionWithTransform"/> - tag component (if <see cref="updatePosition"/> is enabled) + /// - <see cref="SyncRotationWithTransform"/> - tag component (if <see cref="updateRotation"/> is enabled) + /// - <see cref="OrientationYAxisForward"/> - tag component (if <see cref="orientation"/> is <see cref="OrientationMode"/>.YAxisForward) + /// - <see cref="ECS.RVO.RVOAgent"/> (if local avoidance is enabled) + /// + /// Then this script barely does anything by itself. It is a thin wrapper around the ECS components. + /// Instead, actual movement calculations are carried out by the following systems: + /// + /// - <see cref="SyncTransformsToEntitiesSystem"/> - Updates the entity's transform from the GameObject. + /// - <see cref="MovementPlaneFromGraphSystem"/> - Updates the agent's movement plane. + /// - <see cref="SyncDestinationTransformSystem"/> - Updates the destination point if the destination transform moves. + /// - <see cref="FollowerControlSystem"/> - Calculates how the agent wants to move. + /// - <see cref="RVOSystem"/> - Local avoidance calculations. + /// - <see cref="FallbackResolveMovementSystem"/> - NOOP system for if local avoidance is disabled. + /// - <see cref="AIMoveSystem"/> - Performs the actual movement. + /// + /// In fact, as long as you create the appropriate ECS components, you do not even need this script. You can use the systems directly. + /// + /// This is <b>not</b> a baked component. That is, this script will continue to work even in standalone games. It is designed to be easily used + /// without having to care too much about the underlying ECS implementation. + /// + /// <b>Differences compared to AIPath and RichAI</b> + /// + /// This movement script has been written to remedy several inconsistency issues with other movement scrips, to provide very smooth movement, + /// and "just work" for most games. + /// + /// For example, it goes to great lengths to ensure + /// that the <see cref="reachedDestination"/> and <see cref="reachedEndOfPath"/> properties are as accurate as possible at all times, even before it has had time to recalculate its path to account for a new <see cref="destination"/>. + /// It does this by locally repairing the path (if possible) immediately when the destination changes instead of waiting for a path recalculation. + /// This also has a bonus effect that the agent can often work just fine with moving targets, even if it almost never recalculates its path (though the repaired path may not always be optimal), + /// and it leads to very responsive movement. + /// + /// In contrast to other movement scripts, this movement script does not use path modifiers at all. + /// Instead, this script contains its own internal <see cref="FunnelModifier"/> which it uses to simplify the path before it follows it. + /// In also doesn't use a separate <see cref="RVOController"/> component for local avoidance, but instead it stores local avoidance settings in <see cref="rvoSettings"/>. + /// + /// <b>Best practices for good performance</b> + /// + /// Using ECS components has some downsides. Accessing properties on this script is significantly slower compared to accessing properties on other movement scripts. + /// This is because on each property access, the script has to make sure no jobs are running concurrently, which is a relatively expensive operation. + /// Slow is a relative term, though. This only starts to matter if you have lots of agents, maybe a hundred or so. So don't be scared of using it. + /// + /// But if you have a lot of agents, it is recommended to not access properties on this script more often than required. Avoid setting fields to the same value over and over again every frame, for example. + /// If you have a moving target, try to use the <see cref="AIDestinationSetter"/> component instead of setting the <see cref="destination"/> property manually, as that is faster than setting the <see cref="destination"/> property every frame. + /// + /// You can instead write custom ECS systems to access the properties on the ECS components directly. This is much faster. + /// For example, if you want to make the agent follow a particular entity, you could create a new DestinationEntity component which just holds an entity reference, + /// and then create a system that every frame copies that entity's position to the <see cref="DestinationPoint.destination"/> field (a component that this entity will always have). + /// + /// This script has some optional parts. Local avoidance, for example. Local avoidance is used to make sure that agents do not overlap each other. + /// However, if you do not need it, you can disable it to improve performance. + /// </summary> + [AddComponentMenu("Pathfinding/AI/Follower Entity (2D,3D)")] + [UniqueComponent(tag = "ai")] + [UniqueComponent(tag = "rvo")] + public sealed partial class FollowerEntity : VersionedMonoBehaviour, IAstarAI, ISerializationCallbackReceiver { + [SerializeField] + AgentCylinderShape shape = new AgentCylinderShape { + height = 2, + radius = 0.5f, + }; + [SerializeField] + MovementSettings movement = new MovementSettings { + follower = new PIDMovement { + rotationSpeed = 600, + speed = 5, + maxRotationSpeed = 720, + maxOnSpotRotationSpeed = 720, + slowdownTime = 0.5f, + desiredWallDistance = 0.5f, + allowRotatingOnSpot = true, + leadInRadiusWhenApproachingDestination = 1f, + }, + stopDistance = 0.2f, + rotationSmoothing = 0f, + groundMask = -1, + isStopped = false, + }; + + [SerializeField] + ManagedState managedState = new ManagedState { + enableLocalAvoidance = false, + pathfindingSettings = PathRequestSettings.Default, + }; + + [SerializeField] + ECS.AutoRepathPolicy autoRepathBacking = ECS.AutoRepathPolicy.Default; + + /// <summary> + /// Determines which direction the agent moves in. + /// + /// See: <see cref="orientation"/> + /// </summary> + [SerializeField] + OrientationMode orientationBacking; + [SerializeField] + MovementPlaneSource movementPlaneSourceBacking = MovementPlaneSource.Graph; + + /// <summary>Cached transform component</summary> + Transform tr; + + /// <summary> + /// Entity which this movement script represents. + /// + /// An entity will be created when this script is enabled, and destroyed when this script is disabled. + /// + /// Check the class documentation to see which components it usually has, and what systems typically affect it. + /// </summary> + public Entity entity { get; private set; } + + static EntityAccess<DestinationPoint> destinationPointAccessRW = new EntityAccess<DestinationPoint>(false); + static EntityAccess<DestinationPoint> destinationPointAccessRO = new EntityAccess<DestinationPoint>(true); + static EntityAccess<AgentMovementPlane> movementPlaneAccessRW = new EntityAccess<AgentMovementPlane>(false); + static EntityAccess<AgentMovementPlane> movementPlaneAccessRO = new EntityAccess<AgentMovementPlane>(false); + static EntityAccess<MovementState> movementStateAccessRW = new EntityAccess<MovementState>(false); + static EntityAccess<MovementState> movementStateAccessRO = new EntityAccess<MovementState>(true); + static EntityAccess<MovementStatistics> movementOutputAccessRW = new EntityAccess<MovementStatistics>(false); + static EntityAccess<ResolvedMovement> resolvedMovementAccessRO = new EntityAccess<ResolvedMovement>(true); + static EntityAccess<ResolvedMovement> resolvedMovementAccessRW = new EntityAccess<ResolvedMovement>(false); + static EntityAccess<MovementControl> movementControlAccessRO = new EntityAccess<MovementControl>(true); + static EntityAccess<MovementControl> movementControlAccessRW = new EntityAccess<MovementControl>(false); + static ManagedEntityAccess<ManagedState> managedStateAccessRO = new ManagedEntityAccess<ManagedState>(true); + static ManagedEntityAccess<ManagedState> managedStateAccessRW = new ManagedEntityAccess<ManagedState>(false); + static EntityAccess<ECS.AutoRepathPolicy> autoRepathPolicyRW = new EntityAccess<ECS.AutoRepathPolicy>(false); + static EntityAccess<LocalTransform> localTransformAccessRO = new EntityAccess<LocalTransform>(true); + static EntityAccess<LocalTransform> localTransformAccessRW = new EntityAccess<LocalTransform>(false); + static EntityAccess<AgentCylinderShape> agentCylinderShapeAccessRO = new EntityAccess<AgentCylinderShape>(true); + static EntityAccess<AgentCylinderShape> agentCylinderShapeAccessRW = new EntityAccess<AgentCylinderShape>(false); + static EntityAccess<MovementSettings> movementSettingsAccessRO = new EntityAccess<MovementSettings>(true); + static EntityAccess<MovementSettings> movementSettingsAccessRW = new EntityAccess<MovementSettings>(false); + static EntityAccess<AgentOffMeshLinkTraversal> agentOffMeshLinkTraversalRO = new EntityAccess<AgentOffMeshLinkTraversal>(true); + static EntityAccess<ReadyToTraverseOffMeshLink> readyToTraverseOffMeshLinkRW = new EntityAccess<ReadyToTraverseOffMeshLink>(false); + static EntityStorageCache entityStorageCache; + + static EntityArchetype archetype; + static World achetypeWorld; + + void OnEnable () { + scratchReferenceCount++; + + var world = World.DefaultGameObjectInjectionWorld; + if (!archetype.Valid || achetypeWorld != world) { + if (world == null) throw new Exception("World.DefaultGameObjectInjectionWorld is null. Has the world been destroyed?"); + achetypeWorld = world; + archetype = world.EntityManager.CreateArchetype( + typeof(LocalTransform), + typeof(MovementState), + typeof(MovementSettings), + typeof(ECS.AutoRepathPolicy), + typeof(MovementControl), + typeof(ManagedState), + typeof(SearchState), + typeof(MovementStatistics), + typeof(AgentCylinderShape), + typeof(ResolvedMovement), + typeof(DestinationPoint), + typeof(AgentMovementPlane), + typeof(GravityState), + typeof(SimulateMovement), + typeof(SimulateMovementRepair), + typeof(SimulateMovementControl), + typeof(SimulateMovementFinalize), + typeof(SyncPositionWithTransform), + typeof(SyncRotationWithTransform), + typeof(ReadyToTraverseOffMeshLink), + typeof(AgentMovementPlaneSource) + ); + } + + FindComponents(); + + entity = world.EntityManager.CreateEntity(archetype); + var pos = tr.position; + // This GameObject may be in a hierarchy, but the entity will not be. So we copy the world orientation to the entity's local transform component + world.EntityManager.SetComponentData(entity, LocalTransform.FromPositionRotationScale(pos, tr.rotation, tr.localScale.x)); + world.EntityManager.SetComponentData(entity, new MovementState(pos)); +#if UNITY_EDITOR + world.EntityManager.SetName(entity, "Follower Entity"); +#endif + // Set the initial movement plane. This will be overriden before the first simulation loop runs. + world.EntityManager.SetComponentData(entity, new AgentMovementPlane(tr.rotation)); + world.EntityManager.SetComponentData(entity, new DestinationPoint { + destination = new float3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity), + }); + autoRepathBacking.Reset(); + world.EntityManager.SetComponentData(entity, autoRepathBacking); + world.EntityManager.SetComponentData(entity, movement); + if (!managedState.pathTracer.isCreated) { + managedState.pathTracer = new PathTracer(Allocator.Persistent); + } + world.EntityManager.SetComponentData(entity, managedState); + world.EntityManager.SetComponentData(entity, new MovementStatistics { + estimatedVelocity = float3.zero, + lastPosition = pos, + }); + world.EntityManager.SetComponentData(entity, shape); + world.EntityManager.SetComponentEnabled<GravityState>(entity, managedState.enableGravity); + if (orientation == OrientationMode.YAxisForward) { + world.EntityManager.AddComponent<OrientationYAxisForward>(entity); + } + world.EntityManager.SetComponentEnabled<ReadyToTraverseOffMeshLink>(entity, false); + world.EntityManager.SetSharedComponent(entity, new AgentMovementPlaneSource { value = movementPlaneSourceBacking }); + + // Register with the BatchedEvents system + // This is used not for the events, but because it keeps track of a TransformAccessArray + // of all components. This is then used by the SyncEntitiesToTransformsJob. + BatchedEvents.Add(this, BatchedEvents.Event.None, (components, ev) => {}); + + var runtimeBakers = GetComponents<IRuntimeBaker>(); + for (int i = 0; i < runtimeBakers.Length; i++) if (((MonoBehaviour)runtimeBakers[i]).enabled) runtimeBakers[i].OnCreatedEntity(world, entity); + } + + internal void RegisterRuntimeBaker (IRuntimeBaker baker) { + if (entityExists) baker.OnCreatedEntity(World.DefaultGameObjectInjectionWorld, entity); + } + + void Start () { + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + managedStateAccessRW.Update(entityManager); + movementPlaneAccessRO.Update(entityManager); + if (!managedState.pathTracer.hasPath && AstarPath.active != null) { + var nearest = AstarPath.active.GetNearest(position, NNConstraint.Walkable); + if (nearest.node != null) { + var storage = entityManager.GetStorageInfo(entity); + var movementPlane = movementPlaneAccessRO[storage]; + managedState.pathTracer.SetFromSingleNode(nearest.node, nearest.position, movementPlane.value); + managedState.pathTracer.UpdateEnd(new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity), PathTracer.RepairQuality.High, movementPlane.value, null, null); + } + } + } + + /// <summary> + /// Called when the component is disabled or about to be destroyed. + /// + /// This is also called by Unity when an undo/redo event is performed. This means that + /// when an undo event happens the entity will get destroyed and then re-created. + /// </summary> + void OnDisable () { + scratchReferenceCount--; + if (scratchReferenceCount == 0) { + if (indicesScratch.IsCreated) indicesScratch.Dispose(); + if (nextCornersScratch.IsCreated) nextCornersScratch.Dispose(); + } + + BatchedEvents.Remove(this); + CancelCurrentPathRequest(); + if (World.DefaultGameObjectInjectionWorld != null && World.DefaultGameObjectInjectionWorld.IsCreated) World.DefaultGameObjectInjectionWorld.EntityManager.DestroyEntity(entity); + managedState.pathTracer.Dispose(); + } + + /// <summary>\copydoc Pathfinding::IAstarAI::radius</summary> + public float radius { + get => shape.radius; + set { + this.shape.radius = value; + if (entityStorageCache.GetComponentData(entity, ref agentCylinderShapeAccessRW, out var shape)) { + shape.value.radius = value; + } + } + } + + /// <summary> + /// Height of the agent in world units. + /// This is visualized in the scene view as a yellow cylinder around the character. + /// + /// This value is used for various heuristics, and for visualization purposes. + /// For example, the destination is only considered reached if the destination is not above the agent's head, and it's not more than half the agent's height below its feet. + /// + /// If local lavoidance is enabled, this is also used to filter out collisions with agents and obstacles that are too far above or below the agent. + /// </summary> + public float height { + get => shape.height; + set { + this.shape.height = value; + if (entityStorageCache.GetComponentData(entity, ref agentCylinderShapeAccessRW, out var shape)) { + shape.value.height = value; + } + } + } + + /// <summary>Pathfinding settings</summary> + public ref PathRequestSettings pathfindingSettings { + get { + // Complete any job dependencies + // Need RW because this getter has a ref return. + entityStorageCache.GetComponentData(entity, ref movementStateAccessRW, out var _); + return ref managedState.pathfindingSettings; + } + } + + /// <summary>Local avoidance settings</summary> + public ref RVOAgent rvoSettings { + get { + // Complete any job dependencies + // Need RW because this getter has a ref return. + entityStorageCache.GetComponentData(entity, ref movementStateAccessRW, out var _); + return ref managedState.rvoSettings; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::position</summary> + public Vector3 position { + get { + // Make sure we are not waiting for a job to update the world position + if (entityStorageCache.GetComponentData(entity, ref localTransformAccessRO, out var localTransform)) { + return localTransform.value.Position; + } else { + return transform.position; + } + } + set { + if (entityStorageCache.Update(World.DefaultGameObjectInjectionWorld, entity, out var entityManager, out var storage)) { + // Update path and other properties using our new position + if (entityManager.HasComponent<SyncPositionWithTransform>(entity)) { + transform.position = value; + } + movementStateAccessRW.Update(entityManager); + managedStateAccessRW.Update(entityManager); + agentCylinderShapeAccessRO.Update(entityManager); + movementSettingsAccessRO.Update(entityManager); + destinationPointAccessRO.Update(entityManager); + movementPlaneAccessRO.Update(entityManager); + localTransformAccessRW.Update(entityManager); + readyToTraverseOffMeshLinkRW.Update(entityManager); + + ref var localTransform = ref localTransformAccessRW[storage]; + localTransform.Position = value; + ref var movementState = ref movementStateAccessRW[storage]; + movementState.positionOffset = float3.zero; + if (managedState.pathTracer.hasPath) { + Profiler.BeginSample("RepairStart"); + ref var movementPlane = ref movementPlaneAccessRO[storage]; + var oldVersion = managedState.pathTracer.version; + managedState.pathTracer.UpdateStart(value, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + Profiler.EndSample(); + if (managedState.pathTracer.version != oldVersion) { + Profiler.BeginSample("EstimateNative"); + ref var shape = ref agentCylinderShapeAccessRO[storage]; + ref var movementSettings = ref movementSettingsAccessRO[storage]; + ref var destinationPoint = ref destinationPointAccessRO[storage]; + var readyToTraverseOffMeshLink = storage.Chunk.GetEnabledMask(ref readyToTraverseOffMeshLinkRW.handle).GetEnabledRefRW<ReadyToTraverseOffMeshLink>(storage.IndexInChunk); + if (!nextCornersScratch.IsCreated) nextCornersScratch = new NativeList<float3>(4, Allocator.Persistent); + JobRepairPath.Execute( + ref localTransform, + ref movementState, + ref shape, + ref movementPlane, + ref destinationPoint, + readyToTraverseOffMeshLink, + managedState, + in movementSettings, + nextCornersScratch, + ref indicesScratch, + Allocator.Persistent, + false + ); + Profiler.EndSample(); + } + } + } else { + transform.position = value; + } + } + } + + /// <summary> + /// True if the agent is currently traversing an off-mesh link. + /// + /// See: offmeshlinks (view in online documentation for working links) + /// See: <see cref="onTraverseOffMeshLink"/> + /// See: <see cref="offMeshLink"/> + /// </summary> + public bool isTraversingOffMeshLink { + get { + if (!entityExists) return false; + + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + return entityManager.HasComponent<AgentOffMeshLinkTraversal>(entity); + } + } + + /// <summary> + /// The off-mesh link that the agent is currently traversing. + /// + /// This will be a default <see cref="OffMeshLinks.OffMeshLinkTracer"/> if the agent is not traversing an off-mesh link (the <see cref="OffMeshLinks.OffMeshLinkTracer.link"/> field will be null). + /// + /// Note: If the off-mesh link is destroyed while the agent is traversing it, this property will still return the link. + /// But be careful about accessing properties like <see cref="OffMeshLinkSource.gameObject"/>, as that may refer to a destroyed gameObject. + /// + /// See: offmeshlinks (view in online documentation for working links) + /// See: <see cref="onTraverseOffMeshLink"/> + /// See: <see cref="isTraversingOffMeshLink"/> + /// </summary> + public OffMeshLinks.OffMeshLinkTracer offMeshLink { + get { + if (entityStorageCache.Update(World.DefaultGameObjectInjectionWorld, entity, out var entityManager, out var storage) && entityManager.HasComponent<ManagedAgentOffMeshLinkTraversal>(entity)) { + agentOffMeshLinkTraversalRO.Update(entityManager); + var linkTraversal = agentOffMeshLinkTraversalRO[storage]; + var linkTraversalManaged = entityManager.GetComponentData<ManagedAgentOffMeshLinkTraversal>(entity); + return new OffMeshLinks.OffMeshLinkTracer(linkTraversalManaged.context.concreteLink, linkTraversal.relativeStart, linkTraversal.relativeEnd, linkTraversal.isReverse); + } else { + return default; + } + } + } + + /// <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 off-mesh link (<see cref="NodeLink2.onTraverseOffMeshLink"/>) to specify a callback for a specific off-mesh link. + /// + /// Note: The agent's off-mesh link handler takes precedence over the link's off-mesh link handler, if both are set. + /// + /// See: offmeshlinks (view in online documentation for working links) for more details and example code + /// See: <see cref="isTraversingOffMeshLink"/> + /// </summary> + public IOffMeshLinkHandler onTraverseOffMeshLink { + get => managedState.onTraverseOffMeshLink; + set { + // Complete any job dependencies + entityStorageCache.GetComponentData(entity, ref movementStateAccessRW, out var _); + managedState.onTraverseOffMeshLink = value; + } + } + + /// <summary> + /// Node which the agent is currently traversing. + /// + /// You can, for example, use this to make the agent use a different animation when traversing nodes with a specific tag. + /// + /// Note: Will be null if the agent does not have a path, or if the node under the agent has just been destroyed by a graph update. + /// + /// When traversing an off-mesh link, this will return the final non-link node in the path before the agent started traversing the link. + /// </summary> + public GraphNode currentNode { + get { + if (!entityExists) return null; + + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + // Complete any job dependencies + managedStateAccessRO.Update(entityManager); + var node = managedState.pathTracer.startNode; + if (node == null || node.Destroyed) return null; + return node; + } + } + + /// <summary> + /// Rotation of the agent. + /// In world space. + /// + /// The entity internally always treats the Z axis as forward, but this property respects the <see cref="orientation"/> field. So it + /// will return either a rotation with the Y axis as forward, or Z axis as forward, depending on the <see cref="orientation"/> field. + /// + /// This will return the agent's rotation even if <see cref="updateRotation"/> is false. + /// + /// See: <see cref="position"/> + /// </summary> + public Quaternion rotation { + get { + if (entityStorageCache.GetComponentData(entity, ref localTransformAccessRO, out var localTransform)) { + var r = localTransform.value.Rotation; + if (orientation == OrientationMode.YAxisForward) r = math.mul(r, SyncTransformsToEntitiesSystem.ZAxisForwardToYAxisForward); + return r; + } else { + return transform.rotation; + } + } + set { + if (entityStorageCache.Update(World.DefaultGameObjectInjectionWorld, entity, out var entityManager, out var storage)) { + // Update path and other properties using our new position + if (entityManager.HasComponent<SyncRotationWithTransform>(entity)) { + transform.rotation = value; + } + + if (orientation == OrientationMode.YAxisForward) value = math.mul(value, SyncTransformsToEntitiesSystem.YAxisForwardToZAxisForward); + localTransformAccessRW.Update(entityManager); + localTransformAccessRW[storage].Rotation = value; + } else { + transform.rotation = value; + } + } + } + + /// <summary> + /// How to calculate which direction is "up" for the agent. + /// + /// In almost all cases, you should use the Graph option. This will make the agent use the graph's natural "up" direction. + /// However, if you are using a spherical world, or a world with some other strange shape, then you may want to use the NavmeshNormal or Raycast options. + /// + /// See: spherical (view in online documentation for working links) + /// </summary> + public MovementPlaneSource movementPlaneSource { + get => movementPlaneSourceBacking; + set { + movementPlaneSourceBacking = value; + if (entityExists) { + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + entityManager.SetSharedComponent(entity, new AgentMovementPlaneSource { value = value }); + } + } + } + + /// <summary> + /// Determines which layers the agent will stand on. + /// + /// The agent will use a raycast each frame to check if it should stop falling. + /// + /// This layer mask should ideally not contain the agent's own layer, if the agent has a collider, + /// as this may cause it to try to stand on top of itself. + /// </summary> + public LayerMask groundMask { + get => movement.groundMask; + set { + movement.groundMask = value; + if (entityStorageCache.GetComponentData(entity, ref movementSettingsAccessRW, out var movementSettings)) { + movementSettings.value.groundMask = value; + } + } + } + + /// <summary> + /// Enables or disables debug drawing for this agent. + /// + /// This is a bitmask with multiple flags so that you can choose exactly what you want to debug. + /// + /// See: <see cref="PIDMovement.DebugFlags"/> + /// See: bitmasks (view in online documentation for working links) + /// </summary> + public PIDMovement.DebugFlags debugFlags { + get => movement.debugFlags; + set { + movement.debugFlags = value; + if (entityStorageCache.GetComponentData(entity, ref movementSettingsAccessRW, out var movementSettings)) { + movementSettings.value.debugFlags = value; + } + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary> + public float maxSpeed { + get => movement.follower.speed; + set { + movement.follower.speed = value; + if (entityStorageCache.GetComponentData(entity, ref movementSettingsAccessRW, out var movementSettings)) { + movementSettings.value.follower.speed = value; + } + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::velocity</summary> + public Vector3 velocity => entityExists ? (Vector3)World.DefaultGameObjectInjectionWorld.EntityManager.GetComponentData<MovementStatistics>(entity).estimatedVelocity : Vector3.zero; + + /// <summary>\copydoc Pathfinding::IAstarAI::desiredVelocity</summary> + public Vector3 desiredVelocity { + get { + if (entityStorageCache.GetComponentData(entity, ref resolvedMovementAccessRO, out var resolvedMovement)) { + var dt = Mathf.Max(Time.deltaTime, 0.0001f); + return Vector3.ClampMagnitude((Vector3)resolvedMovement.value.targetPoint - position, dt * resolvedMovement.value.speed) / dt; + } else { + return Vector3.zero; + } + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::desiredVelocityWithoutLocalAvoidance</summary> + public Vector3 desiredVelocityWithoutLocalAvoidance { + get { + if (entityStorageCache.GetComponentData(entity, ref movementControlAccessRO, out var movementControl)) { + var dt = Mathf.Max(Time.deltaTime, 0.0001f); + return Vector3.ClampMagnitude((Vector3)movementControl.value.targetPoint - position, dt * movementControl.value.speed) / dt; + } else { + return Vector3.zero; + } + } + set => throw new NotImplementedException("The FollowerEntity does not support setting this property. If you want to override the movement, you'll need to write a custom entity component system."); + } + + /// <summary> + /// Approximate remaining distance along the current path to the end of the path. + /// + /// The agent does not know the true distance at all times, so this is an approximation. + /// It tends to be a bit lower than the true distance. + /// + /// Note: This is the distance to the end of the path, which may or may not be the same as the destination. + /// If the character cannot reach the destination it will try to move as close as possible to it. + /// + /// This value will update immediately if the <see cref="destination"/> property is changed, or if the agent is moved using the <see cref="position"/> property or the <see cref="Teleport"/> method. + /// + /// If the agent has no path, or if the current path is stale (e.g. if the graph has been updated close to the agent, and it hasn't had time to recalculate its path), this will return positive infinity. + /// + /// See: <see cref="reachedDestination"/> + /// See: <see cref="reachedEndOfPath"/> + /// See: <see cref="pathPending"/> + /// </summary> + public float remainingDistance { + get { + if (!entityStorageCache.Update(World.DefaultGameObjectInjectionWorld, entity, out var entityManager, out var storage)) return float.PositiveInfinity; + + movementStateAccessRO.Update(entityManager); + managedStateAccessRO.Update(entityManager); + // TODO: Should this perhaps only check if the start/end points are stale, and ignore the case when the graph is updated and some nodes are destroyed? + if (managedState.pathTracer.hasPath && !managedState.pathTracer.isStale) { + ref var movementState = ref movementStateAccessRO[storage]; + return movementState.remainingDistanceToEndOfPart + Vector3.Distance(managedState.pathTracer.endPointOfFirstPart, managedState.pathTracer.endPoint); + } else { + return float.PositiveInfinity; + } + } + } + + /// <summary>\copydocref{MovementSettings.stopDistance}</summary> + public float stopDistance { + get => movement.stopDistance; + set { + if (movement.stopDistance != value) { + movement.stopDistance = value; + if (entityStorageCache.GetComponentData(entity, ref movementSettingsAccessRW, out var movementSettings)) { + movementSettings.value.stopDistance = value; + } + } + } + } + + /// <summary>\copydocref{MovementSettings.rotationSmoothing}</summary> + public float rotationSmoothing { + get => movement.rotationSmoothing; + set { + if (movement.rotationSmoothing != value) { + movement.rotationSmoothing = value; + if (entityStorageCache.GetComponentData(entity, ref movementSettingsAccessRW, out var movementSettings)) { + movementSettings.value.rotationSmoothing = value; + } + } + } + } + + /// <summary> + /// True if the ai has reached the <see cref="destination"/>. + /// + /// The agent considers the destination reached when it is within <see cref="stopDistance"/> world units from the <see cref="destination"/>. + /// Additionally, the destination must not be above the agent's head, and it must not be more than half the agent's height below its feet. + /// + /// If a facing direction was specified when setting the destination, this will only return true once the agent is approximately facing the correct orientation. + /// + /// This value will be updated immediately when the <see cref="destination"/> is changed. + /// + /// <code> + /// IEnumerator Start () { + /// ai.destination = somePoint; + /// // Start to search for a path to the destination immediately + /// ai.SearchPath(); + /// // Wait until the agent has reached the destination + /// while (!ai.reachedDestination) { + /// yield return null; + /// } + /// // The agent has reached the destination now + /// } + /// </code> + /// + /// Note: The agent may not be able to reach the destination. In that case this property may never become true. Sometimes <see cref="reachedEndOfPath"/> is more appropriate. + /// + /// See: <see cref="stopDistance"/> + /// See: <see cref="remainingDistance"/> + /// See: <see cref="reachedEndOfPath"/> + /// </summary> + public bool reachedDestination => entityStorageCache.GetComponentData(entity, ref movementStateAccessRW, out var movementState) ? movementState.value.reachedDestinationAndOrientation : false; + + /// <summary> + /// True if the agent has reached the end of the current path. + /// + /// The agent considers the end of the path reached when it is within <see cref="stopDistance"/> world units from the end of the path. + /// Additionally, the end of the path must not be above the agent's head, and it must not be more than half the agent's height below its feet. + /// + /// If a facing direction was specified when setting the destination, this will only return true once the agent is approximately facing the correct orientation. + /// + /// This value will be updated immediately when the <see cref="destination"/> is changed. + /// + /// Note: Reaching the end of the path does not imply that it has reached its desired destination, as the destination may not even be possible to reach. + /// Sometimes <see cref="reachedDestination"/> is more appropriate. + /// + /// See: <see cref="remainingDistance"/> + /// See: <see cref="reachedDestination"/> + /// </summary> + public bool reachedEndOfPath => entityStorageCache.GetComponentData(entity, ref movementStateAccessRW, out var movementState) ? movementState.value.reachedEndOfPathAndOrientation : false; + + /// <summary> + /// End point of path the agent is currently following. + /// If the agent has no path (or if it's not calculated yet), this will return the <see cref="destination"/> instead. + /// If the agent has no destination it will return the agent's current position. + /// + /// The end of the path is usually identical or very close to the <see cref="destination"/>, but it may differ + /// if the path for example was blocked by a wall, so that the agent couldn't get any closer. + /// + /// See: <see cref="GetRemainingPath"/> + /// </summary> + public Vector3 endOfPath { + get { + if (entityExists) { + // Make sure we block to ensure no managed state changes are made in jobs while we are reading from it + managedStateAccessRO.Update(World.DefaultGameObjectInjectionWorld.EntityManager); + if (hasPath) return managedState.pathTracer.endPoint; + var d = destination; + if (float.IsFinite(d.x)) return d; + } + return position; + } + } + + static NativeList<float3> nextCornersScratch; + static NativeArray<int> indicesScratch; + static int scratchReferenceCount = 0; + + /// <summary>\copydoc Pathfinding::IAstarAI::destination</summary> + + /// <summary> + /// Position in the world that this agent should move to. + /// + /// If no destination has been set yet, then (+infinity, +infinity, +infinity) will be returned. + /// + /// Setting this property will immediately try to repair the path if the agent already has a path. + /// This will also immediately update properties like <see cref="reachedDestination"/>, <see cref="reachedEndOfPath"/> and <see cref="remainingDistance"/>. + /// + /// The agent may do a full path recalculation if the local repair was not sufficient, + /// but this will at earliest happen in the next simulation step. + /// + /// <code> + /// IEnumerator Start () { + /// ai.destination = somePoint; + /// // Wait until the AI has reached the destination + /// while (!ai.reachedEndOfPath) { + /// yield return null; + /// } + /// // The agent has reached the destination now + /// } + /// </code> + /// + /// See: <see cref="SetDestination"/>, which also allows you to set a facing direction for the agent. + /// </summary> + public Vector3 destination { + get => entityStorageCache.GetComponentData(entity, ref destinationPointAccessRO, out var destination) ? (Vector3)destination.value.destination : Vector3.positiveInfinity; + set => SetDestination(value, default); + } + + /// <summary> + /// Direction the agent will try to face when it reaches the destination. + /// + /// If this is zero, the agent will not try to face any particular direction. + /// + /// The following video shows three agents, one with no facing direction set, and then two agents with varying values of the <see cref="PIDMovement.leadInRadiusWhenApproachingDestination;lead in radius"/>. + /// [Open online documentation to see videos] + /// + /// See: <see cref="MovementSettings.follower.leadInRadiusWhenApproachingDestination"/> + /// See: <see cref="SetDestination"/> + /// </summary> + Vector3 destinationFacingDirection { + get => entityStorageCache.GetComponentData(entity, ref destinationPointAccessRO, out var destination) ? (Vector3)destination.value.facingDirection : Vector3.zero; + } + + /// <summary> + /// Set the position in the world that this agent should move to. + /// + /// This method will immediately try to repair the path if the agent already has a path. + /// This will also immediately update properties like <see cref="reachedDestination"/>, <see cref="reachedEndOfPath"/> and <see cref="remainingDistance"/>. + /// The agent may do a full path recalculation if the local repair was not sufficient, + /// but this will at earliest happen in the next simulation step. + /// + /// If you are setting a destination and want to know when the agent has reached that destination, + /// then you could use either <see cref="reachedDestination"/> or <see cref="reachedEndOfPath"/>. + /// + /// You may also set a facing direction for the agent. If set, the agent will try to approach the destination point + /// with the given heading. <see cref="reachedDestination"/> and <see cref="reachedEndOfPath"/> will only return true once the agent is approximately facing the correct direction. + /// The <see cref="MovementSettings.follower.leadInRadiusWhenApproachingDestination"/> field controls how wide an arc the agent will try to use when approaching the destination. + /// + /// The following video shows three agents, one with no facing direction set, and then two agents with varying values of the <see cref="PIDMovement.leadInRadiusWhenApproachingDestination;lead in radius"/>. + /// [Open online documentation to see videos] + /// + /// <code> + /// IEnumerator Start () { + /// ai.SetDestination(somePoint, Vector3.right); + /// // Wait until the AI has reached the destination and is rotated to the right in world space + /// while (!ai.reachedEndOfPath) { + /// yield return null; + /// } + /// // The agent has reached the destination now + /// } + /// </code> + /// + /// See: <see cref="destination"/> + /// </summary> + public void SetDestination (float3 destination, float3 facingDirection = default) { + AssertEntityExists(); + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + movementStateAccessRW.Update(entityManager); + managedStateAccessRW.Update(entityManager); + agentCylinderShapeAccessRO.Update(entityManager); + movementSettingsAccessRO.Update(entityManager); + localTransformAccessRO.Update(entityManager); + destinationPointAccessRW.Update(entityManager); + movementPlaneAccessRO.Update(entityManager); + readyToTraverseOffMeshLinkRW.Update(entityManager); + + var storage = entityManager.GetStorageInfo(entity); + destinationPointAccessRW[storage] = new DestinationPoint { + destination = destination, + facingDirection = facingDirection, + }; + + // If we already have a path, we try to repair it immediately. + // This ensures that the #reachedDestination and #reachedEndOfPath flags are as up to date as possible. + if (managedState.pathTracer.hasPath) { + Profiler.BeginSample("RepairEnd"); + ref var movementPlane = ref movementPlaneAccessRO[storage]; + managedState.pathTracer.UpdateEnd(destination, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + Profiler.EndSample(); + ref var movementState = ref movementStateAccessRW[storage]; + if (movementState.pathTracerVersion != managedState.pathTracer.version) { + Profiler.BeginSample("EstimateNative"); + ref var shape = ref agentCylinderShapeAccessRO[storage]; + ref var movementSettings = ref movementSettingsAccessRO[storage]; + ref var localTransform = ref localTransformAccessRO[storage]; + ref var destinationPoint = ref destinationPointAccessRW[storage]; + var readyToTraverseOffMeshLink = storage.Chunk.GetEnabledMask(ref readyToTraverseOffMeshLinkRW.handle).GetEnabledRefRW<ReadyToTraverseOffMeshLink>(storage.IndexInChunk); + if (!nextCornersScratch.IsCreated) nextCornersScratch = new NativeList<float3>(4, Allocator.Persistent); + JobRepairPath.Execute( + ref localTransform, + ref movementState, + ref shape, + ref movementPlane, + ref destinationPoint, + readyToTraverseOffMeshLink, + managedState, + in movementSettings, + nextCornersScratch, + ref indicesScratch, + Allocator.Persistent, + false + ); + Profiler.EndSample(); + } + } + } + + /// <summary> + /// Policy for when the agent recalculates its path. + /// + /// See: <see cref="AutoRepathPolicy"/> + /// </summary> + public ECS.AutoRepathPolicy autoRepath { + get { + return autoRepathBacking; + } + set { + autoRepathBacking = value; + if (entityStorageCache.GetComponentData(entity, ref autoRepathPolicyRW, out var component)) { + component.value = value; + } + } + } + + /// <summary> + /// \copydoc Pathfinding::IAstarAI::canSearch + /// Deprecated: This has been superseded by <see cref="autoRepath.mode"/>. + /// </summary> + [System.Obsolete("This has been superseded by autoRepath.mode")] + public bool canSearch { + get { + return autoRepathBacking.mode != AutoRepathPolicy.Mode.Never; + } + set { + if (value) { + if (autoRepathBacking.mode == AutoRepathPolicy.Mode.Never) { + autoRepathBacking.mode = AutoRepathPolicy.Mode.EveryNSeconds; + } + } else { + autoRepathBacking.mode = AutoRepathPolicy.Mode.Never; + } + // Ensure the entity date is up to date + autoRepath = autoRepathBacking; + } + } + + /// <summary> + /// Enables or disables movement completely. + /// If you want the agent to stand still, but still react to local avoidance and use gravity: use <see cref="isStopped"/> instead. + /// + /// Disabling this will remove the <see cref="SimulateMovement"/> component from the entity, which prevents + /// most systems from running for this entity. + /// + /// See: <see cref="autoRepath"/> + /// See: <see cref="isStopped"/> + /// </summary> + public bool canMove { + get { + if (!entityExists) return true; + + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + return entityManager.HasComponent<SimulateMovement>(entity); + } + set => ToggleComponent<SimulateMovement>(entity, value, true); + } + + /// <summary>\copydoc Pathfinding::IAstarAI::movementPlane</summary> + public NativeMovementPlane movementPlane => entityStorageCache.GetComponentData(entity, ref movementPlaneAccessRO, out var movementPlane) ? movementPlane.value.value : new NativeMovementPlane(rotation); + + /// <summary> + /// Enables or disables gravity. + /// + /// If gravity is enabled, the agent will accelerate downwards, and use a raycast to check if it should stop falling. + /// + /// See: <see cref="groundMask"/> + /// </summary> + public bool enableGravity { + get { + if (!entityExists) return managedState.enableGravity; + + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + return entityManager.HasComponent<GravityState>(entity); + } + set { + if (managedState.enableGravity != value) { + managedState.enableGravity = value; + ToggleComponentEnabled<GravityState>(entity, value, false); + } + } + } + + /// <summary>\copydocref{ManagedState.enableLocalAvoidance}</summary> + public bool enableLocalAvoidance { + get => managedState.enableLocalAvoidance; + set => managedState.enableLocalAvoidance = value; + } + + /// <summary> + /// Determines if the character's position should be coupled to the Transform's position. + /// If false then all movement calculations will happen as usual, but the GameObject that this component is attached to will not move. + /// Instead, only the <see cref="position"/> property and the internal entity's position will change. + /// + /// This is useful if you want to control the movement of the character using some other means, such + /// as root motion, but still want the AI to move freely. + /// + /// See: <see cref="canMove"/> which in contrast to this field will disable all movement calculations. + /// See: <see cref="updateRotation"/> + /// </summary> + public bool updatePosition { + get { + if (!entityExists) return true; + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + return entityManager.HasComponent<SyncPositionWithTransform>(entity); + } + set => ToggleComponent<SyncPositionWithTransform>(entity, value, true); + } + + /// <summary> + /// Determines which direction the agent moves in. + /// For 3D games you most likely want the ZAxisIsForward option as that is the convention for 3D games. + /// For 2D games you most likely want the YAxisIsForward option as that is the convention for 2D games. + /// + /// When using ZAxisForard, the +Z axis will be the forward direction of the agent, +Y will be upwards, and +X will be the right direction. + /// When using YAxisForward, the +Y axis will be the forward direction of the agent, +Z will be upwards, and +X will be the right direction. + /// + /// [Open online documentation to see images] + /// </summary> + public OrientationMode orientation { + get => orientationBacking; + set { + if (orientationBacking != value) { + orientationBacking = value; + ToggleComponent<OrientationYAxisForward>(entity, value == OrientationMode.YAxisForward, false); + } + } + } + + /// <summary> + /// Determines if the character's rotation should be coupled to the Transform's rotation. + /// If false then all movement calculations will happen as usual, but the GameObject that this component is attached to will not rotate. + /// Instead, only the <see cref="rotation"/> property and the internal entity's rotation will change. + /// + /// You can enable <see cref="PIDMovement.DebugFlags"/>.Rotation in <see cref="debugFlags"/> to draw a gizmos arrow in the scene view to indicate the agent's internal rotation. + /// + /// See: <see cref="updatePosition"/> + /// See: <see cref="rotation"/> + /// See: <see cref="orientation"/> + /// </summary> + public bool updateRotation { + get { + if (!entityExists) return true; + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + return entityManager.HasComponent<SyncRotationWithTransform>(entity); + } + set => ToggleComponent<SyncRotationWithTransform>(entity, value, true); + } + + /// <summary>Adds or removes a component from an entity</summary> + static void ToggleComponent<T>(Entity entity, bool enabled, bool mustExist) where T : struct, IComponentData { + var world = World.DefaultGameObjectInjectionWorld; + if (world == null || !world.EntityManager.Exists(entity)) { + if (!mustExist) throw new System.InvalidOperationException("Entity does not exist. You can only access this if the component is active and enabled."); + return; + } + if (enabled) { + world.EntityManager.AddComponent<T>(entity); + } else { + world.EntityManager.RemoveComponent<T>(entity); + } + } + + /// <summary>Enables or disables a component on an entity</summary> + static void ToggleComponentEnabled<T>(Entity entity, bool enabled, bool mustExist) where T : struct, IComponentData, IEnableableComponent { + var world = World.DefaultGameObjectInjectionWorld; + if (world == null || !world.EntityManager.Exists(entity)) { + if (!mustExist) throw new System.InvalidOperationException("Entity does not exist. You can only access this if the component is active and enabled."); + return; + } + world.EntityManager.SetComponentEnabled<T>(entity, enabled); + } + + /// <summary> + /// True if this agent currently has a valid path that it follows. + /// + /// This is true if the agent has a path and the path is not stale. + /// + /// A path may become stale if the graph is updated close to the agent and it hasn't had time to recalculate its path yet. + /// </summary> + public bool hasPath { + get { + // Ensure no jobs are writing to the managed state while we are reading from it + if (entityExists) managedStateAccessRO.Update(World.DefaultGameObjectInjectionWorld.EntityManager); + return !managedState.pathTracer.isStale; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary> + public bool pathPending { + get { + if (!entityExists) return false; + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + managedStateAccessRO.Update(entityManager); + return managedState.pendingPath != null; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::isStopped</summary> + public bool isStopped { + get => movement.isStopped; + set { + if (movement.isStopped != value) { + movement.isStopped = value; + if (entityStorageCache.GetComponentData(entity, ref movementSettingsAccessRW, out var movementSettings)) { + movementSettings.value.isStopped = value; + } + } + } + } + + /// <summary> + /// Various movement settings. + /// + /// Some of these settings are exposed on the FollowerEntity directly. For example <see cref="maxSpeed"/>. + /// + /// Note: The return value is a struct. If you want to change some settings, you'll need to modify the returned struct and then assign it back to this property. + /// </summary> + public MovementSettings movementSettings { + get => movement; + set { + movement = value; + if (entityStorageCache.GetComponentData(entity, ref movementSettingsAccessRW, out var movementSettings)) { + movementSettings.value = value; + } + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary> + public Vector3 steeringTarget => entityStorageCache.GetComponentData(entity, ref movementStateAccessRO, out var movementState) ? (Vector3)movementState.value.nextCorner : position; + + /// <summary>\copydoc Pathfinding::IAstarAI::onSearchPath</summary> + Action IAstarAI.onSearchPath { + get => null; + set => throw new NotImplementedException("The FollowerEntity does not support this property."); + } + + /// <summary> + /// Provides callbacks during various parts of the movement calculations. + /// + /// With this property you can register callbacks that will be called during various parts of the movement calculations. + /// These can be used to modify movement of the agent. + /// + /// The following example demonstrates how one can hook into one of the available phases and modify the agent's movement. + /// In this case, the movement is modified to become wavy. + /// + /// [Open online documentation to see videos] + /// + /// <code> + /// using Pathfinding; + /// using Pathfinding.ECS; + /// using Unity.Entities; + /// using Unity.Mathematics; + /// using Unity.Transforms; + /// using UnityEngine; + /// + /// public class MovementModifierNoise : MonoBehaviour { + /// /** How much noise to apply */ + /// public float strength = 1; + /// /** How fast the noise should change */ + /// public float frequency = 1; + /// float phase; + /// + /// public void Start () { + /// // Register a callback to modify the movement. + /// // This will be called during every simulation step for the agent. + /// // This may be called multiple times per frame if the time scale is high or fps is low, + /// // or less than once per frame, if the fps is very high. + /// GetComponent<FollowerEntity>().movementOverrides.AddBeforeControlCallback(MovementOverride); + /// + /// // Randomize a phase, to make different agents behave differently + /// phase = UnityEngine.Random.value * 1000; + /// } + /// + /// public void OnDisable () { + /// // Remove the callback when the component is disabled + /// GetComponent<FollowerEntity>().movementOverrides.RemoveBeforeControlCallback(MovementOverride); + /// } + /// + /// public void MovementOverride (Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings) { + /// // Rotate the next corner the agent is moving towards around the agent by a random angle. + /// // This will make the agent appear to move in a drunken fashion. + /// + /// // Don't modify the movement as much if we are very close to the end of the path + /// var strengthMultiplier = Mathf.Min(1, movementState.remainingDistanceToEndOfPart / Mathf.Max(shape.radius, movementSettings.follower.slowdownTime * movementSettings.follower.speed)); + /// strengthMultiplier *= strengthMultiplier; + /// + /// // Generate a smoothly varying rotation angle + /// var rotationAngleRad = strength * strengthMultiplier * (Mathf.PerlinNoise1D(Time.time * frequency + phase) - 0.5f); + /// // Clamp it to at most plus or minus 90 degrees + /// rotationAngleRad = Mathf.Clamp(rotationAngleRad, -math.PI*0.5f, math.PI*0.5f); + /// + /// // Convert the rotation angle to a world-space quaternion. + /// // We use the movement plane to rotate around the agent's up axis, + /// // making this code work in both 2D and 3D games. + /// var rotation = movementPlane.value.ToWorldRotation(rotationAngleRad); + /// + /// // Rotate the direction to the next corner around the agent + /// movementState.nextCorner = localTransform.Position + math.mul(rotation, movementState.nextCorner - localTransform.Position); + /// } + /// } + /// </code> + /// + /// There are a few different phases that you can register callbacks for: + /// + /// - BeforeControl: Called before the agent's movement is calculated. At this point, the agent has a valid path, and the next corner that is moving towards has been calculated. + /// - AfterControl: Called after the agent's desired movement is calculated. The agent has stored its desired movement in the <see cref="MovementControl"/> component. Local avoidance has not yet run. + /// - BeforeMovement: Called right before the agent's movement is applied. At this point the agent's final movement (including local avoidance) is stored in the <see cref="ResolvedMovement"/> component, which you may modify. + /// + /// Warning: If any agent has a callback registered here, a sync point will be created for all agents when the callback runs. + /// This can make the simulation not able to utilize multiple threads as effectively. If you have a lot of agents, consider using a custom entity component system instead. + /// But as always, profile first to see if this is actually a problem for your game. + /// + /// The callbacks may be called multiple times per frame, if the fps is low, or if the time scale is high. + /// It may also be called less than once per frame if the fps is very high. + /// Each callback is provided with a dt parameter, which is the time in seconds since the last simulation step. You should prefer using this instead of Time.deltaTime. + /// + /// See: <see cref="canMove"/> + /// See: <see cref="updatePosition"/> + /// See: <see cref="updateRotation"/> + /// + /// Note: This API is unstable. It may change in future versions. + /// </summary> + public ManagedMovementOverrides movementOverrides => new ManagedMovementOverrides(entity, World.DefaultGameObjectInjectionWorld); + + /// <summary>\copydoc Pathfinding::IAstarAI::FinalizeMovement</summary> + void IAstarAI.FinalizeMovement (Vector3 nextPosition, Quaternion nextRotation) { + throw new InvalidOperationException("The FollowerEntity component does not support FinalizeMovement. Use an ECS system to override movement instead, or use the movementOverrides property. If you just want to move the agent to a position, set ai.position or call ai.Teleport."); + } + + /// <summary> + /// Fills buffer with the remaining path. + /// + /// If the agent traverses off-mesh links, the buffer will still contain the whole path. Off-mesh links will be represented by a single line segment. + /// You can use the <see cref="GetRemainingPath(List<Vector3>,List<PathPartWithLinkInfo>,bool)"/> overload to get more detailed information about the different parts of the path. + /// + /// <code> + /// var buffer = new List<Vector3>(); + /// + /// ai.GetRemainingPath(buffer, out bool stale); + /// for (int i = 0; i < buffer.Count - 1; i++) { + /// Debug.DrawLine(buffer[i], buffer[i+1], Color.red); + /// } + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="buffer">The buffer will be cleared and replaced with the path. The first point is the current position of the agent.</param> + /// <param name="stale">May be true if the path is invalid in some way. For example if the agent has no path or if the agent has detected that some nodes in the path have been destroyed.</param> + public void GetRemainingPath (List<Vector3> buffer, out bool stale) { + GetRemainingPath(buffer, null, out stale); + } + + /// <summary> + /// Fills buffer with the remaining path. + /// + /// <code> + /// var buffer = new List<Vector3>(); + /// var parts = new List<PathPartWithLinkInfo>(); + /// + /// ai.GetRemainingPath(buffer, parts, out bool stale); + /// foreach (var part in parts) { + /// for (int i = part.startIndex; i < part.endIndex; i++) { + /// Debug.DrawLine(buffer[i], buffer[i+1], part.type == Funnel.PartType.NodeSequence ? Color.red : Color.green); + /// } + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// Note: The <see cref="FollowerEntity"/> simplifies its path continuously as it moves along it. This means that the agent may not follow this exact path, if it manages to simplify the path later. + /// Furthermore, the agent will apply a steering behavior on top of this path, to make its movement smoother. + /// </summary> + /// <param name="buffer">The buffer will be cleared and replaced with the path. The first point is the current position of the agent.</param> + /// <param name="partsBuffer">If not null, this list will be filled with information about the different parts of the path. A part is a sequence of nodes or an off-mesh link.</param> + /// <param name="stale">May be true if the path is invalid in some way. For example if the agent has no path or if the agent has detected that some nodes in the path have been destroyed.</param> + public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale) { + buffer.Clear(); + if (partsBuffer != null) partsBuffer.Clear(); + if (!entityExists) { + buffer.Add(position); + if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo { startIndex = 0, endIndex = 0 }); + stale = true; + return; + } + + var ms = World.DefaultGameObjectInjectionWorld.EntityManager.GetComponentData<ManagedState>(entity); + stale = false; + if (ms.pathTracer.hasPath) { + var nativeBuffer = new NativeList<float3>(Allocator.Temp); + var scratch = new NativeArray<int>(8, Allocator.Temp); + ms.pathTracer.GetNextCorners(nativeBuffer, int.MaxValue, ref scratch, Allocator.Temp, ms.pathfindingSettings.traversalProvider, ms.activePath); + if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo(0, nativeBuffer.Length - 1)); + + if (ms.pathTracer.partCount > 1) { + // There are more parts in the path. We need to create a new PathTracer to get the other parts. + // This can be comparatively expensive, since it needs to generate all the other types from scratch. + var pathTracer = ms.pathTracer.Clone(); + while (pathTracer.partCount > 1) { + pathTracer.PopParts(1, ms.pathfindingSettings.traversalProvider, ms.activePath); + var startIndex = nativeBuffer.Length; + if (pathTracer.GetPartType() == Funnel.PartType.NodeSequence) { + pathTracer.GetNextCorners(nativeBuffer, int.MaxValue, ref scratch, Allocator.Temp, ms.pathfindingSettings.traversalProvider, ms.activePath); + if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo(startIndex, nativeBuffer.Length - 1)); + } else { + // If the link contains destroyed nodes, we cannot get a valid link object. + // In that case, we stop here and mark the path as stale. + if (pathTracer.PartContainsDestroyedNodes()) { + stale = true; + break; + } + // Note: startIndex will refer to the last point in the previous part, and endIndex will refer to the first point in the next part + Assert.IsTrue(startIndex > 0); + if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo(startIndex - 1, startIndex, pathTracer.GetLinkInfo())); + } + // We need to check if the path is stale after each part because the path tracer may have realized that some nodes are destroyed + stale |= pathTracer.isStale; + } + } + + nativeBuffer.AsUnsafeSpan().Reinterpret<Vector3>().CopyTo(buffer); + } else { + buffer.Add(position); + if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo { startIndex = 0, endIndex = 0 }); + } + stale |= ms.pathTracer.isStale; + } + + /// <summary>\copydoc Pathfinding::IAstarAI::Move</summary> + public void Move (Vector3 deltaPosition) { + position += deltaPosition; + } + + void IAstarAI.MovementUpdate (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) { + throw new InvalidOperationException("The FollowerEntity component does not support MovementUpdate. Use an ECS system to override movement instead, or use the movementOverrides property"); + } + + /// <summary>\copydoc Pathfinding::IAstarAI::SearchPath</summary> + public void SearchPath () { + var dest = destination; + if (!float.IsFinite(dest.x)) return; + + SetPath(ABPath.Construct(position, dest, null), false); + } + + void AssertEntityExists () { + if (World.DefaultGameObjectInjectionWorld == null || !World.DefaultGameObjectInjectionWorld.EntityManager.Exists(entity)) throw new System.InvalidOperationException("Entity does not exist. You can only access this if the component is active and enabled."); + } + + /// <summary> + /// True if this component's entity exists. + /// + /// This is typically true if the component is active and enabled and the game is running. + /// + /// See: <see cref="entity"/> + /// </summary> + public bool entityExists => World.DefaultGameObjectInjectionWorld != null && World.DefaultGameObjectInjectionWorld.EntityManager.Exists(entity); + + void CancelCurrentPathRequest () { + if (entityExists) { + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + managedStateAccessRW.Update(entityManager); + managedState.CancelCurrentPathRequest(); + } + } + + void ClearPath() => ClearPath(entity); + + static void ClearPath (Entity entity) { + if (entityStorageCache.Update(World.DefaultGameObjectInjectionWorld, entity, out var entityManager, out var storage)) { + agentOffMeshLinkTraversalRO.Update(entityManager); + + if (agentOffMeshLinkTraversalRO.HasComponent(storage)) { + // Agent is traversing an off-mesh link. We must abort this link traversal. + var managedInfo = entityManager.GetComponentData<ManagedAgentOffMeshLinkTraversal>(entity); + if (managedInfo.stateMachine != null) managedInfo.stateMachine.OnAbortTraversingOffMeshLink(); + managedInfo.context.Restore(); + entityManager.RemoveComponent<AgentOffMeshLinkTraversal>(entity); + entityManager.RemoveComponent<ManagedAgentOffMeshLinkTraversal>(entity); + // We need to get the storage info again, because the entity will have been moved to another chunk + entityStorageCache.Update(World.DefaultGameObjectInjectionWorld, entity, out entityManager, out storage); + } + + entityManager.SetComponentEnabled<ReadyToTraverseOffMeshLink>(entity, false); + + managedStateAccessRW.Update(entityManager); + movementStateAccessRW.Update(entityManager); + localTransformAccessRO.Update(entityManager); + movementPlaneAccessRO.Update(entityManager); + resolvedMovementAccessRW.Update(entityManager); + movementControlAccessRW.Update(entityManager); + + ref var movementState = ref movementStateAccessRW[storage]; + ref var localTransform = ref localTransformAccessRO[storage]; + ref var movementPlane = ref movementPlaneAccessRO[storage]; + ref var resolvedMovement = ref resolvedMovementAccessRW[storage]; + ref var controlOutput = ref movementControlAccessRW[storage]; + var managedState = managedStateAccessRW[storage]; + + managedState.ClearPath(); + managedState.CancelCurrentPathRequest(); + movementState.SetPathIsEmpty(localTransform.Position); + + // This emulates what the ControlJob does when the agent has no path. + // This ensures that properties like #desiredVelocity return the correct value immediately after the path has been cleared. + resolvedMovement.targetPoint = localTransform.Position; + resolvedMovement.speed = 0; + resolvedMovement.targetRotation = movementPlane.value.ToPlane(localTransform.Rotation); + controlOutput.endOfPath = movementState.endOfPath; + controlOutput.speed = 0f; + controlOutput.targetPoint = localTransform.Position; + } + } + + /// <summary> + /// Make the AI follow the specified path. + /// + /// In case the path has not been calculated, the script will schedule the path to be calculated. + /// This means the AI may not actually start to follow the path until in a few frames when the path has been calculated. + /// The <see cref="pathPending"/> field will, as usual, return true while the path is being calculated. + /// + /// In case the path has already been calculated, it will immediately replace the current path the AI is following. + /// + /// If you pass null path, then the current path will be cleared and the agent will stop moving. + /// The agent will also abort traversing any off-mesh links it is currently traversing. + /// Note than unless you have also disabled <see cref="canSearch"/>, then the agent will soon recalculate its path and start moving again. + /// + /// Note: Stopping the agent by passing a null path works. But this will stop the agent instantly, and it will not be able to use local avoidance or know its place on the navmesh. + /// Usually it's better to set <see cref="isStopped"/> to false, which will make the agent slow down smoothly. + /// + /// You can disable the automatic path recalculation by setting the <see cref="canSearch"/> field to false. + /// + /// Note: This call will be ignored if the agent is currently traversing an off-mesh link. + /// Furthermore, if the agent starts traversing an off-mesh link, the current path request will be canceled (if one is currently in progress). + /// + /// <code> + /// IEnumerator Start () { + /// var pointToAvoid = enemy.position; + /// // Make the AI flee from an enemy. + /// // The path will be about 20 world units long (the default cost of moving 1 world unit is 1000). + /// var path = FleePath.Construct(ai.position, pointToAvoid, 1000 * 20); + /// ai.SetPath(path); + /// + /// while (!ai.reachedEndOfPath) { + /// yield return null; + /// } + /// } + /// </code> + /// </summary> + /// <param name="path">The path to follow.</param> + /// <param name="updateDestinationFromPath">If true, the \reflink{destination} property will be set to the end point of the path. If false, the previous destination value will be kept. + /// If you pass a path which has no well defined destination before it is calculated (e.g. a MultiTargetPath or RandomPath), then the destination will be first be cleared, but once the path has been calculated, it will be set to the end point of the path.</param> + public void SetPath(Path path, bool updateDestinationFromPath = true) => SetPath(entity, path, updateDestinationFromPath); + + /// <summary>\copydocref{SetPath(Path,bool)}</summary> + public static void SetPath (Entity entity, Path path, bool updateDestinationFromPath = true) { + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + if (!entityManager.Exists(entity)) throw new System.InvalidOperationException("Entity does not exist. You can only assign a path if the component is active and enabled."); + + managedStateAccessRW.Update(entityManager); + movementPlaneAccessRO.Update(entityManager); + agentOffMeshLinkTraversalRO.Update(entityManager); + movementStateAccessRW.Update(entityManager); + localTransformAccessRO.Update(entityManager); + destinationPointAccessRW.Update(entityManager); + + var storage = entityManager.GetStorageInfo(entity); + + bool isTraversingOffMeshLink = agentOffMeshLinkTraversalRO.HasComponent(storage); + if (isTraversingOffMeshLink) { + // Agent is traversing an off-mesh link. We ignore any path updates during this time. + // TODO: Race condition when adding off mesh link component? + // TODO: If passing null, should we clear the whole path after the off-mesh link? + return; + } + + if (path == null) { + ClearPath(entity); + return; + } + + var managedState = managedStateAccessRW[storage]; + ref var movementPlane = ref movementPlaneAccessRO[storage]; + ref var movementState = ref movementStateAccessRW[storage]; + ref var localTransform = ref localTransformAccessRO[storage]; + ref var destination = ref destinationPointAccessRW[storage]; + + if (updateDestinationFromPath && path is ABPath abPath) { + // If the user supplies a new ABPath manually, they probably want the agent to move to that point. + // So by default we update the destination to match the path. + if (abPath.endPointKnownBeforeCalculation) { + destination = new DestinationPoint { destination = abPath.originalEndPoint, facingDirection = default }; + } else { + // If the destination is not known, we set it to positive infinity. + // This is the case for MultiTargetPath and RandomPath, for example. + destination = new DestinationPoint { destination = Vector3.positiveInfinity, facingDirection = default }; + } + } + + ManagedState.SetPath(path, managedState, in movementPlane, ref destination); + + if (path.IsDone()) { + agentCylinderShapeAccessRO.Update(entityManager); + movementSettingsAccessRO.Update(entityManager); + readyToTraverseOffMeshLinkRW.Update(entityManager); + + // This remaining part ensures that the path tracer is fully up to date immediately after the path has been assigned. + // So that things like GetRemainingPath, and various properties like reachedDestination are up to date immediately. + managedState.pathTracer.UpdateStart(localTransform.Position, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + managedState.pathTracer.UpdateEnd(destination.destination, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + + if (movementState.pathTracerVersion != managedState.pathTracer.version) { + if (!nextCornersScratch.IsCreated) nextCornersScratch = new NativeList<float3>(4, Allocator.Persistent); + ref var shape = ref agentCylinderShapeAccessRO[storage]; + ref var movementSettings = ref movementSettingsAccessRO[storage]; + var readyToTraverseOffMeshLink = storage.Chunk.GetEnabledMask(ref readyToTraverseOffMeshLinkRW.handle).GetEnabledRefRW<ReadyToTraverseOffMeshLink>(storage.IndexInChunk); + JobRepairPath.Execute( + ref localTransform, + ref movementState, + ref shape, + ref movementPlane, + ref destination, + readyToTraverseOffMeshLink, + managedState, + in movementSettings, + nextCornersScratch, + ref indicesScratch, + Allocator.Persistent, + false + ); + } + } + } + + /// <summary> + /// Instantly move the agent to a new position. + /// + /// The current path will be cleared by default. + /// + /// This method is preferred for long distance teleports. If you only move the agent a very small distance (so that it is reasonable that it can keep its current path), + /// then setting the <see cref="position"/> property is preferred. + /// Setting the <see cref="position"/> property very far away from the agent could cause the agent to fail to move the full distance, as it can get blocked by the navmesh. + /// + /// See: Works similarly to Unity's NavmeshAgent.Warp. + /// See: <see cref="position"/> + /// See: <see cref="SearchPath"/> + /// </summary> + public void Teleport (Vector3 newPosition, bool clearPath = true) { + if (clearPath) ClearPath(); + + if (entityExists) { + var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + movementOutputAccessRW.Update(entityManager); + managedStateAccessRW.Update(entityManager); + movementPlaneAccessRO.Update(entityManager); + var storage = entityManager.GetStorageInfo(entity); + ref var movementOutput = ref movementOutputAccessRW[storage]; + movementOutput.lastPosition = newPosition; + managedState.CancelCurrentPathRequest(); + if (AstarPath.active != null) { + // TODO: Should we use the from-above distance metric here? + // This would fail when used on a spherical world and the agent was teleported + // to another part of the sphere. + var nearest = AstarPath.active.GetNearest(newPosition, NNConstraint.Walkable); + if (nearest.node != null) { + var movementPlane = movementPlaneAccessRO[storage]; + managedState.pathTracer.SetFromSingleNode(nearest.node, nearest.position, movementPlane.value); + } + } + + // Note: Since we are starting from a completely new path, + // setting the position will also cause the path tracer to repair the destination. + // Therefore we don't have to also set the destination here. + position = newPosition; + } else { + position = newPosition; + } + } + + void FindComponents () { + tr = transform; + } + + static readonly Color ShapeGizmoColor = new Color(240/255f, 213/255f, 30/255f); + + public override void DrawGizmos () { + if (!Application.isPlaying || !enabled) FindComponents(); + + var color = ShapeGizmoColor; + var destination = this.destination; + var rotation = this.rotation; + var localScale = tr.localScale; + var radius = shape.radius * math.abs(localScale.x); + + if (orientation == OrientationMode.YAxisForward) { + Draw.Circle(position, rotation * Vector3.forward, radius, color); + } else { + Draw.WireCylinder(position, rotation * Vector3.up, localScale.y * shape.height, radius, color); + } + + if (!updateRotation) { + Draw.ArrowheadArc(position, rotation * Vector3.forward, radius * 1.05f, color); + } + + if (!float.IsPositiveInfinity(destination.x) && Application.isPlaying) { + var dir = destinationFacingDirection; + if (dir != Vector3.zero) { + Draw.xz.ArrowheadArc(destination, dir, 0.25f, Color.blue); + } + Draw.xz.Circle(destination, 0.2f, Color.blue); + } + } + + [System.Flags] + enum FollowerEntityMigrations { + MigratePathfindingSettings = 1 << 0, + MigrateMovementPlaneSource = 1 << 1, + MigrateAutoRepathPolicy = 1 << 2, + } + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + if (migrations.TryMigrateFromLegacyFormat(out var _legacyVersion)) { + // Only 1 version of the previous version format existed for this component + if (this.pathfindingSettings.tagPenalties.Length != 0) migrations.MarkMigrationFinished((int)FollowerEntityMigrations.MigratePathfindingSettings); + } + + if (migrations.AddAndMaybeRunMigration((int)FollowerEntityMigrations.MigratePathfindingSettings, unityThread)) { + if (TryGetComponent<Seeker>(out var seeker)) { + this.pathfindingSettings = new PathRequestSettings { + graphMask = seeker.graphMask, + traversableTags = seeker.traversableTags, + tagPenalties = seeker.tagPenalties, + }; + UnityEngine.Object.DestroyImmediate(seeker); + } else { + this.pathfindingSettings = PathRequestSettings.Default; + } + } + #pragma warning disable 618 + if (migrations.AddAndMaybeRunMigration((int)FollowerEntityMigrations.MigrateMovementPlaneSource, unityThread)) { + this.movementPlaneSource = this.movement.movementPlaneSource; + } + if (migrations.AddAndMaybeRunMigration((int)FollowerEntityMigrations.MigrateAutoRepathPolicy, unityThread)) { + this.autoRepathBacking = new ECS.AutoRepathPolicy(managedState.autoRepath); + } + #pragma warning restore 618 + } + +#if UNITY_EDITOR + /// <summary>\cond IGNORE_IN_DOCS</summary> + + /// <summary> + /// Copies all settings from this component to the entity's components. + /// + /// Note: This is an internal method and you should never need to use it yourself. + /// Typically it is used by the editor to keep the entity's state in sync with the component's state. + /// </summary> + public void SyncWithEntity () { + if (!entityStorageCache.Update(World.DefaultGameObjectInjectionWorld, entity, out var entityManager, out var storage)) return; + + this.position = this.position; + this.autoRepath = this.autoRepath; + movementSettingsAccessRW.Update(entityManager); + managedStateAccessRW.Update(entityManager); + agentCylinderShapeAccessRW.Update(entityManager); + + SyncWithEntity(managedStateAccessRW[storage], ref agentCylinderShapeAccessRW[storage], ref movementSettingsAccessRW[storage]); + + // Structural changes + ToggleComponent<GravityState>(entity, enableGravity, false); + ToggleComponent<OrientationYAxisForward>(entity, orientation == OrientationMode.YAxisForward, false); + this.movementPlaneSource = this.movementPlaneSource; + } + + /// <summary> + /// Copies all settings from this component to the entity's components. + /// + /// Note: This is an internal method and you should never need to use it yourself. + /// </summary> + public void SyncWithEntity (ManagedState managedState, ref AgentCylinderShape shape, ref MovementSettings movementSettings) { + movementSettings = this.movement; + shape = this.shape; + // Copy all fields to the managed state object. + // Don't copy the PathTracer or the onTraverseOffMeshLink, though, since they are not serialized + #pragma warning disable 618 + managedState.autoRepath = this.managedState.autoRepath; + #pragma warning restore 618 + managedState.rvoSettings = this.managedState.rvoSettings; + managedState.enableLocalAvoidance = this.managedState.enableLocalAvoidance; + // Replace this instance of the managed state with the entity component + this.managedState = managedState; + // Note: RVO settings are copied every frame automatically before local avoidance simulations + } + + static List<FollowerEntity> needsSyncWithEntityList = new List<FollowerEntity>(); + + void ISerializationCallbackReceiver.OnBeforeSerialize () {} + + void ISerializationCallbackReceiver.OnAfterDeserialize () { + UpgradeSerializedData(false); + + // This is (among other times) called after an undo or redo event has happened. + // In that case, the entity's state might be out of sync with this component's state, + // so we need to sync the two together. Unfortunately this method is called + // from Unity's separate serialization thread, so we cannot access the entity directly. + // Instead we add this component to a list and make sure to process them in the next + // editor update. + lock (needsSyncWithEntityList) { + needsSyncWithEntityList.Add(this); + if (needsSyncWithEntityList.Count == 1) { + UnityEditor.EditorApplication.update += SyncWithEntities; + } + } + } + + static void SyncWithEntities () { + lock (needsSyncWithEntityList) { + for (int i = 0; i < needsSyncWithEntityList.Count; i++) { + needsSyncWithEntityList[i].SyncWithEntity(); + } + needsSyncWithEntityList.Clear(); + UnityEditor.EditorApplication.update -= SyncWithEntities; + } + } + + /// <summary>\endcond</summary> +#endif + } +} +#else +namespace Pathfinding { + public sealed partial class FollowerEntity : VersionedMonoBehaviour { + public void Start () { + UnityEngine.Debug.LogError("The FollowerEntity component requires at least version 1.0 of the 'Entities' package to be installed. You can install it using the Unity package manager."); + } + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + // Since most of the code for this component is stripped out, we should just preserve the current state, + // and not try to migrate anything. + // If we don't do this, then the base code will log an error about an unknown migration already being done. + migrations.IgnoreMigrationAttempt(); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs.meta new file mode 100644 index 0000000..6e40ab6 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cfe9431ea8ad072f2aecd3041b1524dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs new file mode 100644 index 0000000..0c4395f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs @@ -0,0 +1,475 @@ +using UnityEngine; +using System.Collections.Generic; +using Pathfinding.Util; + +namespace Pathfinding { + /// <summary> + /// Common interface for all movement scripts in the A* Pathfinding Project. + /// See: <see cref="Pathfinding.AIPath"/> + /// See: <see cref="Pathfinding.RichAI"/> + /// See: <see cref="Pathfinding.AILerp"/> + /// </summary> + public interface IAstarAI { + /// <summary> + /// Radius of the agent in world units. + /// This is visualized in the scene view as a yellow cylinder around the character. + /// + /// Note that this does not affect pathfinding in any way. + /// The graph used completely determines where the agent can move. + /// + /// Note: The <see cref="Pathfinding.AILerp"/> script doesn't really have any use of knowing the radius or the height of the character, so this property will always return 0 in that script. + /// </summary> + float radius { get; set; } + + /// <summary> + /// Height of the agent in world units. + /// This is visualized in the scene view as a yellow cylinder around the character. + /// + /// This value is currently only used if an RVOController is attached to the same GameObject, otherwise it is only used for drawing nice gizmos in the scene view. + /// However since the height value is used for some things, the radius field is always visible for consistency and easier visualization of the character. + /// That said, it may be used for something in a future release. + /// + /// Note: The <see cref="Pathfinding.AILerp"/> script doesn't really have any use of knowing the radius or the height of the character, so this property will always return 0 in that script. + /// </summary> + float height { get; set; } + + /// <summary> + /// Position of the agent. + /// In world space. + /// See: <see cref="rotation"/> + /// + /// If you want to move the agent you may use <see cref="Teleport"/> or <see cref="Move"/>. + /// </summary> + Vector3 position { get; } + + /// <summary> + /// Rotation of the agent. + /// In world space. + /// See: <see cref="position"/> + /// </summary> + Quaternion rotation { get; set; } + + /// <summary>Max speed in world units per second</summary> + float maxSpeed { get; set; } + + /// <summary> + /// Actual velocity that the agent is moving with. + /// In world units per second. + /// + /// See: <see cref="desiredVelocity"/> + /// </summary> + Vector3 velocity { get; } + + /// <summary> + /// Velocity that this agent wants to move with. + /// Includes gravity and local avoidance if applicable. + /// In world units per second. + /// + /// See: <see cref="velocity"/> + /// + /// Note: The <see cref="Pathfinding.AILerp"/> movement script doesn't use local avoidance or gravity so this property will always be identical to <see cref="velocity"/> on that component. + /// </summary> + Vector3 desiredVelocity { get; } + + /// <summary> + /// Velocity that this agent wants to move with before taking local avoidance into account. + /// + /// Includes gravity. + /// In world units per second. + /// + /// Setting this property will set the current velocity that the agent is trying to move with, including gravity. + /// This can be useful if you want to make the agent come to a complete stop in a single frame or if you want to modify the velocity in some way. + /// + /// <code> + /// // Set the velocity to zero, but keep the current gravity + /// var newVelocity = new Vector3(0, ai.desiredVelocityWithoutLocalAvoidance.y, 0); + /// + /// ai.desiredVelocityWithoutLocalAvoidance = newVelocity; + /// </code> + /// + /// Note: The <see cref="Pathfinding.AILerp"/> movement script doesn't use local avoidance so this property will always be identical to <see cref="velocity"/> on that component. + /// + /// Warning: Trying to set this property on an AILerp component will throw an exception since its velocity cannot meaningfully be changed abitrarily. + /// + /// If you are not using local avoidance then this property will in almost all cases be identical to <see cref="desiredVelocity"/> plus some noise due to floating point math. + /// + /// See: <see cref="velocity"/> + /// See: <see cref="desiredVelocity"/> + /// See: <see cref="Move"/> + /// See: <see cref="MovementUpdate"/> + /// </summary> + Vector3 desiredVelocityWithoutLocalAvoidance { get; set; } + + /// <summary> + /// Approximate remaining distance along the current path to the end of the path. + /// The RichAI movement script approximates this distance since it is quite expensive to calculate the real distance. + /// However it will be accurate when the agent is within 1 corner of the destination. + /// You can use <see cref="GetRemainingPath"/> to calculate the actual remaining path more precisely. + /// + /// The AIPath and AILerp scripts use a more accurate distance calculation at all times. + /// + /// If the agent does not currently have a path, then positive infinity will be returned. + /// + /// Note: This is the distance to the end of the path, which may or may not be at the <see cref="destination"/>. If the character cannot reach the destination it will try to move as close as possible to it. + /// + /// Warning: Since path requests are asynchronous, there is a small delay between a path request being sent and this value being updated with the new calculated path. + /// + /// See: <see cref="reachedDestination"/> + /// See: <see cref="reachedEndOfPath"/> + /// See: <see cref="pathPending"/> + /// </summary> + float remainingDistance { get; } + + /// <summary> + /// True if the ai has reached the <see cref="destination"/>. + /// This is a best effort calculation to see if the <see cref="destination"/> has been reached. + /// For the AIPath/RichAI scripts, this is when the character is within <see cref="AIPath.endReachedDistance"/> world units from the <see cref="destination"/>. + /// For the AILerp script it is when the character is at the destination (±a very small margin). + /// + /// This value will be updated immediately when the <see cref="destination"/> is changed (in contrast to <see cref="reachedEndOfPath)"/>, however since path requests are asynchronous + /// it will use an approximation until it sees the real path result. What this property does is to check the distance to the end of the current path, and add to that the distance + /// from the end of the path to the <see cref="destination"/> (i.e. is assumes it is possible to move in a straight line between the end of the current path to the destination) and then checks if that total + /// distance is less than <see cref="AIPath.endReachedDistance"/>. This property is therefore only a best effort, but it will work well for almost all use cases. + /// + /// Furthermore it will not report that the destination is reached if the destination is above the head of the character or more than half the <see cref="height"/> of the character below its feet + /// (so if you have a multilevel building, it is important that you configure the <see cref="height"/> of the character correctly). + /// + /// The cases which could be problematic are if an agent is standing next to a very thin wall and the destination suddenly changes to the other side of that thin wall. + /// During the time that it takes for the path to be calculated the agent may see itself as alredy having reached the destination because the destination only moved a very small distance (the wall was thin), + /// even though it may actually be quite a long way around the wall to the other side. + /// + /// In contrast to <see cref="reachedEndOfPath"/>, this property is immediately updated when the <see cref="destination"/> is changed. + /// + /// <code> + /// IEnumerator Start () { + /// ai.destination = somePoint; + /// // Start to search for a path to the destination immediately + /// ai.SearchPath(); + /// // Wait until the agent has reached the destination + /// while (!ai.reachedDestination) { + /// yield return null; + /// } + /// // The agent has reached the destination now + /// } + /// </code> + /// + /// See: <see cref="AIPath.endReachedDistance"/> + /// See: <see cref="remainingDistance"/> + /// See: <see cref="reachedEndOfPath"/> + /// </summary> + bool reachedDestination { get; } + + /// <summary> + /// True if the agent has reached the end of the current path. + /// + /// Note that setting the <see cref="destination"/> does not immediately update the path, nor is there any guarantee that the + /// AI will actually be able to reach the destination that you set. The AI will try to get as close as possible. + /// Often you want to use <see cref="reachedDestination"/> instead which is easier to work with. + /// + /// It is very hard to provide a method for detecting if the AI has reached the <see cref="destination"/> that works across all different games + /// because the destination may not even lie on the navmesh and how that is handled differs from game to game (see also the code snippet in the docs for <see cref="destination"/>). + /// + /// See: <see cref="remainingDistance"/> + /// See: <see cref="reachedDestination"/> + /// </summary> + bool reachedEndOfPath { get; } + + /// <summary> + /// End point of path the agent is currently following. + /// If the agent has no path (or it might not be calculated yet), this will return the <see cref="destination"/> instead. + /// If the agent has no destination it will return the agent's current position. + /// + /// The end of the path is usually identical or very close to the <see cref="destination"/>, but it may differ + /// if the path for example was blocked by a wall so that the agent couldn't get any closer. + /// + /// This is only updated when the path is recalculated. + /// </summary> + Vector3 endOfPath { get; } + + /// <summary> + /// Position in the world that this agent should move to. + /// + /// If no destination has been set yet, then (+infinity, +infinity, +infinity) will be returned. + /// + /// Note that setting this property does not immediately cause the agent to recalculate its path. + /// So it may take some time before the agent starts to move towards this point. + /// Most movement scripts have a repathRate field which indicates how often the agent looks + /// for a new path. You can also call the <see cref="SearchPath"/> method to immediately + /// start to search for a new path. Paths are calculated asynchronously so when an agent starts to + /// search for path it may take a few frames (usually 1 or 2) until the result is available. + /// During this time the <see cref="pathPending"/> property will return true. + /// + /// If you are setting a destination and then want to know when the agent has reached that destination + /// then you could either use <see cref="reachedDestination"/> (recommended) or check both <see cref="pathPending"/> and <see cref="reachedEndOfPath"/>. + /// Check the documentation for the respective fields to learn about their differences. + /// + /// <code> + /// IEnumerator Start () { + /// ai.destination = somePoint; + /// // Start to search for a path to the destination immediately + /// ai.SearchPath(); + /// // Wait until the agent has reached the destination + /// while (!ai.reachedDestination) { + /// yield return null; + /// } + /// // The agent has reached the destination now + /// } + /// </code> + /// <code> + /// IEnumerator Start () { + /// ai.destination = somePoint; + /// // Start to search for a path to the destination immediately + /// // Note that the result may not become available until after a few frames + /// // ai.pathPending will be true while the path is being calculated + /// ai.SearchPath(); + /// // Wait until we know for sure that the agent has calculated a path to the destination we set above + /// while (ai.pathPending || !ai.reachedEndOfPath) { + /// yield return null; + /// } + /// // The agent has reached the destination now + /// } + /// </code> + /// </summary> + Vector3 destination { get; set; } + + /// <summary> + /// Enables or disables recalculating the path at regular intervals. + /// Setting this to false does not stop any active path requests from being calculated or stop it from continuing to follow the current path. + /// + /// Note that this only disables automatic path recalculations. If you call the <see cref="SearchPath()"/> method a path will still be calculated. + /// + /// See: <see cref="canMove"/> + /// See: <see cref="isStopped"/> + /// </summary> + bool canSearch { get; set; } + + /// <summary> + /// Enables or disables movement completely. + /// If you want the agent to stand still, but still react to local avoidance and use gravity: use <see cref="isStopped"/> instead. + /// + /// This is also useful if you want to have full control over when the movement calculations run. + /// Take a look at <see cref="MovementUpdate"/> + /// + /// See: <see cref="canSearch"/> + /// See: <see cref="isStopped"/> + /// </summary> + bool canMove { get; set; } + + /// <summary>True if this agent currently has a path that it follows</summary> + bool hasPath { get; } + + /// <summary>True if a path is currently being calculated</summary> + bool pathPending { get; } + + /// <summary> + /// Gets or sets if the agent should stop moving. + /// If this is set to true the agent will immediately start to slow down as quickly as it can to come to a full stop. + /// The agent will still react to local avoidance and gravity (if applicable), but it will not try to move in any particular direction. + /// + /// The current path of the agent will not be cleared, so when this is set + /// to false again the agent will continue moving along the previous path. + /// + /// This is a purely user-controlled parameter, so for example it is not set automatically when the agent stops + /// moving because it has reached the target. Use <see cref="reachedEndOfPath"/> for that. + /// + /// If this property is set to true while the agent is traversing an off-mesh link (RichAI script only), then the agent will + /// continue traversing the link and stop once it has completed it. + /// + /// Note: This is not the same as the <see cref="canMove"/> setting which some movement scripts have. The <see cref="canMove"/> setting + /// disables movement calculations completely (which among other things makes it not be affected by local avoidance or gravity). + /// For the AILerp movement script which doesn't use gravity or local avoidance anyway changing this property is very similar to + /// changing <see cref="canMove"/>. + /// + /// The <see cref="steeringTarget"/> property will continue to indicate the point which the agent would move towards if it would not be stopped. + /// </summary> + bool isStopped { get; set; } + + /// <summary> + /// Point on the path which the agent is currently moving towards. + /// This is usually a point a small distance ahead of the agent + /// or the end of the path. + /// + /// If the agent does not have a path at the moment, then the agent's current position will be returned. + /// </summary> + Vector3 steeringTarget { get; } + + /// <summary> + /// Called when the agent recalculates its path. + /// This is called both for automatic path recalculations (see <see cref="canSearch)"/> and manual ones (see <see cref="SearchPath)"/>. + /// + /// See: Take a look at the <see cref="Pathfinding.AIDestinationSetter"/> source code for an example of how it can be used. + /// </summary> + System.Action onSearchPath { get; set; } + + /// <summary> + /// The plane the agent is moving in. + /// + /// This is typically the ground plane, which will be the XZ plane in a 3D game, and the XY plane in a 2D game. + /// Ultimately it depends on the graph orientation. + /// + /// If you are doing pathfinding on a spherical world (see spherical) (view in online documentation for working links), the the movement plane will be the tangent plane of the sphere at the agent's position. + /// </summary> + NativeMovementPlane movementPlane { get; } + /// <summary> + /// Fills buffer with the remaining path. + /// + /// <code> + /// var buffer = new List<Vector3>(); + /// + /// ai.GetRemainingPath(buffer, out bool stale); + /// for (int i = 0; i < buffer.Count - 1; i++) { + /// Debug.DrawLine(buffer[i], buffer[i+1], Color.red); + /// } + /// </code> + /// [Open online documentation to see images] + /// </summary> + /// <param name="buffer">The buffer will be cleared and replaced with the path. The first point is the current position of the agent.</param> + /// <param name="stale">May be true if the path is invalid in some way. For example if the agent has no path or (for the RichAI/FollowerEntity components only) if the agent has detected that some nodes in the path have been destroyed.</param> + void GetRemainingPath(List<Vector3> buffer, out bool stale); + + /// <summary> + /// Fills buffer with the remaining path. + /// + /// <code> + /// var buffer = new List<Vector3>(); + /// var parts = new List<PathPartWithLinkInfo>(); + /// + /// ai.GetRemainingPath(buffer, parts, out bool stale); + /// foreach (var part in parts) { + /// for (int i = part.startIndex; i < part.endIndex; i++) { + /// Debug.DrawLine(buffer[i], buffer[i+1], part.type == Funnel.PartType.NodeSequence ? Color.red : Color.green); + /// } + /// } + /// </code> + /// [Open online documentation to see images] + /// + /// Note: The <see cref="AIPath"/> and <see cref="AILerp"/> movement scripts do not know about off-mesh links, so the partsBuffer will always be filled with a single node-sequence part. + /// </summary> + /// <param name="buffer">The buffer will be cleared and replaced with the path. The first point is the current position of the agent.</param> + /// <param name="partsBuffer">If not null, this list will be cleared and filled with information about the different parts of the path. A part is a sequence of nodes or an off-mesh link.</param> + /// <param name="stale">May be true if the path is invalid in some way. For example if the agent has no path or (for the RichAI/FollowerEntity components only) if the agent has detected that some nodes in the path have been destroyed.</param> + void GetRemainingPath(List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale); + + /// <summary> + /// Recalculate the current path. + /// You can for example use this if you want very quick reaction times when you have changed the <see cref="destination"/> + /// so that the agent does not have to wait until the next automatic path recalculation (see <see cref="canSearch)"/>. + /// + /// If there is an ongoing path calculation, it will be canceled, so make sure you leave time for the paths to get calculated before calling this function again. + /// A canceled path will show up in the log with the message "Canceled by script" (see <see cref="Seeker.CancelCurrentPathRequest"/>). + /// + /// If no <see cref="destination"/> has been set yet then nothing will be done. + /// + /// Note: The path result may not become available until after a few frames. + /// During the calculation time the <see cref="pathPending"/> property will return true. + /// + /// See: <see cref="pathPending"/> + /// </summary> + void SearchPath(); + + /// <summary> + /// Make the AI follow the specified path. + /// + /// In case the path has not been calculated, the script will call seeker.StartPath to calculate it. + /// This means the AI may not actually start to follow the path until in a few frames when the path has been calculated. + /// The <see cref="pathPending"/> field will as usual return true while the path is being calculated. + /// + /// In case the path has already been calculated it will immediately replace the current path the AI is following. + /// This is useful if you want to replace how the AI calculates its paths. + /// + /// If you pass null as a parameter then the current path will be cleared and the agent will stop moving. + /// Note than unless you have also disabled <see cref="canSearch"/> then the agent will soon recalculate its path and start moving again. + /// + /// You can disable the automatic path recalculation by setting the <see cref="canSearch"/> field to false. + /// + /// Note: This call will be ignored if the agent is currently traversing an off-mesh link. Furthermore, if the agent starts traversing an off-mesh link, the current path request will be canceled (if one is currently in progress). + /// + /// <code> + /// // Disable the automatic path recalculation + /// ai.canSearch = false; + /// var pointToAvoid = enemy.position; + /// // Make the AI flee from the enemy. + /// // The path will be about 20 world units long (the default cost of moving 1 world unit is 1000). + /// var path = FleePath.Construct(ai.position, pointToAvoid, 1000 * 20); + /// ai.SetPath(path); + /// </code> + /// </summary> + /// <param name="path">The path to follow.</param> + /// <param name="updateDestinationFromPath">If true, the \reflink{destination} property will be set to the end point of the path. If false, the previous destination value will be kept.</param> + void SetPath(Path path, bool updateDestinationFromPath = true); + + /// <summary> + /// Instantly move the agent to a new position. + /// This will trigger a path recalculation (if clearPath is true, which is the default) so if you want to teleport the agent and change its <see cref="destination"/> + /// it is recommended that you set the <see cref="destination"/> before calling this method. + /// + /// The current path will be cleared by default. + /// + /// This method is preferred for long distance teleports. If you only move the agent a very small distance (so that it is reasonable that it can keep its current path), + /// then setting the <see cref="position"/> property is preferred. + /// When using the <see cref="FollowerEntity"/> movement script, setting the <see cref="position"/> property when using a long distance teleport could cause the agent to fail to move the full distance, as it can get blocked by the navmesh. + /// + /// See: Works similarly to Unity's NavmeshAgent.Warp. + /// See: <see cref="SearchPath"/> + /// </summary> + void Teleport(Vector3 newPosition, bool clearPath = true); + + /// <summary> + /// Move the agent. + /// + /// This is intended for external movement forces such as those applied by wind, conveyor belts, knockbacks etc. + /// + /// Some movement scripts may ignore this completely (notably the <see cref="AILerp"/> script) if it does not have + /// any concept of being moved externally. + /// + /// For the <see cref="AIPath"/> and <see cref="RichAI"/> movement scripts, the agent will not be moved immediately when calling this method. Instead this offset will be stored and then + /// applied the next time the agent runs its movement calculations (which is usually later this frame or the next frame). + /// If you want to move the agent immediately then call: + /// <code> + /// ai.Move(someVector); + /// ai.FinalizeMovement(ai.position, ai.rotation); + /// </code> + /// + /// The <see cref="FollowerEntity"/> movement script will, on the other hand, move the agent immediately. + /// </summary> + /// <param name="deltaPosition">Direction and distance to move the agent in world space.</param> + void Move(Vector3 deltaPosition); + + /// <summary> + /// Calculate how the character wants to move during this frame. + /// + /// Note that this does not actually move the character. You need to call <see cref="FinalizeMovement"/> for that. + /// This is called automatically unless <see cref="canMove"/> is false. + /// + /// To handle movement yourself you can disable <see cref="canMove"/> and call this method manually. + /// This code will replicate the normal behavior of the component: + /// <code> + /// void Update () { + /// // Disable the AIs own movement code + /// ai.canMove = false; + /// Vector3 nextPosition; + /// Quaternion nextRotation; + /// // Calculate how the AI wants to move + /// ai.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation); + /// // Modify nextPosition and nextRotation in any way you wish + /// // Actually move the AI + /// ai.FinalizeMovement(nextPosition, nextRotation); + /// } + /// </code> + /// </summary> + /// <param name="deltaTime">time to simulate movement for. Usually set to Time.deltaTime.</param> + /// <param name="nextPosition">the position that the agent wants to move to during this frame.</param> + /// <param name="nextRotation">the rotation that the agent wants to rotate to during this frame.</param> + void MovementUpdate(float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation); + + /// <summary> + /// Move the agent. + /// To be called as the last step when you are handling movement manually. + /// + /// The movement will be clamped to the navmesh if applicable (this is done for the RichAI movement script). + /// + /// See: <see cref="MovementUpdate"/> for a code example. + /// </summary> + void FinalizeMovement(Vector3 nextPosition, Quaternion nextRotation); + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs.meta new file mode 100644 index 0000000..b7106e3 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: b7438f3f6b9404f05ab7f584f92aa7d5 +timeCreated: 1495013922 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs new file mode 100644 index 0000000..076f8a4 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs @@ -0,0 +1,73 @@ +using UnityEngine; +namespace Pathfinding.Examples { + using Pathfinding.Util; + + /// <summary> + /// RichAI for local space (pathfinding on moving graphs). + /// + /// What this script does is that it fakes graph movement. + /// It can be seen in the example scene called 'Moving' where + /// a character is pathfinding on top of a moving ship. + /// The graph does not actually move in that example + /// instead there is some 'cheating' going on. + /// + /// When requesting a path, we first transform + /// the start and end positions of the path request + /// into local space for the object we are moving on + /// (e.g the ship in the example scene), then when we get the + /// path back, they will still be in these local coordinates. + /// When following the path, we will every frame transform + /// the coordinates of the waypoints in the path to global + /// coordinates so that we can follow them. + /// + /// At the start of the game (when the graph is scanned) the + /// object we are moving on should be at a valid position on the graph and + /// you should attach the <see cref="Pathfinding.LocalSpaceGraph"/> component to it. The <see cref="Pathfinding.LocalSpaceGraph"/> + /// component will store the position and orientation of the object right there are the start + /// and then we can use that information to transform coordinates back to that region of the graph + /// as if the object had not moved at all. + /// + /// This functionality is only implemented for the RichAI + /// script, however it should not be hard to + /// use the same approach for other movement scripts. + /// </summary> + [HelpURL("https://arongranberg.com/astar/documentation/stable/localspacerichai.html")] + public class LocalSpaceRichAI : RichAI { + /// <summary>Root of the object we are moving on</summary> + public LocalSpaceGraph graph; + + protected override Vector3 ClampPositionToGraph (Vector3 newPosition) { + RefreshTransform(); + // Clamp the new position to the navmesh + // First we need to transform the position to the same space that the graph is in though. + var nearest = AstarPath.active != null? AstarPath.active.GetNearest(graph.transformation.InverseTransform(newPosition)) : new NNInfo(); + float elevation; + + movementPlane.ToPlane(newPosition, out elevation); + return movementPlane.ToWorld(movementPlane.ToPlane(nearest.node != null ? graph.transformation.Transform(nearest.position) : newPosition), elevation); + } + + void RefreshTransform () { + graph.Refresh(); + richPath.transform = graph.transformation; + movementPlane = graph.transformation.ToSimpleMovementPlane(); + } + + protected override void Start () { + RefreshTransform(); + base.Start(); + } + + protected override void CalculatePathRequestEndpoints (out Vector3 start, out Vector3 end) { + RefreshTransform(); + base.CalculatePathRequestEndpoints(out start, out end); + start = graph.transformation.InverseTransform(start); + end = graph.transformation.InverseTransform(end); + } + + protected override void OnUpdate (float dt) { + RefreshTransform(); + base.OnUpdate(dt); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs.meta new file mode 100644 index 0000000..0c09569 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e342e9f54c9d04f05b77eff70a36605e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichAI.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichAI.cs new file mode 100644 index 0000000..e955115 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichAI.cs @@ -0,0 +1,645 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; + +namespace Pathfinding { + using Pathfinding.RVO; + using Pathfinding.Util; + using Pathfinding.Drawing; + + [AddComponentMenu("Pathfinding/AI/RichAI (3D, for navmesh)")] + [UniqueComponent(tag = "ai")] + /// <summary> + /// Advanced AI for navmesh based graphs. + /// + /// See: movementscripts (view in online documentation for working links) + /// </summary> + public partial class RichAI : AIBase, IAstarAI { + /// <summary> + /// Max acceleration of the agent. + /// In world units per second per second. + /// </summary> + public float acceleration = 5; + + /// <summary> + /// Max rotation speed of the agent. + /// In degrees per second. + /// </summary> + public float rotationSpeed = 360; + + /// <summary> + /// How long before reaching the end of the path to start to slow down. + /// A lower value will make the agent stop more abruptly. + /// + /// Note: The agent may require more time to slow down if + /// its maximum <see cref="acceleration"/> is not high enough. + /// + /// If set to zero the agent will not even attempt to slow down. + /// This can be useful if the target point is not a point you want the agent to stop at + /// but it might for example be the player and you want the AI to slam into the player. + /// + /// Note: A value of zero will behave differently from a small but non-zero value (such as 0.0001). + /// When it is non-zero the agent will still respect its <see cref="acceleration"/> when determining if it needs + /// to slow down, but if it is zero it will disable that check. + /// This is useful if the <see cref="destination"/> is not a point where you want the agent to stop. + /// + /// \htmlonly <video class="tinyshadow" controls="true" loop="true"><source src="images/richai_slowdown_time.mp4" type="video/mp4" /></video> \endhtmlonly + /// </summary> + public float slowdownTime = 0.5f; + + /// <summary> + /// Force to avoid walls with. + /// The agent will try to steer away from walls slightly. + /// + /// See: <see cref="wallDist"/> + /// </summary> + public float wallForce = 3; + + /// <summary> + /// Walls within this range will be used for avoidance. + /// Setting this to zero disables wall avoidance and may improve performance slightly + /// + /// See: <see cref="wallForce"/> + /// </summary> + public float wallDist = 1; + + /// <summary> + /// Use funnel simplification. + /// On tiled navmesh maps, but sometimes on normal ones as well, it can be good to simplify + /// the funnel as a post-processing step to make the paths straighter. + /// + /// This has a moderate performance impact during frames when a path calculation is completed. + /// + /// The RichAI script uses its own internal funnel algorithm, so you never + /// need to attach the FunnelModifier component. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="Pathfinding.FunnelModifier"/> + /// </summary> + public bool funnelSimplification = false; + + /// <summary> + /// Slow down when not facing the target direction. + /// Incurs at a small performance overhead. + /// + /// This setting only has an effect if <see cref="enableRotation"/> is enabled. + /// </summary> + public bool slowWhenNotFacingTarget = true; + + /// <summary> + /// Prevent the velocity from being too far away from the forward direction of the character. + /// If the character is ordered to move in the opposite direction from where it is facing + /// then enabling this will cause it to make a small loop instead of turning on the spot. + /// + /// This setting only has an effect if <see cref="slowWhenNotFacingTarget"/> is enabled. + /// </summary> + public bool preventMovingBackwards = false; + + /// <summary> + /// Called when the agent starts to traverse an off-mesh link. + /// Register to this callback to handle off-mesh links in a custom way. + /// + /// If this event is set to null then the agent will fall back to traversing + /// off-mesh links using a very simple linear interpolation. + /// + /// <code> + /// void OnEnable () { + /// ai = GetComponent<RichAI>(); + /// if (ai != null) ai.onTraverseOffMeshLink += TraverseOffMeshLink; + /// } + /// + /// void OnDisable () { + /// if (ai != null) ai.onTraverseOffMeshLink -= TraverseOffMeshLink; + /// } + /// + /// IEnumerator TraverseOffMeshLink (RichSpecial link) { + /// // Traverse the link over 1 second + /// float startTime = Time.time; + /// + /// while (Time.time < startTime + 1) { + /// transform.position = Vector3.Lerp(link.first.position, link.second.position, Time.time - startTime); + /// yield return null; + /// } + /// transform.position = link.second.position; + /// } + /// </code> + /// </summary> + public System.Func<RichSpecial, IEnumerator> onTraverseOffMeshLink; + + /// <summary>Holds the current path that this agent is following</summary> + protected readonly RichPath richPath = new RichPath(); + + protected bool delayUpdatePath; + protected bool lastCorner; + + /// <summary>Internal state used for filtering out noise in the agent's rotation</summary> + Vector2 rotationFilterState; + Vector2 rotationFilterState2; + + /// <summary>Distance to <see cref="steeringTarget"/> in the movement plane</summary> + protected float distanceToSteeringTarget = float.PositiveInfinity; + + protected readonly List<Vector3> nextCorners = new List<Vector3>(); + protected readonly List<Vector3> wallBuffer = new List<Vector3>(); + + public bool traversingOffMeshLink { get; protected set; } + + /// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary> + public float remainingDistance { + get { + return distanceToSteeringTarget + Vector3.Distance(steeringTarget, richPath.Endpoint); + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::reachedEndOfPath</summary> + public bool reachedEndOfPath { get { return approachingPathEndpoint && distanceToSteeringTarget < endReachedDistance; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary> + public override bool reachedDestination { + get { + if (!reachedEndOfPath) return false; + // Distance from our position to the current steering target + + // Distance from the steering target to the end of the path + + // distance from the end of the path to the destination. + // Note that most distance checks are done only in the movement plane (which means in most cases that the y coordinate differences are discarded). + // This is because those coordinates are often not very accurate. + // A separate check is done below to make sure that the destination y coordinate is correct + if (distanceToSteeringTarget + movementPlane.ToPlane(steeringTarget - richPath.Endpoint).magnitude + movementPlane.ToPlane(destination - richPath.Endpoint).magnitude > endReachedDistance) return false; + + // Don't do height checks in 2D mode + if (orientation != OrientationMode.YAxisForward) { + // Check if the destination is above the head of the character or far below the feet of it + float yDifference; + movementPlane.ToPlane(destination - position, out yDifference); + var h = tr.localScale.y * height; + if (yDifference > h || yDifference < -h*0.5) return false; + } + + return true; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary> + public bool hasPath { get { return richPath.GetCurrentPart() != null; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary> + public bool pathPending { get { return waitingForPathCalculation || delayUpdatePath; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary> + public Vector3 steeringTarget { get; protected set; } + + /// <summary>\copydoc Pathfinding::IAstarAI::radius</summary> + float IAstarAI.radius { get { return radius; } set { radius = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::height</summary> + float IAstarAI.height { get { return height; } set { height = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary> + float IAstarAI.maxSpeed { get { return maxSpeed; } set { maxSpeed = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary> + bool IAstarAI.canSearch { get { return canSearch; } set { canSearch = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary> + bool IAstarAI.canMove { get { return canMove; } set { canMove = value; } } + + /// <summary>\copydoc Pathfinding::IAstarAI::movementPlane</summary> + NativeMovementPlane IAstarAI.movementPlane => new NativeMovementPlane(movementPlane); + + /// <summary> + /// True if approaching the last waypoint in the current part of the path. + /// Path parts are separated by off-mesh links. + /// + /// See: <see cref="approachingPathEndpoint"/> + /// </summary> + public bool approachingPartEndpoint { + get { + return lastCorner && nextCorners.Count == 1; + } + } + + /// <summary> + /// True if approaching the last waypoint of all parts in the current path. + /// Path parts are separated by off-mesh links. + /// + /// See: <see cref="approachingPartEndpoint"/> + /// </summary> + public bool approachingPathEndpoint { + get { + return approachingPartEndpoint && richPath.IsLastPart; + } + } + + /// <summary>\copydoc Pathfinding::IAstarAI::endOfPath</summary> + public override Vector3 endOfPath { + get { + if (hasPath) return richPath.Endpoint; + if (float.IsFinite(destination.x)) return destination; + return position; + } + } + + public override Quaternion rotation { + get { + return base.rotation; + } + set { + base.rotation = value; + // Make the agent keep this rotation instead of just rotating back to whatever it used before + rotationFilterState = Vector2.zero; + rotationFilterState2 = Vector2.zero; + } + } + + /// <summary> + /// \copydoc Pathfinding::IAstarAI::Teleport + /// + /// When setting transform.position directly the agent + /// will be clamped to the part of the navmesh it can + /// reach, so it may not end up where you wanted it to. + /// This ensures that the agent can move to any part of the navmesh. + /// </summary> + public override void Teleport (Vector3 newPosition, bool clearPath = true) { + base.Teleport(ClampPositionToGraph(newPosition), clearPath); + } + + protected virtual Vector3 ClampPositionToGraph (Vector3 newPosition) { + // Clamp the new position to the navmesh + var nearest = AstarPath.active != null? AstarPath.active.GetNearest(newPosition) : new NNInfo(); + float elevation; + + movementPlane.ToPlane(newPosition, out elevation); + return movementPlane.ToWorld(movementPlane.ToPlane(nearest.node != null ? nearest.position : newPosition), elevation); + } + + /// <summary>Called when the component is disabled</summary> + protected override void OnDisable () { + base.OnDisable(); + traversingOffMeshLink = false; + // Stop the off mesh link traversal coroutine + StopAllCoroutines(); + rotationFilterState = Vector2.zero; + rotationFilterState2 = Vector2.zero; + } + + protected override bool shouldRecalculatePath { + get { + // Don't automatically recalculate the path in the middle of an off-mesh link + return base.shouldRecalculatePath && !traversingOffMeshLink; + } + } + + public override void SearchPath () { + // Calculate paths after the current off-mesh link has been completed + if (traversingOffMeshLink) { + delayUpdatePath = true; + } else { + base.SearchPath(); + } + } + + protected override void OnPathComplete (Path p) { + waitingForPathCalculation = false; + p.Claim(this); + + if (p.error) { + p.Release(this); + return; + } + + if (traversingOffMeshLink) { + delayUpdatePath = true; + } else { + // The RandomPath and MultiTargetPath do not have a well defined destination that could have been + // set before the paths were calculated. So we instead set the destination here so that some properties + // like #reachedDestination and #remainingDistance work correctly. + if (p is ABPath abPath && !abPath.endPointKnownBeforeCalculation) { + destination = abPath.originalEndPoint; + } + + richPath.Initialize(seeker, p, true, funnelSimplification); + + // Check if we have already reached the end of the path + // We need to do this here to make sure that the #reachedEndOfPath + // property is up to date. + var part = richPath.GetCurrentPart() as RichFunnel; + if (part != null) { + if (updatePosition) simulatedPosition = tr.position; + + // Note: UpdateTarget has some side effects like setting the nextCorners list and the lastCorner field + var localPosition = movementPlane.ToPlane(UpdateTarget(part)); + + // Target point + steeringTarget = nextCorners[0]; + Vector2 targetPoint = movementPlane.ToPlane(steeringTarget); + distanceToSteeringTarget = (targetPoint - localPosition).magnitude; + + if (lastCorner && nextCorners.Count == 1 && distanceToSteeringTarget <= endReachedDistance) { + NextPart(); + } + } + } + p.Release(this); + } + + protected override void ClearPath () { + CancelCurrentPathRequest(); + richPath.Clear(); + lastCorner = false; + delayUpdatePath = false; + distanceToSteeringTarget = float.PositiveInfinity; + } + + /// <summary> + /// Declare that the AI has completely traversed the current part. + /// This will skip to the next part, or call OnTargetReached if this was the last part + /// </summary> + protected void NextPart () { + if (!richPath.CompletedAllParts) { + if (!richPath.IsLastPart) lastCorner = false; + richPath.NextPart(); + if (richPath.CompletedAllParts) { + OnTargetReached(); + } + } + } + + /// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,bool)}</summary> + public void GetRemainingPath (List<Vector3> buffer, out bool stale) { + richPath.GetRemainingPath(buffer, null, simulatedPosition, out stale); + } + + /// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,List<PathPartWithLinkInfo>,bool)}</summary> + public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale) { + richPath.GetRemainingPath(buffer, partsBuffer, simulatedPosition, out stale); + } + + /// <summary> + /// Called when the end of the path is reached. + /// + /// Deprecated: Avoid overriding this method. Instead poll the <see cref="reachedDestination"/> or <see cref="reachedEndOfPath"/> properties. + /// </summary> + protected virtual void OnTargetReached () { + } + + protected virtual Vector3 UpdateTarget (RichFunnel fn) { + nextCorners.Clear(); + + // This method assumes simulatedPosition is up to date as our current position. + // We read and write to tr.position as few times as possible since doing so + // is much slower than to read and write from/to a local/member variable. + bool requiresRepath; + Vector3 position = fn.Update(simulatedPosition, nextCorners, 2, out lastCorner, out requiresRepath); + + if (requiresRepath && !waitingForPathCalculation && canSearch) { + // TODO: What if canSearch is false? How do we notify other scripts that might be handling the path calculation that a new path needs to be calculated? + SearchPath(); + } + + return position; + } + + /// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary> + protected override void MovementUpdateInternal (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) { + if (updatePosition) simulatedPosition = tr.position; + if (updateRotation) simulatedRotation = tr.rotation; + + RichPathPart currentPart = richPath.GetCurrentPart(); + + if (currentPart is RichSpecial) { + // Start traversing the off mesh link if we haven't done it yet + if (!traversingOffMeshLink && !richPath.CompletedAllParts) { + StartCoroutine(TraverseSpecial(currentPart as RichSpecial)); + } + + nextPosition = steeringTarget = simulatedPosition; + nextRotation = rotation; + } else { + var funnel = currentPart as RichFunnel; + + // Check if we have a valid path to follow and some other script has not stopped the character + bool stopped = isStopped || (reachedDestination && whenCloseToDestination == CloseToDestinationMode.Stop); + if (rvoController != null) rvoDensityBehavior.Update(rvoController.enabled, reachedDestination, ref stopped, ref rvoController.priorityMultiplier, ref rvoController.flowFollowingStrength, simulatedPosition); + + if (funnel != null && !stopped) { + TraverseFunnel(funnel, deltaTime, out nextPosition, out nextRotation); + } else { + // Unknown, null path part, or the character is stopped + // Slow down as quickly as possible + velocity2D -= Vector2.ClampMagnitude(velocity2D, acceleration * deltaTime); + FinalMovement(simulatedPosition, deltaTime, float.PositiveInfinity, 1f, out nextPosition, out nextRotation); + if (funnel == null || isStopped) { + steeringTarget = simulatedPosition; + } + } + } + } + + void TraverseFunnel (RichFunnel fn, float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) { + // Clamp the current position to the navmesh + // and update the list of upcoming corners in the path + // and store that in the 'nextCorners' field + var position3D = UpdateTarget(fn); + float elevation; + Vector2 position = movementPlane.ToPlane(position3D, out elevation); + + // Only find nearby walls every 5th frame to improve performance + if (Time.frameCount % 5 == 0 && wallForce > 0 && wallDist > 0) { + wallBuffer.Clear(); + fn.FindWalls(wallBuffer, wallDist); + } + + // Target point + steeringTarget = nextCorners[0]; + Vector2 targetPoint = movementPlane.ToPlane(steeringTarget); + // Direction to target + Vector2 dir = targetPoint - position; + + // Normalized direction to the target + Vector2 normdir = VectorMath.Normalize(dir, out distanceToSteeringTarget); + // Calculate force from walls + Vector2 wallForceVector = CalculateWallForce(position, elevation, normdir); + Vector2 targetVelocity; + + if (approachingPartEndpoint) { + targetVelocity = slowdownTime > 0 ? Vector2.zero : normdir * maxSpeed; + + // Reduce the wall avoidance force as we get closer to our target + wallForceVector *= System.Math.Min(distanceToSteeringTarget/0.5f, 1); + + if (distanceToSteeringTarget <= endReachedDistance) { + // Reached the end of the path or an off mesh link + NextPart(); + } + } else { + var nextNextCorner = nextCorners.Count > 1 ? movementPlane.ToPlane(nextCorners[1]) : position + 2*dir; + targetVelocity = (nextNextCorner - targetPoint).normalized * maxSpeed; + } + + var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward)); + + // Update the velocity using the acceleration + Vector2 accel = MovementUtilities.CalculateAccelerationToReachPoint(targetPoint - position, targetVelocity, velocity2D, acceleration, rotationSpeed, maxSpeed, forwards); + velocity2D += (accel + wallForceVector*wallForce)*deltaTime; + + // Distance to the end of the path (almost as the crow flies) + var distanceToEndOfPath = distanceToSteeringTarget + Vector3.Distance(steeringTarget, fn.exactEnd); + + // How fast to move depending on the distance to the destination. + // Move slower as the character gets closer to the destination. + // This is always a value between 0 and 1. + var speedLimitFactor = distanceToEndOfPath < maxSpeed * slowdownTime? Mathf.Sqrt(distanceToEndOfPath / (maxSpeed * slowdownTime)) : 1; + + FinalMovement(position3D, deltaTime, distanceToEndOfPath, speedLimitFactor, out nextPosition, out nextRotation); + } + + void FinalMovement (Vector3 position3D, float deltaTime, float distanceToEndOfPath, float speedLimitFactor, out Vector3 nextPosition, out Quaternion nextRotation) { + var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward)); + + ApplyGravity(deltaTime); + + velocity2D = MovementUtilities.ClampVelocity(velocity2D, maxSpeed, speedLimitFactor, slowWhenNotFacingTarget && enableRotation, preventMovingBackwards, forwards); + bool avoidingAnyAgents = false; + + if (rvoController != null && rvoController.enabled) { + // Send a message to the RVOController that we want to move + // with this velocity. In the next simulation step, this + // velocity will be processed and it will be fed back to the + // rvo controller and finally it will be used by this script + // when calling the CalculateMovementDelta method below + + // Make sure that we don't move further than to the end point + // of the path. If the RVO simulation FPS is low and we did + // not do this, the agent might overshoot the target a lot. + var rvoTarget = position3D + movementPlane.ToWorld(Vector2.ClampMagnitude(velocity2D, distanceToEndOfPath)); + rvoController.SetTarget(rvoTarget, velocity2D.magnitude, maxSpeed, endOfPath); + avoidingAnyAgents = rvoController.AvoidingAnyAgents; + } + + // Direction and distance to move during this frame + var deltaPosition = lastDeltaPosition = CalculateDeltaToMoveThisFrame(position3D, distanceToEndOfPath, deltaTime); + + if (enableRotation) { + // Rotate towards the direction we are moving in + // Filter out noise in the movement direction + // This is especially important when the agent is almost standing still and when using local avoidance + float noiseThreshold = radius * tr.localScale.x * 0.2f; + float rotationSpeedFactor = MovementUtilities.FilterRotationDirection(ref rotationFilterState, ref rotationFilterState2, deltaPosition, noiseThreshold, deltaTime, avoidingAnyAgents); + nextRotation = SimulateRotationTowards(rotationFilterState, rotationSpeed * deltaTime * rotationSpeedFactor, rotationSpeed * deltaTime); + } else { + nextRotation = simulatedRotation; + } + + nextPosition = position3D + movementPlane.ToWorld(deltaPosition, verticalVelocity * deltaTime); + } + + protected override Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) { + if (richPath != null) { + var funnel = richPath.GetCurrentPart() as RichFunnel; + if (funnel != null) { + var clampedPosition = funnel.ClampToNavmesh(position); + + // Inform the RVO system about the edges of the navmesh which will allow + // it to better keep inside the navmesh in the first place. + if (rvoController != null && rvoController.enabled) rvoController.SetObstacleQuery(funnel.CurrentNode); + + // We cannot simply check for equality because some precision may be lost + // if any coordinate transformations are used. + var difference = movementPlane.ToPlane(clampedPosition - position); + float sqrDifference = difference.sqrMagnitude; + if (sqrDifference > 0.001f*0.001f) { + // The agent was outside the navmesh. Remove that component of the velocity + // so that the velocity only goes along the direction of the wall, not into it + velocity2D -= difference * Vector2.Dot(difference, velocity2D) / sqrDifference; + positionChanged = true; + // Return the new position, but ignore any changes in the y coordinate from the ClampToNavmesh method as the y coordinates in the navmesh are rarely very accurate + return position + movementPlane.ToWorld(difference); + } + } + } + + positionChanged = false; + return position; + } + + Vector2 CalculateWallForce (Vector2 position, float elevation, Vector2 directionToTarget) { + if (wallForce <= 0 || wallDist <= 0) return Vector2.zero; + + float wLeft = 0; + float wRight = 0; + + var position3D = movementPlane.ToWorld(position, elevation); + for (int i = 0; i < wallBuffer.Count; i += 2) { + Vector3 closest = VectorMath.ClosestPointOnSegment(wallBuffer[i], wallBuffer[i+1], position3D); + float dist = (closest-position3D).sqrMagnitude; + + if (dist > wallDist*wallDist) continue; + + Vector2 tang = movementPlane.ToPlane(wallBuffer[i+1]-wallBuffer[i]).normalized; + + // Using the fact that all walls are laid out clockwise (looking from inside the obstacle) + // Then left and right (ish) can be figured out like this + float dot = Vector2.Dot(directionToTarget, tang); + float weight = 1 - System.Math.Max(0, (2*(dist / (wallDist*wallDist))-1)); + if (dot > 0) wRight = System.Math.Max(wRight, dot * weight); + else wLeft = System.Math.Max(wLeft, -dot * weight); + } + + Vector2 normal = new Vector2(directionToTarget.y, -directionToTarget.x); + return normal*(wRight-wLeft); + } + + /// <summary>Traverses an off-mesh link</summary> + protected virtual IEnumerator TraverseSpecial (RichSpecial link) { + traversingOffMeshLink = true; + // The current path part is a special part, for example a link + // Movement during this part of the path is handled by the TraverseSpecial coroutine + velocity2D = Vector3.zero; + var offMeshLinkCoroutine = onTraverseOffMeshLink != null? onTraverseOffMeshLink(link) : TraverseOffMeshLinkFallback(link); + yield return StartCoroutine(offMeshLinkCoroutine); + + // Off-mesh link traversal completed + traversingOffMeshLink = false; + NextPart(); + + // If a path completed during the time we traversed the special connection, we need to recalculate it + if (delayUpdatePath) { + delayUpdatePath = false; + // TODO: What if canSearch is false? How do we notify other scripts that might be handling the path calculation that a new path needs to be calculated? + if (canSearch) SearchPath(); + } + } + + /// <summary> + /// Fallback for traversing off-mesh links in case <see cref="onTraverseOffMeshLink"/> is not set. + /// This will do a simple linear interpolation along the link. + /// </summary> + protected IEnumerator TraverseOffMeshLinkFallback (RichSpecial link) { + float duration = maxSpeed > 0 ? Vector3.Distance(link.second.position, link.first.position) / maxSpeed : 1; + float startTime = Time.time; + + while (true) { + var pos = Vector3.Lerp(link.first.position, link.second.position, Mathf.InverseLerp(startTime, startTime + duration, Time.time)); + if (updatePosition) tr.position = pos; + else simulatedPosition = pos; + + if (Time.time >= startTime + duration) break; + yield return null; + } + } + + protected static readonly Color GizmoColorPath = new Color(8.0f/255, 78.0f/255, 194.0f/255); + + public override void DrawGizmos () { + base.DrawGizmos(); + + if (tr != null) { + Vector3 lastPosition = position; + for (int i = 0; i < nextCorners.Count; lastPosition = nextCorners[i], i++) { + Draw.Line(lastPosition, nextCorners[i], GizmoColorPath); + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichAI.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichAI.cs.meta new file mode 100644 index 0000000..d2bc561 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichAI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce11ea984e202491d9271f53021d8b89 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichPath.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichPath.cs new file mode 100644 index 0000000..268a34e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichPath.cs @@ -0,0 +1,797 @@ +using UnityEngine; +using System.Collections.Generic; +using Pathfinding.Util; +using UnityEngine.Assertions; + +namespace Pathfinding { + public class RichPath { + int currentPart; + readonly List<RichPathPart> parts = new List<RichPathPart>(); + + public Seeker seeker; + + /// <summary> + /// Transforms points from path space to world space. + /// If null the identity transform will be used. + /// + /// This is used when the world position of the agent does not match the + /// corresponding position on the graph. This is the case in the example + /// scene called 'Moving'. + /// + /// See: <see cref="Pathfinding.Examples.LocalSpaceRichAI"/> + /// </summary> + public ITransform transform; + + public RichPath () { + Clear(); + } + + public void Clear () { + parts.Clear(); + currentPart = 0; + Endpoint = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + } + + /// <summary>Use this for initialization.</summary> + /// <param name="seeker">Optionally provide in order to take tag penalties into account. May be null if you do not use a Seeker\</param> + /// <param name="path">Path to follow</param> + /// <param name="mergePartEndpoints">If true, then adjacent parts that the path is split up in will + /// try to use the same start/end points. For example when using a link on a navmesh graph + /// Instead of first following the path to the center of the node where the link is and then + /// follow the link, the path will be adjusted to go to the exact point where the link starts + /// which usually makes more sense.</param> + /// <param name="simplificationMode">The path can optionally be simplified. This can be a bit expensive for long paths.</param> + public void Initialize (Seeker seeker, Path path, bool mergePartEndpoints, bool simplificationMode) { + if (path.error) throw new System.ArgumentException("Path has an error"); + + List<GraphNode> nodes = path.path; + if (nodes.Count == 0) throw new System.ArgumentException("Path traverses no nodes"); + + this.seeker = seeker; + // Release objects back to object pool + // Yeah, I know, it's casting... but this won't be called much + for (int i = 0; i < parts.Count; i++) { + var funnelPart = parts[i] as RichFunnel; + var specialPart = parts[i] as RichSpecial; + if (funnelPart != null) ObjectPool<RichFunnel>.Release(ref funnelPart); + else if (specialPart != null) ObjectPool<RichSpecial>.Release(ref specialPart); + } + + Clear(); + + // Initialize new + Endpoint = path.vectorPath[path.vectorPath.Count-1]; + + //Break path into parts + for (int i = 0; i < nodes.Count; i++) { + if (nodes[i] is TriangleMeshNode) { + var graph = AstarData.GetGraph(nodes[i]) as NavmeshBase; + if (graph == null) throw new System.Exception("Found a TriangleMeshNode that was not in a NavmeshBase graph"); + + RichFunnel f = ObjectPool<RichFunnel>.Claim().Initialize(this, graph); + + f.funnelSimplification = simplificationMode; + + int sIndex = i; + uint currentGraphIndex = nodes[sIndex].GraphIndex; + + + for (; i < nodes.Count; i++) { + if (nodes[i].GraphIndex != currentGraphIndex && !(nodes[i] is NodeLink3Node)) { + break; + } + } + i--; + + if (sIndex == 0) { + f.exactStart = path.vectorPath[0]; + } else { + f.exactStart = (Vector3)nodes[mergePartEndpoints ? sIndex-1 : sIndex].position; + } + + if (i == nodes.Count-1) { + f.exactEnd = path.vectorPath[path.vectorPath.Count-1]; + } else { + f.exactEnd = (Vector3)nodes[mergePartEndpoints ? i+1 : i].position; + } + + f.BuildFunnelCorridor(nodes, sIndex, i); + + parts.Add(f); + } else if (nodes[i] is LinkNode nl) { + int sIndex = i; + uint currentGraphIndex = nodes[sIndex].GraphIndex; + + while (i < nodes.Count && nodes[i].GraphIndex == currentGraphIndex) i++; + i--; + + if (i - sIndex > 1) { + throw new System.Exception("NodeLink2 path length greater than two (2) nodes. " + (i - sIndex)); + } else if (i - sIndex == 0) { + // The link is a single node. + // Just ignore it. It can happen in very rare circumstances with some path types. + // For example, a RandomPath can stop at the first node of a node link, without including the other end of the link + continue; + } + Assert.AreEqual(nl.linkConcrete, (nodes[i] as LinkNode).linkConcrete); + + RichSpecial rps = ObjectPool<RichSpecial>.Claim().Initialize(nl.linkConcrete.GetTracer(nl)); + parts.Add(rps); + } else if (!(nodes[i] is PointNode)) { + // Some other graph type which we do not have support for + throw new System.InvalidOperationException("The RichAI movment script can only be used on recast/navmesh graphs. A node of type " + nodes[i].GetType().Name + " was in the path."); + } + } + } + + public Vector3 Endpoint { get; private set; } + + /// <summary>True if we have completed (called NextPart for) the last part in the path</summary> + public bool CompletedAllParts { + get { + return currentPart >= parts.Count; + } + } + + /// <summary>True if we are traversing the last part of the path</summary> + public bool IsLastPart { + get { + return currentPart >= parts.Count - 1; + } + } + + public void NextPart () { + currentPart = Mathf.Min(currentPart + 1, parts.Count); + } + + public RichPathPart GetCurrentPart () { + if (parts.Count == 0) return null; + return currentPart < parts.Count ? parts[currentPart] : parts[parts.Count - 1]; + } + + /// <summary> + /// Replaces the buffer with the remaining path. + /// See: <see cref="Pathfinding.IAstarAI.GetRemainingPath"/> + /// </summary> + public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, Vector3 currentPosition, out bool requiresRepath) { + buffer.Clear(); + buffer.Add(currentPosition); + requiresRepath = false; + for (int i = currentPart; i < parts.Count; i++) { + var part = parts[i]; + if (part is RichFunnel funnel) { + bool lastCorner; + var startIndex = buffer.Count; + if (i != 0) buffer.Add(funnel.exactStart); + funnel.Update(i == 0 ? currentPosition : funnel.exactStart, buffer, int.MaxValue, out lastCorner, out requiresRepath); + if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo(startIndex, buffer.Count-1)); + if (requiresRepath) { + return; + } + } else if (part is RichSpecial rs) { + // By adding all points above the link will look like just a stright line, which is reasonable + // The part's start/end indices refer to the last point in previous part and first point in the next part, respectively + if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo(buffer.Count-1, buffer.Count, rs.nodeLink)); + } + } + } + } + + public abstract class RichPathPart : Pathfinding.Util.IAstarPooledObject { + public abstract void OnEnterPool(); + } + + public class RichFunnel : RichPathPart { + readonly List<Vector3> left; + readonly List<Vector3> right; + List<TriangleMeshNode> nodes; + public Vector3 exactStart; + public Vector3 exactEnd; + NavmeshBase graph; + int currentNode; + Vector3 currentPosition; + int checkForDestroyedNodesCounter; + RichPath path; + int[] triBuffer = new int[3]; + + /// <summary>Post process the funnel corridor or not</summary> + public bool funnelSimplification = true; + + public RichFunnel () { + left = Pathfinding.Util.ListPool<Vector3>.Claim(); + right = Pathfinding.Util.ListPool<Vector3>.Claim(); + nodes = new List<TriangleMeshNode>(); + this.graph = null; + } + + /// <summary>Works like a constructor, but can be used even for pooled objects. Returns this for easy chaining</summary> + public RichFunnel Initialize (RichPath path, NavmeshBase graph) { + if (graph == null) throw new System.ArgumentNullException("graph"); + if (this.graph != null) throw new System.InvalidOperationException("Trying to initialize an already initialized object. " + graph); + + this.graph = graph; + this.path = path; + return this; + } + + public override void OnEnterPool () { + left.Clear(); + right.Clear(); + nodes.Clear(); + graph = null; + currentNode = 0; + checkForDestroyedNodesCounter = 0; + } + + public TriangleMeshNode CurrentNode { + get { + var node = nodes[currentNode]; + if (!node.Destroyed) { + return node; + } + return null; + } + } + + /// <summary> + /// Build a funnel corridor from a node list slice. + /// The nodes are assumed to be of type TriangleMeshNode. + /// </summary> + /// <param name="nodes">Nodes to build the funnel corridor from</param> + /// <param name="start">Start index in the nodes list</param> + /// <param name="end">End index in the nodes list, this index is inclusive</param> + public void BuildFunnelCorridor (List<GraphNode> nodes, int start, int end) { + //Make sure start and end points are on the correct nodes + exactStart = (nodes[start] as MeshNode).ClosestPointOnNode(exactStart); + exactEnd = (nodes[end] as MeshNode).ClosestPointOnNode(exactEnd); + + left.Clear(); + right.Clear(); + left.Add(exactStart); + right.Add(exactStart); + + this.nodes.Clear(); + + if (funnelSimplification) { + List<GraphNode> tmp = ListPool<GraphNode>.Claim(end-start); + + var tagPenalties = path.seeker != null ? path.seeker.tagPenalties : Path.ZeroTagPenalties; + var traversableTags = path.seeker != null ? path.seeker.traversableTags : -1; + + Funnel.Simplify(new Funnel.PathPart { + startIndex = start, + endIndex = end, + startPoint = exactStart, + endPoint = exactEnd, + type = Funnel.PartType.NodeSequence, + }, graph, nodes, tmp, tagPenalties, traversableTags); + + if (this.nodes.Capacity < tmp.Count) this.nodes.Capacity = tmp.Count; + + for (int i = 0; i < tmp.Count; i++) { + // Guaranteed to be TriangleMeshNodes since they are all in the same graph + var node = tmp[i] as TriangleMeshNode; + if (node != null) this.nodes.Add(node); + } + + ListPool<GraphNode>.Release(ref tmp); + } else { + if (this.nodes.Capacity < end-start) this.nodes.Capacity = (end-start); + for (int i = start; i <= end; i++) { + // Guaranteed to be TriangleMeshNodes since they are all in the same graph + var node = nodes[i] as TriangleMeshNode; + if (node != null) this.nodes.Add(node); + } + } + + for (int i = 0; i < this.nodes.Count-1; i++) { + if (this.nodes[i].GetPortal(this.nodes[i+1], out var leftP, out var rightP)) { + left.Add(leftP); + right.Add(rightP); + } else { + // Can happen in case custom connections have been added + left.Add((Vector3)this.nodes[i].position); + right.Add((Vector3)this.nodes[i].position); + left.Add((Vector3)this.nodes[i+1].position); + right.Add((Vector3)this.nodes[i+1].position); + } + } + + left.Add(exactEnd); + right.Add(exactEnd); + } + + /// <summary> + /// Split funnel at node index splitIndex and throw the nodes up to that point away and replace with prefix. + /// Used when the AI has happened to get sidetracked and entered a node outside the funnel. + /// </summary> + void UpdateFunnelCorridor (int splitIndex, List<TriangleMeshNode> prefix) { + nodes.RemoveRange(0, splitIndex); + nodes.InsertRange(0, prefix); + + left.Clear(); + right.Clear(); + left.Add(exactStart); + right.Add(exactStart); + + for (int i = 0; i < nodes.Count-1; i++) { + if (nodes[i].GetPortal(nodes[i+1], out var leftP, out var rightP)) { + left.Add(leftP); + right.Add(rightP); + } + } + + left.Add(exactEnd); + right.Add(exactEnd); + } + + /// <summary>True if any node in the path is destroyed</summary> + bool CheckForDestroyedNodes () { + // Loop through all nodes and check if they are destroyed + // If so, we really need a recalculation of our path quickly + // since there might be an obstacle blocking our path after + // a graph update or something similar + for (int i = 0, t = nodes.Count; i < t; i++) { + if (nodes[i].Destroyed) { + return true; + } + } + return false; + } + + /// <summary> + /// Approximate distance (as the crow flies) to the endpoint of this path part. + /// See: <see cref="exactEnd"/> + /// </summary> + public float DistanceToEndOfPath { + get { + var currentNode = CurrentNode; + Vector3 closestOnNode = currentNode != null? currentNode.ClosestPointOnNode(currentPosition) : currentPosition; + return (exactEnd - closestOnNode).magnitude; + } + } + + /// <summary> + /// Clamps the position to the navmesh and repairs the path if the agent has moved slightly outside it. + /// You should not call this method with anything other than the agent's position. + /// </summary> + public Vector3 ClampToNavmesh (Vector3 position) { + if (path.transform != null) position = path.transform.InverseTransform(position); + UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x)); + ClampToNavmeshInternal(ref position); + if (path.transform != null) position = path.transform.Transform(position); + UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x)); + return position; + } + + /// <summary> + /// Find the next points to move towards and clamp the position to the navmesh. + /// + /// Returns: The position of the agent clamped to make sure it is inside the navmesh. + /// </summary> + /// <param name="position">The position of the agent.</param> + /// <param name="buffer">Will be filled with up to numCorners points which are the next points in the path towards the target.</param> + /// <param name="numCorners">See buffer.</param> + /// <param name="lastCorner">True if the buffer contains the end point of the path.</param> + /// <param name="requiresRepath">True if nodes along the path have been destroyed and a path recalculation is necessary.</param> + public Vector3 Update (Vector3 position, List<Vector3> buffer, int numCorners, out bool lastCorner, out bool requiresRepath) { + if (path.transform != null) position = path.transform.InverseTransform(position); + UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x)); + + lastCorner = false; + requiresRepath = false; + + // Only check for destroyed nodes every 10 frames + if (checkForDestroyedNodesCounter >= 10) { + checkForDestroyedNodesCounter = 0; + requiresRepath |= CheckForDestroyedNodes(); + } else { + checkForDestroyedNodesCounter++; + } + + bool nodesDestroyed = ClampToNavmeshInternal(ref position); + + currentPosition = position; + + if (nodesDestroyed) { + // Some nodes on the path have been destroyed + // we need to recalculate the path immediately + requiresRepath = true; + lastCorner = false; + buffer.Add(position); + } else if (!FindNextCorners(position, currentNode, buffer, numCorners, out lastCorner)) { + Debug.LogError("Failed to find next corners in the path"); + buffer.Add(position); + } + + if (path.transform != null) { + for (int i = 0; i < buffer.Count; i++) { + buffer[i] = path.transform.Transform(buffer[i]); + } + + position = path.transform.Transform(position); + UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x)); + } + + return position; + } + + /// <summary>Cached object to avoid unnecessary allocations</summary> + static Queue<TriangleMeshNode> navmeshClampQueue = new Queue<TriangleMeshNode>(); + /// <summary>Cached object to avoid unnecessary allocations</summary> + static List<TriangleMeshNode> navmeshClampList = new List<TriangleMeshNode>(); + /// <summary>Cached object to avoid unnecessary allocations</summary> + static Dictionary<TriangleMeshNode, TriangleMeshNode> navmeshClampDict = new Dictionary<TriangleMeshNode, TriangleMeshNode>(); + + /// <summary> + /// Searches for the node the agent is inside. + /// This will also clamp the position to the navmesh + /// and repair the funnel cooridor if the agent moves slightly outside it. + /// + /// Returns: True if nodes along the path have been destroyed so that a path recalculation is required + /// </summary> + bool ClampToNavmeshInternal (ref Vector3 position) { + var previousNode = nodes[currentNode]; + + if (previousNode.Destroyed) { + return true; + } + + // Check if we are in the same node as we were in during the last frame and otherwise do a more extensive search + if (previousNode.ContainsPoint(position)) { + return false; + } + + // This part of the code is relatively seldom called + // Most of the time we are still on the same node as during the previous frame + + var que = navmeshClampQueue; + var allVisited = navmeshClampList; + var parent = navmeshClampDict; + previousNode.TemporaryFlag1 = true; + parent[previousNode] = null; + que.Enqueue(previousNode); + allVisited.Add(previousNode); + + float bestDistance = float.PositiveInfinity; + Vector3 bestPoint = position; + TriangleMeshNode bestNode = null; + + while (que.Count > 0) { + var node = que.Dequeue(); + + // Snap to the closest point in XZ space (keep the Y coordinate) + // If we would have snapped to the closest point in 3D space, the agent + // might slow down when traversing slopes + var closest = node.ClosestPointOnNodeXZ(position); + var dist = VectorMath.MagnitudeXZ(closest - position); + + // Check if this node is any closer than the previous best node. + // Allow for a small margin to both avoid floating point errors and to allow + // moving past very small local minima. + if (dist <= bestDistance * 1.05f + 0.001f) { + if (dist < bestDistance) { + bestDistance = dist; + bestPoint = closest; + bestNode = node; + } + + for (int i = 0; i < node.connections.Length; i++) { + if (!node.connections[i].isOutgoing) continue; + var neighbour = node.connections[i].node as TriangleMeshNode; + if (neighbour != null && !neighbour.TemporaryFlag1) { + neighbour.TemporaryFlag1 = true; + parent[neighbour] = node; + que.Enqueue(neighbour); + allVisited.Add(neighbour); + } + } + } + } + + UnityEngine.Assertions.Assert.IsNotNull(bestNode); + + for (int i = 0; i < allVisited.Count; i++) allVisited[i].TemporaryFlag1 = false; + allVisited.ClearFast(); + + var closestNodeInPath = nodes.IndexOf(bestNode); + + // Move the x and z coordinates of the chararacter but not the y coordinate + // because the navmesh surface may not line up with the ground + position.x = bestPoint.x; + position.z = bestPoint.z; + + // Check if the closest node + // was on the path already or if we need to adjust it + if (closestNodeInPath == -1) { + // Reuse this list, because why not. + var prefix = navmeshClampList; + + while (closestNodeInPath == -1) { + prefix.Add(bestNode); + bestNode = parent[bestNode]; + closestNodeInPath = nodes.IndexOf(bestNode); + } + + // We have found a node containing the position, but it is outside the funnel + // Recalculate the funnel to include this node + exactStart = position; + UpdateFunnelCorridor(closestNodeInPath, prefix); + + prefix.ClearFast(); + + // Restart from the first node in the updated path + currentNode = 0; + } else { + currentNode = closestNodeInPath; + } + + parent.Clear(); + // Do a quick check to see if the next node in the path has been destroyed + // If that is the case then we should plan a new path immediately + return currentNode + 1 < nodes.Count && nodes[currentNode+1].Destroyed; + } + + /// <summary> + /// Fill wallBuffer with all navmesh wall segments close to the current position. + /// A wall segment is a node edge which is not shared by any other neighbour node, i.e an outer edge on the navmesh. + /// </summary> + public void FindWalls (List<Vector3> wallBuffer, float range) { + FindWalls(currentNode, wallBuffer, currentPosition, range); + } + + void FindWalls (int nodeIndex, List<Vector3> wallBuffer, Vector3 position, float range) { + if (range <= 0) return; + + bool negAbort = false; + bool posAbort = false; + + range *= range; + + position.y = 0; + //Looping as 0,-1,1,-2,2,-3,3,-4,4 etc. Avoids code duplication by keeping it to one loop instead of two + for (int i = 0; !negAbort || !posAbort; i = i < 0 ? -i : -i-1) { + if (i < 0 && negAbort) continue; + if (i > 0 && posAbort) continue; + + if (i < 0 && nodeIndex+i < 0) { + negAbort = true; + continue; + } + + if (i > 0 && nodeIndex+i >= nodes.Count) { + posAbort = true; + continue; + } + + TriangleMeshNode prev = nodeIndex+i-1 < 0 ? null : nodes[nodeIndex+i-1]; + TriangleMeshNode node = nodes[nodeIndex+i]; + TriangleMeshNode next = nodeIndex+i+1 >= nodes.Count ? null : nodes[nodeIndex+i+1]; + + if (node.Destroyed) { + break; + } + + if ((node.ClosestPointOnNodeXZ(position)-position).sqrMagnitude > range) { + if (i < 0) negAbort = true; + else posAbort = true; + continue; + } + + for (int j = 0; j < 3; j++) triBuffer[j] = 0; + + for (int j = 0; j < node.connections.Length; j++) { + var other = node.connections[j].node as TriangleMeshNode; + if (other == null) continue; + + int va = -1; + for (int a = 0; a < 3; a++) { + for (int b = 0; b < 3; b++) { + if (node.GetVertex(a) == other.GetVertex((b+1) % 3) && node.GetVertex((a+1) % 3) == other.GetVertex(b)) { + va = a; + a = 3; + break; + } + } + } + if (va == -1) { + //No direct connection + } else { + triBuffer[va] = other == prev || other == next ? 2 : 1; + } + } + + for (int j = 0; j < 3; j++) { + //Tribuffer values + // 0 : Navmesh border, outer edge + // 1 : Inner edge, to node inside funnel + // 2 : Inner edge, to node outside funnel + if (triBuffer[j] == 0) { + //Add edge to list of walls + wallBuffer.Add((Vector3)node.GetVertex(j)); + wallBuffer.Add((Vector3)node.GetVertex((j+1) % 3)); + } + } + } + + if (path.transform != null) { + for (int i = 0; i < wallBuffer.Count; i++) { + wallBuffer[i] = path.transform.Transform(wallBuffer[i]); + } + } + } + + bool FindNextCorners (Vector3 origin, int startIndex, List<Vector3> funnelPath, int numCorners, out bool lastCorner) { + lastCorner = false; + + if (left == null) throw new System.Exception("left list is null"); + if (right == null) throw new System.Exception("right list is null"); + if (funnelPath == null) throw new System.ArgumentNullException("funnelPath"); + + if (left.Count != right.Count) throw new System.ArgumentException("left and right lists must have equal length"); + + int diagonalCount = left.Count; + + if (diagonalCount == 0) throw new System.ArgumentException("no diagonals"); + + if (diagonalCount-startIndex < 3) { + //Direct path + funnelPath.Add(left[diagonalCount-1]); + lastCorner = true; + return true; + } + +#if ASTARDEBUG + for (int i = startIndex; i < left.Count-1; i++) { + Debug.DrawLine(left[i], left[i+1], Color.red); + Debug.DrawLine(right[i], right[i+1], Color.magenta); + Debug.DrawRay(right[i], Vector3.up, Color.magenta); + } + for (int i = 0; i < left.Count; i++) { + Debug.DrawLine(right[i], left[i], Color.cyan); + } +#endif + + //Remove identical vertices + while (left[startIndex+1] == left[startIndex+2] && right[startIndex+1] == right[startIndex+2]) { + //System.Console.WriteLine ("Removing identical left and right"); + //left.RemoveAt (1); + //right.RemoveAt (1); + startIndex++; + + if (diagonalCount-startIndex <= 3) { + return false; + } + } + + Vector3 swPoint = left[startIndex+2]; + if (swPoint == left[startIndex+1]) { + swPoint = right[startIndex+2]; + } + + + //Test + while (VectorMath.IsColinearXZ(origin, left[startIndex+1], right[startIndex+1]) || VectorMath.RightOrColinearXZ(left[startIndex+1], right[startIndex+1], swPoint) == VectorMath.RightOrColinearXZ(left[startIndex+1], right[startIndex+1], origin)) { +#if ASTARDEBUG + Debug.DrawLine(left[startIndex+1], right[startIndex+1], new Color(0, 0, 0, 0.5F)); + Debug.DrawLine(origin, swPoint, new Color(0, 0, 0, 0.5F)); +#endif + //left.RemoveAt (1); + //right.RemoveAt (1); + startIndex++; + + if (diagonalCount-startIndex < 3) { + //Debug.Log ("#2 " + left.Count + " - " + startIndex + " = " + (left.Count-startIndex)); + //Direct path + funnelPath.Add(left[diagonalCount-1]); + lastCorner = true; + return true; + } + + swPoint = left[startIndex+2]; + if (swPoint == left[startIndex+1]) { + swPoint = right[startIndex+2]; + } + } + + + //funnelPath.Add (origin); + + Vector3 portalApex = origin; + Vector3 portalLeft = left[startIndex+1]; + Vector3 portalRight = right[startIndex+1]; + + int apexIndex = startIndex+0; + int rightIndex = startIndex+1; + int leftIndex = startIndex+1; + + for (int i = startIndex+2; i < diagonalCount; i++) { + if (funnelPath.Count >= numCorners) { + return true; + } + + if (funnelPath.Count > 2000) { + Debug.LogWarning("Avoiding infinite loop. Remove this check if you have this long paths."); + break; + } + + Vector3 pLeft = left[i]; + Vector3 pRight = right[i]; + + /*Debug.DrawLine (portalApex,portalLeft,Color.red); + * Debug.DrawLine (portalApex,portalRight,Color.yellow); + * Debug.DrawLine (portalApex,left,Color.cyan); + * Debug.DrawLine (portalApex,right,Color.cyan);*/ + + if (VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalRight, pRight) >= 0) { + if (portalApex == portalRight || VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalLeft, pRight) <= 0) { + portalRight = pRight; + rightIndex = i; + } else { + funnelPath.Add(portalLeft); + portalApex = portalLeft; + apexIndex = leftIndex; + + portalLeft = portalApex; + portalRight = portalApex; + + leftIndex = apexIndex; + rightIndex = apexIndex; + + i = apexIndex; + + continue; + } + } + + if (VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalLeft, pLeft) <= 0) { + if (portalApex == portalLeft || VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalRight, pLeft) >= 0) { + portalLeft = pLeft; + leftIndex = i; + } else { + funnelPath.Add(portalRight); + portalApex = portalRight; + apexIndex = rightIndex; + + portalLeft = portalApex; + portalRight = portalApex; + + leftIndex = apexIndex; + rightIndex = apexIndex; + + i = apexIndex; + + continue; + } + } + } + + lastCorner = true; + funnelPath.Add(left[diagonalCount-1]); + + return true; + } + } + + public struct FakeTransform { + public Vector3 position; + public Quaternion rotation; + } + + public class RichSpecial : RichPathPart { + public OffMeshLinks.OffMeshLinkTracer nodeLink; + public FakeTransform first => new FakeTransform { position = nodeLink.relativeStart, rotation = nodeLink.isReverse ? nodeLink.link.end.rotation : nodeLink.link.start.rotation }; + public FakeTransform second => new FakeTransform { position = nodeLink.relativeEnd, rotation = nodeLink.isReverse ? nodeLink.link.start.rotation : nodeLink.link.end.rotation }; + public bool reverse => nodeLink.isReverse; + + public override void OnEnterPool () { + nodeLink = default; + } + + /// <summary>Works like a constructor, but can be used even for pooled objects. Returns this for easy chaining</summary> + public RichSpecial Initialize (OffMeshLinks.OffMeshLinkTracer nodeLink) { + this.nodeLink = nodeLink; + return this; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichPath.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichPath.cs.meta new file mode 100644 index 0000000..bad8489 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/RichPath.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6865b0d3859fe4641ad0839b829e00d2 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/Seeker.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/Seeker.cs new file mode 100644 index 0000000..41d40ff --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/Seeker.cs @@ -0,0 +1,709 @@ +using UnityEngine; +using System.Collections.Generic; +using Pathfinding.Util; +using UnityEngine.Profiling; + +namespace Pathfinding { + using Pathfinding.Drawing; + + /// <summary> + /// Handles path calls for a single unit. + /// + /// This is a component which is meant to be attached to a single unit (AI, Robot, Player, whatever) to handle its pathfinding calls. + /// It also handles post-processing of paths using modifiers. + /// + /// See: calling-pathfinding (view in online documentation for working links) + /// See: modifiers (view in online documentation for working links) + /// </summary> + [AddComponentMenu("Pathfinding/Seeker")] + [HelpURL("https://arongranberg.com/astar/documentation/stable/seeker.html")] + public class Seeker : VersionedMonoBehaviour { + /// <summary> + /// Enables drawing of the last calculated path using Gizmos. + /// The path will show up in green. + /// + /// See: OnDrawGizmos + /// </summary> + public bool drawGizmos = true; + + /// <summary> + /// Enables drawing of the non-postprocessed path using Gizmos. + /// The path will show up in orange. + /// + /// Requires that <see cref="drawGizmos"/> is true. + /// + /// This will show the path before any post processing such as smoothing is applied. + /// + /// See: drawGizmos + /// See: OnDrawGizmos + /// </summary> + public bool detailedGizmos; + + /// <summary>Path modifier which tweaks the start and end points of a path</summary> + [HideInInspector] + public StartEndModifier startEndModifier = new StartEndModifier(); + + /// <summary> + /// The tags which the Seeker can traverse. + /// + /// Note: This field is a bitmask. + /// See: bitmasks (view in online documentation for working links) + /// </summary> + [HideInInspector] + public int traversableTags = -1; + + /// <summary> + /// Penalties for each tag. + /// Tag 0 which is the default tag, will have added a penalty of tagPenalties[0]. + /// These should only be positive values since the A* algorithm cannot handle negative penalties. + /// + /// The length of this array should be exactly 32, one for each tag. + /// + /// See: Pathfinding.Path.tagPenalties + /// </summary> + [HideInInspector] + public int[] tagPenalties = new int[32]; + + /// <summary> + /// Graphs that this Seeker 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> seeker.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> + /// + /// Some overloads of the <see cref="StartPath"/> methods take a graphMask parameter. If those overloads are used then they + /// will override the graph mask for that path request. + /// + /// [Open online documentation to see images] + /// + /// See: multiple-agent-types (view in online documentation for working links) + /// </summary> + [HideInInspector] + public GraphMask graphMask = GraphMask.everything; + + /// <summary> + /// Custom traversal provider to calculate which nodes are traversable and their penalties. + /// + /// This can be used to override the built-in pathfinding logic. + /// + /// <code> + /// seeker.traversalProvider = new MyCustomTraversalProvider(); + /// </code> + /// + /// See: traversal_provider (view in online documentation for working links) + /// </summary> + public ITraversalProvider traversalProvider; + + /// <summary>Used for serialization backwards compatibility</summary> + [UnityEngine.Serialization.FormerlySerializedAs("graphMask")] + int graphMaskCompatibility = -1; + + /// <summary> + /// Callback for when a path is completed. + /// Movement scripts should register to this delegate. + /// A temporary callback can also be set when calling StartPath, but that delegate will only be called for that path + /// + /// <code> + /// public void Start () { + /// // Assumes a Seeker component is attached to the GameObject + /// Seeker seeker = GetComponent<Seeker>(); + /// + /// // seeker.pathCallback is a OnPathDelegate, we add the function OnPathComplete to it so it will be called whenever a path has finished calculating on that seeker + /// seeker.pathCallback += OnPathComplete; + /// } + /// + /// public void OnPathComplete (Path p) { + /// Debug.Log("This is called when a path is completed on the seeker attached to this GameObject"); + /// } + /// </code> + /// + /// Deprecated: Pass a callback every time to the StartPath method instead, or use ai.SetPath+ai.pathPending on the movement script. You can cache it in your own script if you want to avoid the GC allocation of creating a new delegate. + /// </summary> + [System.Obsolete("Pass a callback every time to the StartPath method instead, or use ai.SetPath+ai.pathPending on the movement script. You can cache it in your own script if you want to avoid the GC allocation of creating a new delegate.")] + public OnPathDelegate pathCallback; + + /// <summary>Called before pathfinding is started</summary> + public OnPathDelegate preProcessPath; + + /// <summary>Called after a path has been calculated, right before modifiers are executed.</summary> + public OnPathDelegate postProcessPath; + +#if UNITY_EDITOR + /// <summary>Used for drawing gizmos</summary> + [System.NonSerialized] + List<Vector3> lastCompletedVectorPath; + + /// <summary>Used for drawing gizmos</summary> + [System.NonSerialized] + List<GraphNode> lastCompletedNodePath; +#endif + + /// <summary>The current path</summary> + [System.NonSerialized] + protected Path path; + + /// <summary>Previous path. Used to draw gizmos</summary> + [System.NonSerialized] + private Path prevPath; + + /// <summary>Cached delegate to avoid allocating one every time a path is started</summary> + private readonly OnPathDelegate onPathDelegate; + /// <summary>Cached delegate to avoid allocating one every time a path is started</summary> + private readonly OnPathDelegate onPartialPathDelegate; + + /// <summary>Temporary callback only called for the current path. This value is set by the StartPath functions</summary> + private OnPathDelegate tmpPathCallback; + + /// <summary>The path ID of the last path queried</summary> + protected uint lastPathID; + + /// <summary>Internal list of all modifiers</summary> + readonly List<IPathModifier> modifiers = new List<IPathModifier>(); + + public enum ModifierPass { + PreProcess, + // An obsolete item occupied index 1 previously + PostProcess = 2, + } + + public Seeker () { + onPathDelegate = OnPathComplete; + onPartialPathDelegate = OnPartialPathComplete; + } + + /// <summary>Initializes a few variables</summary> + protected override void Awake () { + base.Awake(); + startEndModifier.Awake(this); + } + + /// <summary> + /// Path that is currently being calculated or was last calculated. + /// You should rarely have to use this. Instead get the path when the path callback is called. + /// + /// See: <see cref="StartPath"/> + /// </summary> + public Path GetCurrentPath() => path; + + /// <summary> + /// Stop calculating the current path request. + /// If this Seeker is currently calculating a path it will be canceled. + /// The callback (usually to a method named OnPathComplete) will soon be called + /// with a path that has the 'error' field set to true. + /// + /// This does not stop the character from moving, it just aborts + /// the path calculation. + /// </summary> + /// <param name="pool">If true then the path will be pooled when the pathfinding system is done with it.</param> + public void CancelCurrentPathRequest (bool pool = true) { + if (!IsDone()) { + path.FailWithError("Canceled by script (Seeker.CancelCurrentPathRequest)"); + if (pool) { + // Make sure the path has had its reference count incremented and decremented once. + // If this is not done the system will think no pooling is used at all and will not pool the path. + // The particular object that is used as the parameter (in this case 'path') doesn't matter at all + // it just has to be *some* object. + path.Claim(path); + path.Release(path); + } + } + } + + /// <summary> + /// Cleans up some variables. + /// Releases any eventually claimed paths. + /// Calls OnDestroy on the <see cref="startEndModifier"/>. + /// + /// See: <see cref="ReleaseClaimedPath"/> + /// See: <see cref="startEndModifier"/> + /// </summary> + void OnDestroy () { + ReleaseClaimedPath(); + startEndModifier.OnDestroy(this); + } + + /// <summary> + /// Releases the path used for gizmos (if any). + /// The seeker keeps the latest path claimed so it can draw gizmos. + /// In some cases this might not be desireable and you want it released. + /// In that case, you can call this method to release it (not that path gizmos will then not be drawn). + /// + /// If you didn't understand anything from the description above, you probably don't need to use this method. + /// + /// See: pooling (view in online documentation for working links) + /// </summary> + void ReleaseClaimedPath () { + if (prevPath != null) { + prevPath.Release(this, true); + prevPath = null; + } + } + + /// <summary>Called by modifiers to register themselves</summary> + public void RegisterModifier (IPathModifier modifier) { + modifiers.Add(modifier); + + // Sort the modifiers based on their specified order + modifiers.Sort((a, b) => a.Order.CompareTo(b.Order)); + } + + /// <summary>Called by modifiers when they are disabled or destroyed</summary> + public void DeregisterModifier (IPathModifier modifier) { + modifiers.Remove(modifier); + } + + /// <summary> + /// Post Processes the path. + /// This will run any modifiers attached to this GameObject on the path. + /// This is identical to calling RunModifiers(ModifierPass.PostProcess, path) + /// See: <see cref="RunModifiers"/> + /// </summary> + public void PostProcess (Path path) { + RunModifiers(ModifierPass.PostProcess, path); + } + + /// <summary>Runs modifiers on a path</summary> + public void RunModifiers (ModifierPass pass, Path path) { + if (pass == ModifierPass.PreProcess) { + if (preProcessPath != null) preProcessPath(path); + + for (int i = 0; i < modifiers.Count; i++) modifiers[i].PreProcess(path); + } else if (pass == ModifierPass.PostProcess) { + Profiler.BeginSample("Running Path Modifiers"); + // Call delegates if they exist + if (postProcessPath != null) postProcessPath(path); + + // Loop through all modifiers and apply post processing + for (int i = 0; i < modifiers.Count; i++) modifiers[i].Apply(path); + Profiler.EndSample(); + } + } + + /// <summary> + /// Is the current path done calculating. + /// Returns true if the current <see cref="path"/> has been returned or if the <see cref="path"/> is null. + /// + /// Note: Do not confuse this with Pathfinding.Path.IsDone. They usually return the same value, but not always. + /// The path might be completely calculated, but has not yet been processed by the Seeker. + /// + /// Inside the OnPathComplete callback this method will return true. + /// + /// Version: Before version 4.2.13 this would return false inside the OnPathComplete callback. However, this behaviour was unintuitive. + /// </summary> + public bool IsDone() => path == null || path.PipelineState >= PathState.Returning; + + /// <summary>Called when a path has completed</summary> + void OnPathComplete (Path path) { + OnPathComplete(path, true, true); + } + + /// <summary> + /// Called when a path has completed. + /// Will post process it and return it by calling <see cref="tmpPathCallback"/> and <see cref="pathCallback"/> + /// </summary> + void OnPathComplete (Path p, bool runModifiers, bool sendCallbacks) { + if (p != null && p != path && sendCallbacks) { + return; + } + + if (this == null || p == null || p != path) + return; + + if (!path.error && runModifiers) { + // This will send the path for post processing to modifiers attached to this Seeker + RunModifiers(ModifierPass.PostProcess, path); + } + + if (sendCallbacks) { + p.Claim(this); + +#if UNITY_EDITOR + lastCompletedNodePath = p.path; + lastCompletedVectorPath = p.vectorPath; +#endif + + #pragma warning disable 618 + if (tmpPathCallback == null && pathCallback == null) { +#if UNITY_EDITOR + // This checks for a common error that people make when they upgrade from an older version + // This will be removed in a future version to avoid the slight performance cost. + if (TryGetComponent<IAstarAI>(out var ai)) { + Debug.LogWarning("A path was calculated, but no callback was specified when calling StartPath. If you wanted a movement script to use this path, use <b>ai.SetPath</b> instead of calling StartPath on the Seeker directly. The path will be forwarded to the attached movement script, but this behavior will be removed in the future.", this); + ai.SetPath(p); + } +#endif + } else { + // This will send the path to the callback (if any) specified when calling StartPath + if (tmpPathCallback != null) { + tmpPathCallback(p); + } + + // This will send the path to any script which has registered to the callback + if (pathCallback != null) { + pathCallback(p); + } + } + #pragma warning restore 618 + + // Note: it is important that #prevPath is kept alive (i.e. not pooled) + // if we are drawing gizmos. + // It is also important that #path is kept alive since it can be returned + // from the GetCurrentPath method. + // Since #path will be copied to #prevPath it is sufficient that #prevPath + // is kept alive until it is replaced. + + // Recycle the previous path to reduce the load on the GC + if (prevPath != null) { + prevPath.Release(this, true); + } + + prevPath = p; + } + } + + /// <summary> + /// Called for each path in a MultiTargetPath. + /// Only post processes the path, does not return it. + /// </summary> + void OnPartialPathComplete (Path p) { + OnPathComplete(p, true, false); + } + + /// <summary>Called once for a MultiTargetPath. Only returns the path, does not post process.</summary> + void OnMultiPathComplete (Path p) { + OnPathComplete(p, false, true); + } + + /// <summary> + /// Queue a path to be calculated. + /// Since this method does not take a callback parameter, you should set the <see cref="pathCallback"/> field before calling this method. + /// + /// <code> + /// void Start () { + /// // Get the seeker component attached to this GameObject + /// var seeker = GetComponent<Seeker>(); + /// + /// // Schedule a new path request from the current position to a position 10 units forward. + /// // When the path has been calculated, the OnPathComplete method will be called, unless it was canceled by another path request + /// seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete); + /// + /// // Note that the path is NOT calculated at this point + /// // It has just been queued for calculation + /// } + /// + /// void OnPathComplete (Path path) { + /// // The path is now calculated! + /// + /// if (path.error) { + /// Debug.LogError("Path failed: " + path.errorLog); + /// return; + /// } + /// + /// // Cast the path to the path type we were using + /// var abPath = path as ABPath; + /// + /// // Draw the path in the scene view for 10 seconds + /// for (int i = 0; i < abPath.vectorPath.Count - 1; i++) { + /// Debug.DrawLine(abPath.vectorPath[i], abPath.vectorPath[i+1], Color.red, 10); + /// } + /// } + /// </code> + /// + /// Deprecated: Use <see cref="StartPath(Vector3,Vector3,OnPathDelegate)"/> instead. + /// </summary> + /// <param name="start">The start point of the path</param> + /// <param name="end">The end point of the path</param> + [System.Obsolete("Use the overload that takes a callback instead")] + public Path StartPath (Vector3 start, Vector3 end) { + return StartPath(start, end, null); + } + + /// <summary> + /// Queue a path to be calculated. + /// + /// The callback will be called when the path has been calculated (which may be several frames into the future). + /// Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed) + /// + /// <code> + /// void Start () { + /// // Get the seeker component attached to this GameObject + /// var seeker = GetComponent<Seeker>(); + /// + /// // Schedule a new path request from the current position to a position 10 units forward. + /// // When the path has been calculated, the OnPathComplete method will be called, unless it was canceled by another path request + /// seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete); + /// + /// // Note that the path is NOT calculated at this point + /// // It has just been queued for calculation + /// } + /// + /// void OnPathComplete (Path path) { + /// // The path is now calculated! + /// + /// if (path.error) { + /// Debug.LogError("Path failed: " + path.errorLog); + /// return; + /// } + /// + /// // Cast the path to the path type we were using + /// var abPath = path as ABPath; + /// + /// // Draw the path in the scene view for 10 seconds + /// for (int i = 0; i < abPath.vectorPath.Count - 1; i++) { + /// Debug.DrawLine(abPath.vectorPath[i], abPath.vectorPath[i+1], Color.red, 10); + /// } + /// } + /// </code> + /// </summary> + /// <param name="start">The start point of the path</param> + /// <param name="end">The end point of the path</param> + /// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param> + public Path StartPath (Vector3 start, Vector3 end, OnPathDelegate callback) { + return StartPath(ABPath.Construct(start, end, null), callback); + } + + /// <summary> + /// Queue a path to be calculated. + /// + /// The callback will be called when the path has been calculated (which may be several frames into the future). + /// Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed) + /// + /// <code> + /// // Schedule a path search that will only start searching the graphs with index 0 and 3 + /// seeker.StartPath(startPoint, endPoint, null, 1 << 0 | 1 << 3); + /// </code> + /// </summary> + /// <param name="start">The start point of the path</param> + /// <param name="end">The end point of the path</param> + /// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param> + /// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See #Pathfinding.NNConstraint.graphMask. This will override #graphMask for this path request.</param> + public Path StartPath (Vector3 start, Vector3 end, OnPathDelegate callback, GraphMask graphMask) { + return StartPath(ABPath.Construct(start, end, null), callback, graphMask); + } + + /// <summary> + /// Queue a path to be calculated. + /// + /// Version: Since 4.1.x this method will no longer overwrite the graphMask on the path unless it is explicitly passed as a parameter (see other overloads of this method). + /// + /// This overload takes no callback parameter. Instead, it is expected that you poll the path for completion, or block until it is completed. + /// + /// See: <see cref="IsDone"/> + /// See: <see cref="Path.WaitForPath"/> + /// See: <see cref="Path.BlockUntilCalculated"/> + /// + /// However, <see cref="Path.IsDone"/> should not be used with the Seeker component. This is because while the path itself may be calculated, the Seeker may not have had time to run post processing modifiers on the path yet. + /// </summary> + /// <param name="p">The path to start calculating</param> + public Path StartPath (Path p) { + return StartPath(p, null); + } + + /// <summary> + /// Queue a path to be calculated. + /// + /// The callback will be called when the path has been calculated (which may be several frames into the future). + /// The callback will not be called if a new path request is started before this path request has been calculated. + /// </summary> + /// <param name="p">The path to start calculating</param> + /// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param> + public Path StartPath (Path p, OnPathDelegate callback) { + // Set the graph mask only if the user has not changed it from the default value. + // This is not perfect as the user may have wanted it to be precisely -1 + // however it is the best detection that I can do. + // The non-default check is primarily for compatibility reasons to avoid breaking peoples existing code. + // The StartPath overloads with an explicit graphMask field should be used instead to set the graphMask. + if (p.nnConstraint.graphMask == -1) p.nnConstraint.graphMask = graphMask; + StartPathInternal(p, callback); + return p; + } + + /// <summary> + /// Call this function to start calculating a path. + /// + /// The callback will be called when the path has been calculated (which may be several frames into the future). + /// The callback will not be called if a new path request is started before this path request has been calculated. + /// </summary> + /// <param name="p">The path to start calculating</param> + /// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param> + /// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See #Pathfinding.GraphMask. This will override #graphMask for this path request.</param> + public Path StartPath (Path p, OnPathDelegate callback, GraphMask graphMask) { + p.nnConstraint.graphMask = graphMask; + StartPathInternal(p, callback); + return p; + } + + /// <summary>Internal method to start a path and mark it as the currently active path</summary> + void StartPathInternal (Path p, OnPathDelegate callback) { + var mtp = p as MultiTargetPath; + if (mtp != null) { + // TODO: Allocation, cache + var callbacks = new OnPathDelegate[mtp.targetPoints.Length]; + + for (int i = 0; i < callbacks.Length; i++) { + callbacks[i] = onPartialPathDelegate; + } + + mtp.callbacks = callbacks; + p.callback += OnMultiPathComplete; + } else { + p.callback += onPathDelegate; + } + + p.enabledTags = traversableTags; + p.tagPenalties = tagPenalties; + if (traversalProvider != null) p.traversalProvider = traversalProvider; + + // Cancel a previously requested path is it has not been processed yet and also make sure that it has not been recycled and used somewhere else + if (path != null && path.PipelineState <= PathState.Processing && path.CompleteState != PathCompleteState.Error && lastPathID == path.pathID) { + path.FailWithError("Canceled path because a new one was requested.\n"+ + "This happens when a new path is requested from the seeker when one was already being calculated.\n" + + "For example if a unit got a new order, you might request a new path directly instead of waiting for the now" + + " invalid path to be calculated. Which is probably what you want.\n" + + "If you are getting this a lot, you might want to consider how you are scheduling path requests."); + // No callback will be sent for the canceled path + } + + // Set p as the active path + path = p; + tmpPathCallback = callback; + + // Save the path id so we can make sure that if we cancel a path (see above) it should not have been recycled yet. + lastPathID = path.pathID; + + // Pre process the path + RunModifiers(ModifierPass.PreProcess, path); + + // Send the request to the pathfinder + AstarPath.StartPath(path); + } + + /// <summary> + /// Starts a Multi Target Path from one start point to multiple end points. + /// A Multi Target Path will search for all the end points in one search and will return all paths if pathsForAll is true, or only the shortest one if pathsForAll is false. + /// + /// callback and <see cref="pathCallback"/> will be called when the path has completed. Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed) + /// + /// See: Pathfinding.MultiTargetPath + /// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths" + /// + /// <code> + /// var endPoints = new Vector3[] { + /// transform.position + Vector3.forward * 5, + /// transform.position + Vector3.right * 10, + /// transform.position + Vector3.back * 15 + /// }; + /// // Start a multi target path, where endPoints is a Vector3[] array. + /// // The pathsForAll parameter specifies if a path to every end point should be searched for + /// // or if it should only try to find the shortest path to any end point. + /// var path = seeker.StartMultiTargetPath(transform.position, endPoints, pathsForAll: true, callback: null); + /// path.BlockUntilCalculated(); + /// + /// if (path.error) { + /// Debug.LogError("Error calculating path: " + path.errorLog); + /// return; + /// } + /// + /// Debug.Log("The closest target was index " + path.chosenTarget); + /// + /// // Draw the path to all targets + /// foreach (var subPath in path.vectorPaths) { + /// for (int i = 0; i < subPath.Count - 1; i++) { + /// Debug.DrawLine(subPath[i], subPath[i+1], Color.green, 10); + /// } + /// } + /// + /// // Draw the path to the closest target + /// for (int i = 0; i < path.vectorPath.Count - 1; i++) { + /// Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10); + /// } + /// </code> + /// </summary> + /// <param name="start">The start point of the path</param> + /// <param name="endPoints">The end points of the path</param> + /// <param name="pathsForAll">Indicates whether or not a path to all end points should be searched for or only to the closest one</param> + /// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param> + /// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See Pathfinding.NNConstraint.graphMask.</param> + public MultiTargetPath StartMultiTargetPath (Vector3 start, Vector3[] endPoints, bool pathsForAll, OnPathDelegate callback, int graphMask = -1) { + MultiTargetPath p = MultiTargetPath.Construct(start, endPoints, null, null); + + p.pathsForAll = pathsForAll; + StartPath(p, callback, graphMask); + return p; + } + + /// <summary> + /// Starts a Multi Target Path from multiple start points to a single target point. + /// A Multi Target Path will search from all start points to the target point in one search and will return all paths if pathsForAll is true, or only the shortest one if pathsForAll is false. + /// + /// callback and <see cref="pathCallback"/> will be called when the path has completed. Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed) + /// + /// See: Pathfinding.MultiTargetPath + /// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths" + /// </summary> + /// <param name="startPoints">The start points of the path</param> + /// <param name="end">The end point of the path</param> + /// <param name="pathsForAll">Indicates whether or not a path from all start points should be searched for or only to the closest one</param> + /// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param> + /// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See Pathfinding.NNConstraint.graphMask.</param> + public MultiTargetPath StartMultiTargetPath (Vector3[] startPoints, Vector3 end, bool pathsForAll, OnPathDelegate callback, int graphMask = -1) { + MultiTargetPath p = MultiTargetPath.Construct(startPoints, end, null, null); + + p.pathsForAll = pathsForAll; + StartPath(p, callback, graphMask); + return p; + } + +#if UNITY_EDITOR + /// <summary>Draws gizmos for the Seeker</summary> + public override void DrawGizmos () { + if (lastCompletedNodePath == null || !drawGizmos) { + return; + } + + if (detailedGizmos && lastCompletedNodePath != null) { + using (Draw.WithColor(new Color(0.7F, 0.5F, 0.1F, 0.5F))) { + for (int i = 0; i < lastCompletedNodePath.Count-1; i++) { + Draw.Line((Vector3)lastCompletedNodePath[i].position, (Vector3)lastCompletedNodePath[i+1].position); + } + } + } + + if (lastCompletedVectorPath != null) { + using (Draw.WithColor(new Color(0, 1F, 0, 1F))) { + for (int i = 0; i < lastCompletedVectorPath.Count-1; i++) { + Draw.Line(lastCompletedVectorPath[i], lastCompletedVectorPath[i+1]); + } + } + } + } +#endif + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + if (graphMaskCompatibility != -1) { + graphMask = graphMaskCompatibility; + graphMaskCompatibility = -1; + } + base.OnUpgradeSerializedData(ref migrations, unityThread); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/Seeker.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/Seeker.cs.meta new file mode 100644 index 0000000..30f5e18 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/Seeker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 373b52eb9bf8c40f785bb6947a1aee66 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2c41d544537d6ee4a8ecd487b2ac9724, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs new file mode 100644 index 0000000..2bce7cf --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs @@ -0,0 +1,25 @@ +using UnityEngine; +using System.Collections.Generic; + +namespace Pathfinding.Examples { + /// <summary>Helper script in the example scene 'Turn Based'</summary> + [HelpURL("https://arongranberg.com/astar/documentation/stable/turnbasedai.html")] + public class TurnBasedAI : VersionedMonoBehaviour { + public int movementPoints = 2; + public BlockManager blockManager; + public SingleNodeBlocker blocker; + public GraphNode targetNode; + public BlockManager.TraversalProvider traversalProvider; + + void Start () { + blocker.BlockAtCurrentPosition(); + } + + protected override void Awake () { + base.Awake(); + // Set the traversal provider to block all nodes that are blocked by a SingleNodeBlocker + // except the SingleNodeBlocker owned by this AI (we don't want to be blocked by ourself) + traversalProvider = new BlockManager.TraversalProvider(blockManager, BlockManager.BlockMode.AllExceptSelector, new List<SingleNodeBlocker>() { blocker }); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs.meta new file mode 100644 index 0000000..4fa7210 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 8f95b80c439d6408b9afac9d013922e4 +timeCreated: 1453035991 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarData.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarData.cs new file mode 100644 index 0000000..505f74b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarData.cs @@ -0,0 +1,687 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using Pathfinding.WindowsStore; +using Pathfinding.Serialization; +using Pathfinding.Util; +#if UNITY_WINRT && !UNITY_EDITOR +//using MarkerMetro.Unity.WinLegacy.IO; +//using MarkerMetro.Unity.WinLegacy.Reflection; +#endif + +namespace Pathfinding { + [System.Serializable] + /// <summary> + /// Stores the navigation graphs for the A* Pathfinding System. + /// + /// An instance of this class is assigned to <see cref="AstarPath.data"/>. From it you can access all graphs loaded through the <see cref="graphs"/> variable. + /// This class also handles a lot of the high level serialization. + /// </summary> + public class AstarData { + /// <summary>The AstarPath component which owns this AstarData</summary> + internal AstarPath active; + + #region Fields + /// <summary>Shortcut to the first <see cref="NavMeshGraph"/></summary> + public NavMeshGraph navmesh { get; private set; } + +#if !ASTAR_NO_GRID_GRAPH + /// <summary>Shortcut to the first <see cref="GridGraph"/></summary> + public GridGraph gridGraph { get; private set; } + + /// <summary>Shortcut to the first <see cref="LayerGridGraph"/>.</summary> + public LayerGridGraph layerGridGraph { get; private set; } +#endif + +#if !ASTAR_NO_POINT_GRAPH + /// <summary>Shortcut to the first <see cref="PointGraph"/>.</summary> + public PointGraph pointGraph { get; private set; } +#endif + + /// <summary>Shortcut to the first <see cref="RecastGraph"/>.</summary> + public RecastGraph recastGraph { get; private set; } + + /// <summary>Shortcut to the first <see cref="LinkGraph"/>.</summary> + public LinkGraph linkGraph { get; private set; } + + /// <summary> + /// All supported graph types. + /// Populated through reflection search + /// </summary> + public System.Type[] graphTypes { get; private set; } + +#if ASTAR_FAST_NO_EXCEPTIONS || UNITY_WINRT || UNITY_WEBGL + /// <summary> + /// Graph types to use when building with Fast But No Exceptions for iPhone. + /// If you add any custom graph types, you need to add them to this hard-coded list. + /// </summary> + public static readonly System.Type[] DefaultGraphTypes = new System.Type[] { +#if !ASTAR_NO_GRID_GRAPH + typeof(GridGraph), + typeof(LayerGridGraph), +#endif +#if !ASTAR_NO_POINT_GRAPH + typeof(PointGraph), +#endif + typeof(NavMeshGraph), + typeof(RecastGraph), + typeof(LinkGraph), + }; +#endif + + /// <summary> + /// All graphs. + /// This will be filled only after deserialization has completed. + /// May contain null entries if graph have been removed. + /// </summary> + [System.NonSerialized] + public NavGraph[] graphs = new NavGraph[0]; + + /// <summary> + /// Serialized data for all graphs and settings. + /// Stored as a base64 encoded string because otherwise Unity's Undo system would sometimes corrupt the byte data (because it only stores deltas). + /// + /// This can be accessed as a byte array from the <see cref="data"/> property. + /// </summary> + [SerializeField] + string dataString; + + /// <summary>Serialized data for all graphs and settings</summary> + private byte[] data { + get { + var d = dataString != null? System.Convert.FromBase64String(dataString) : null; + // Unity can initialize the dataString to an empty string, but that's not a valid zip file + if (d != null && d.Length == 0) return null; + return d; + } + set { + dataString = value != null? System.Convert.ToBase64String(value) : null; + } + } + + /// <summary> + /// Serialized data for cached startup. + /// If set, on start the graphs will be deserialized from this file. + /// </summary> + public TextAsset file_cachedStartup; + + /// <summary> + /// Should graph-data be cached. + /// Caching the startup means saving the whole graphs - not only the settings - to a file (<see cref="file_cachedStartup)"/> which can + /// be loaded when the game starts. This is usually much faster than scanning the graphs when the game starts. This is configured from the editor under the "Save & Load" tab. + /// + /// See: save-load-graphs (view in online documentation for working links) + /// </summary> + [SerializeField] + public bool cacheStartup; + + //End Serialization Settings + + List<bool> graphStructureLocked = new List<bool>(); + + #endregion + + internal AstarData (AstarPath active) { + this.active = active; + } + + public byte[] GetData() => data; + + public void SetData (byte[] data) { + this.data = data; + } + + /// <summary>Loads the graphs from memory, will load cached graphs if any exists</summary> + public void OnEnable () { + if (graphTypes == null) { + FindGraphTypes(); + } + + if (graphs == null) graphs = new NavGraph[0]; + + if (cacheStartup && file_cachedStartup != null && Application.isPlaying) { + LoadFromCache(); + } else { + DeserializeGraphs(); + } + } + + /// <summary> + /// Prevent the graph structure from changing during the time this lock is held. + /// This prevents graphs from being added or removed and also prevents graphs from being serialized or deserialized. + /// This is used when e.g an async scan is happening to ensure that for example a graph that is being scanned is not destroyed. + /// + /// Each call to this method *must* be paired with exactly one call to <see cref="UnlockGraphStructure"/>. + /// The calls may be nested. + /// </summary> + internal void LockGraphStructure (bool allowAddingGraphs = false) { + graphStructureLocked.Add(allowAddingGraphs); + } + + /// <summary> + /// Allows the graph structure to change again. + /// See: <see cref="LockGraphStructure"/> + /// </summary> + internal void UnlockGraphStructure () { + if (graphStructureLocked.Count == 0) throw new System.InvalidOperationException(); + graphStructureLocked.RemoveAt(graphStructureLocked.Count - 1); + } + + PathProcessor.GraphUpdateLock AssertSafe (bool onlyAddingGraph = false) { + if (graphStructureLocked.Count > 0) { + bool allowAdding = true; + for (int i = 0; i < graphStructureLocked.Count; i++) allowAdding &= graphStructureLocked[i]; + if (!(onlyAddingGraph && allowAdding)) throw new System.InvalidOperationException("Graphs cannot be added, removed or serialized while the graph structure is locked. This is the case when a graph is currently being scanned and when executing graph updates and work items.\nHowever as a special case, graphs can be added inside work items."); + } + + // Pause the pathfinding threads + var graphLock = active.PausePathfinding(); + if (!active.IsInsideWorkItem) { + // Make sure all graph updates and other callbacks are done + // Only do this if this code is not being called from a work item itself as that would cause a recursive wait that could never complete. + // There are some valid cases when this can happen. For example it may be necessary to add a new graph inside a work item. + active.FlushWorkItems(); + + // Paths that are already calculated and waiting to be returned to the Seeker component need to be + // processed immediately as their results usually depend on graphs that currently exist. If this was + // not done then after destroying a graph one could get a path result with destroyed nodes in it. + active.pathReturnQueue.ReturnPaths(false); + } + return graphLock; + } + + /// <summary> + /// Calls the callback with every node in all graphs. + /// This is the easiest way to iterate through every existing node. + /// + /// <code> + /// AstarPath.active.data.GetNodes(node => { + /// Debug.Log("I found a node at position " + (Vector3)node.position); + /// }); + /// </code> + /// + /// See: <see cref="Pathfinding.NavGraph.GetNodes"/> for getting the nodes of a single graph instead of all. + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public void GetNodes (System.Action<GraphNode> callback) { + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null) graphs[i].GetNodes(callback); + } + } + + /// <summary> + /// Updates shortcuts to the first graph of different types. + /// Hard coding references to some graph types is not really a good thing imo. I want to keep it dynamic and flexible. + /// But these references ease the use of the system, so I decided to keep them. + /// </summary> + public void UpdateShortcuts () { + navmesh = (NavMeshGraph)FindGraphOfType(typeof(NavMeshGraph)); + +#if !ASTAR_NO_GRID_GRAPH + gridGraph = (GridGraph)FindGraphOfType(typeof(GridGraph)); + layerGridGraph = (LayerGridGraph)FindGraphOfType(typeof(LayerGridGraph)); +#endif + +#if !ASTAR_NO_POINT_GRAPH + pointGraph = (PointGraph)FindGraphOfType(typeof(PointGraph)); +#endif + + recastGraph = (RecastGraph)FindGraphOfType(typeof(RecastGraph)); + linkGraph = (LinkGraph)FindGraphOfType(typeof(LinkGraph)); + } + + /// <summary>Load from data from <see cref="file_cachedStartup"/></summary> + public void LoadFromCache () { + var graphLock = AssertSafe(); + + if (file_cachedStartup != null) { + var bytes = file_cachedStartup.bytes; + DeserializeGraphs(bytes); + + GraphModifier.TriggerEvent(GraphModifier.EventType.PostCacheLoad); + } else { + Debug.LogError("Can't load from cache since the cache is empty"); + } + graphLock.Release(); + } + + #region Serialization + + /// <summary> + /// Serializes all graphs settings to a byte array. + /// See: DeserializeGraphs(byte[]) + /// </summary> + public byte[] SerializeGraphs () { + return SerializeGraphs(SerializeSettings.Settings); + } + + /// <summary> + /// Serializes all graphs settings and optionally node data to a byte array. + /// See: DeserializeGraphs(byte[]) + /// See: Pathfinding.Serialization.SerializeSettings + /// </summary> + public byte[] SerializeGraphs (SerializeSettings settings) { + return SerializeGraphs(settings, out var _); + } + + /// <summary> + /// Main serializer function. + /// Serializes all graphs to a byte array + /// A similar function exists in the AstarPathEditor.cs script to save additional info + /// </summary> + public byte[] SerializeGraphs (SerializeSettings settings, out uint checksum) { + var graphLock = AssertSafe(); + var sr = new AstarSerializer(this, settings, active.gameObject); + + sr.OpenSerialize(); + sr.SerializeGraphs(graphs); + sr.SerializeExtraInfo(); + byte[] bytes = sr.CloseSerialize(); + checksum = sr.GetChecksum(); +#if ASTARDEBUG + Debug.Log("Got a whole bunch of data, "+bytes.Length+" bytes"); +#endif + graphLock.Release(); + return bytes; + } + + /// <summary>Deserializes graphs from <see cref="data"/></summary> + public void DeserializeGraphs () { + var dataBytes = data; + if (dataBytes != null) { + DeserializeGraphs(dataBytes); + } + } + + /// <summary> + /// Destroys all graphs and sets <see cref="graphs"/> to null. + /// See: <see cref="RemoveGraph"/> + /// </summary> + public void ClearGraphs () { + var graphLock = AssertSafe(); + + ClearGraphsInternal(); + graphLock.Release(); + } + + void ClearGraphsInternal () { + if (graphs == null) return; + var graphLock = AssertSafe(); + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null) { + active.DirtyBounds(graphs[i].bounds); + ((IGraphInternals)graphs[i]).OnDestroy(); + graphs[i].active = null; + } + } + graphs = new NavGraph[0]; + UpdateShortcuts(); + graphLock.Release(); + } + + public void DisposeUnmanagedData () { + if (graphs == null) return; + var graphLock = AssertSafe(); + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null) { + ((IGraphInternals)graphs[i]).DisposeUnmanagedData(); + } + } + graphLock.Release(); + } + + /// <summary>Makes all graphs become unscanned</summary> + internal void DestroyAllNodes () { + if (graphs == null) return; + var graphLock = AssertSafe(); + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null) { + ((IGraphInternals)graphs[i]).DestroyAllNodes(); + } + } + graphLock.Release(); + } + + public void OnDestroy () { + ClearGraphsInternal(); + } + + /// <summary> + /// Deserializes graphs from the specified byte array. + /// An error will be logged if deserialization fails. + /// </summary> + public void DeserializeGraphs (byte[] bytes) { + var graphLock = AssertSafe(); + + ClearGraphs(); + DeserializeGraphsAdditive(bytes); + graphLock.Release(); + } + + /// <summary> + /// Deserializes graphs from the specified byte array additively. + /// An error will be logged if deserialization fails. + /// This function will add loaded graphs to the current ones. + /// </summary> + public void DeserializeGraphsAdditive (byte[] bytes) { + var graphLock = AssertSafe(); + + try { + if (bytes != null) { + var sr = new AstarSerializer(this, active.gameObject); + + if (sr.OpenDeserialize(bytes)) { + DeserializeGraphsPartAdditive(sr); + sr.CloseDeserialize(); + } else { + Debug.Log("Invalid data file (cannot read zip).\nThe data is either corrupt or it was saved using a 3.0.x or earlier version of the system"); + } + } else { + throw new System.ArgumentNullException(nameof(bytes)); + } + active.VerifyIntegrity(); + } catch (System.Exception e) { + Debug.LogError(new System.Exception("Caught exception while deserializing data.", e)); + graphs = new NavGraph[0]; + } + + UpdateShortcuts(); + GraphModifier.TriggerEvent(GraphModifier.EventType.PostGraphLoad); + graphLock.Release(); + } + + /// <summary>Helper function for deserializing graphs</summary> + void DeserializeGraphsPartAdditive (AstarSerializer sr) { + if (graphs == null) graphs = new NavGraph[0]; + + var gr = new List<NavGraph>(graphs); + + // Set an offset so that the deserializer will load + // the graphs with the correct graph indexes + sr.SetGraphIndexOffset(gr.Count); + + FindGraphTypes(); + var newGraphs = sr.DeserializeGraphs(graphTypes); + gr.AddRange(newGraphs); + + if (gr.Count > GraphNode.MaxGraphIndex + 1) { + throw new System.InvalidOperationException("Graph Count Limit Reached. You cannot have more than " + GraphNode.MaxGraphIndex + " graphs."); + } + + graphs = gr.ToArray(); + + // Assign correct graph indices. + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] == null) continue; + graphs[i].GetNodes(node => node.GraphIndex = (uint)i); + } + + for (int i = 0; i < graphs.Length; i++) { + for (int j = i+1; j < graphs.Length; j++) { + if (graphs[i] != null && graphs[j] != null && graphs[i].guid == graphs[j].guid) { + Debug.LogWarning("Guid Conflict when importing graphs additively. Imported graph will get a new Guid.\nThis message is (relatively) harmless."); + graphs[i].guid = Pathfinding.Util.Guid.NewGuid(); + break; + } + } + } + + sr.PostDeserialization(); + + // This will refresh off-mesh links, + // and also recalculate the hierarchical graph if necessary + active.AddWorkItem(ctx => { + for (int i = 0; i < newGraphs.Length; i++) { + ctx.DirtyBounds(newGraphs[i].bounds); + } + }); + active.FlushWorkItems(); + } + + #endregion + + /// <summary> + /// Find all graph types supported in this build. + /// Using reflection, the assembly is searched for types which inherit from NavGraph. + /// </summary> + public void FindGraphTypes () { + if (graphTypes != null) return; + +#if !ASTAR_FAST_NO_EXCEPTIONS && !UNITY_WINRT && !UNITY_WEBGL + var graphList = new List<System.Type>(); + foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { + System.Type[] types = null; + try { + types = assembly.GetTypes(); + } catch { + // Ignore type load exceptions and things like that. + // We might not be able to read all assemblies for some reason, but hopefully the relevant types exist in the assemblies that we can read + continue; + } + + foreach (var type in types) { +#if NETFX_CORE && !UNITY_EDITOR + System.Type baseType = type.GetTypeInfo().BaseType; +#else + var baseType = type.BaseType; +#endif + while (baseType != null) { + if (System.Type.Equals(baseType, typeof(NavGraph))) { + graphList.Add(type); + + break; + } + +#if NETFX_CORE && !UNITY_EDITOR + baseType = baseType.GetTypeInfo().BaseType; +#else + baseType = baseType.BaseType; +#endif + } + } + } + + graphTypes = graphList.ToArray(); + +#if ASTARDEBUG + Debug.Log("Found "+graphTypes.Length+" graph types"); +#endif +#else + graphTypes = DefaultGraphTypes; +#endif + } + + #region GraphCreation + + /// <summary>Creates a new graph instance of type type</summary> + internal NavGraph CreateGraph (System.Type type) { + var graph = System.Activator.CreateInstance(type) as NavGraph; + + graph.active = active; + return graph; + } + + /// <summary> + /// Adds a graph of type T to the <see cref="graphs"/> array. + /// See: runtime-graphs (view in online documentation for working links) + /// </summary> + public T AddGraph<T> () where T : NavGraph => AddGraph(typeof(T)) as T; + + /// <summary> + /// Adds a graph of type type to the <see cref="graphs"/> array. + /// See: runtime-graphs (view in online documentation for working links) + /// </summary> + public NavGraph AddGraph (System.Type type) { + NavGraph graph = null; + + for (int i = 0; i < graphTypes.Length; i++) { + if (System.Type.Equals(graphTypes[i], type)) { + graph = CreateGraph(graphTypes[i]); + } + } + + if (graph == null) { + Debug.LogError("No NavGraph of type '"+type+"' could be found, "+graphTypes.Length+" graph types are avaliable"); + return null; + } + + AddGraph(graph); + + return graph; + } + + /// <summary>Adds the specified graph to the <see cref="graphs"/> array</summary> + void AddGraph (NavGraph graph) { + // Make sure to not interfere with pathfinding + var graphLock = AssertSafe(true); + + // Try to fill in an empty position + bool foundEmpty = false; + + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] == null) { + graphs[i] = graph; + graph.graphIndex = (uint)i; + foundEmpty = true; + break; + } + } + + if (!foundEmpty) { + if (graphs != null && graphs.Length >= GraphNode.MaxGraphIndex) { + throw new System.Exception("Graph Count Limit Reached. You cannot have more than " + GraphNode.MaxGraphIndex + " graphs."); + } + + // Add a new entry to the list + Memory.Realloc(ref graphs, graphs.Length + 1); + graphs[graphs.Length - 1] = graph; + graph.graphIndex = (uint)(graphs.Length-1); + } + + UpdateShortcuts(); + graph.active = active; + graphLock.Release(); + } + + /// <summary> + /// Removes the specified graph from the <see cref="graphs"/> array and Destroys it in a safe manner. + /// To avoid changing graph indices for the other graphs, the graph is simply nulled in the array instead + /// of actually removing it from the array. + /// The empty position will be reused if a new graph is added. + /// + /// Returns: True if the graph was sucessfully removed (i.e it did exist in the <see cref="graphs"/> array). False otherwise. + /// + /// See: <see cref="ClearGraphs"/> + /// </summary> + public bool RemoveGraph (NavGraph graph) { + // Make sure the pathfinding threads are paused + var graphLock = AssertSafe(); + + active.DirtyBounds(graph.bounds); + ((IGraphInternals)graph).OnDestroy(); + graph.active = null; + + int i = System.Array.IndexOf(graphs, graph); + if (i != -1) graphs[i] = null; + + UpdateShortcuts(); + active.offMeshLinks.Refresh(); + graphLock.Release(); + return i != -1; + } + + #endregion + + #region GraphUtility + + /// <summary> + /// Returns the graph which contains the specified node. + /// The graph must be in the <see cref="graphs"/> array. + /// + /// Returns: Returns the graph which contains the node. Null if the graph wasn't found + /// </summary> + public static NavGraph GetGraph (GraphNode node) { + if (node == null || node.Destroyed) return null; + + AstarPath script = AstarPath.active; + if (System.Object.ReferenceEquals(script, null)) return null; + + AstarData data = script.data; + if (data == null || data.graphs == null) return null; + + uint graphIndex = node.GraphIndex; + return data.graphs[(int)graphIndex]; + } + + /// <summary>Returns the first graph which satisfies the predicate. Returns null if no graph was found.</summary> + public NavGraph FindGraph (System.Func<NavGraph, bool> predicate) { + if (graphs != null) { + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null && predicate(graphs[i])) { + return graphs[i]; + } + } + } + return null; + } + + /// <summary>Returns the first graph of type type found in the <see cref="graphs"/> array. Returns null if no graph was found.</summary> + public NavGraph FindGraphOfType (System.Type type) { + return FindGraph(graph => System.Type.Equals(graph.GetType(), type)); + } + + /// <summary>Returns the first graph which inherits from the type type. Returns null if no graph was found.</summary> + public NavGraph FindGraphWhichInheritsFrom (System.Type type) { + return FindGraph(graph => WindowsStoreCompatibility.GetTypeInfo(type).IsAssignableFrom(WindowsStoreCompatibility.GetTypeInfo(graph.GetType()))); + } + + /// <summary> + /// Loop through this function to get all graphs of type 'type' + /// <code> + /// foreach (GridGraph graph in AstarPath.data.FindGraphsOfType (typeof(GridGraph))) { + /// //Do something with the graph + /// } + /// </code> + /// See: AstarPath.RegisterSafeNodeUpdate + /// </summary> + public IEnumerable FindGraphsOfType (System.Type type) { + if (graphs == null) yield break; + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null && System.Type.Equals(graphs[i].GetType(), type)) { + yield return graphs[i]; + } + } + } + + /// <summary> + /// All graphs which implements the UpdateableGraph interface + /// <code> foreach (IUpdatableGraph graph in AstarPath.data.GetUpdateableGraphs ()) { + /// //Do something with the graph + /// } </code> + /// See: AstarPath.AddWorkItem + /// See: Pathfinding.IUpdatableGraph + /// </summary> + public IEnumerable GetUpdateableGraphs () { + if (graphs == null) yield break; + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] is IUpdatableGraph) { + yield return graphs[i]; + } + } + } + + /// <summary>Gets the index of the NavGraph in the <see cref="graphs"/> array</summary> + public int GetGraphIndex (NavGraph graph) { + if (graph == null) throw new System.ArgumentNullException("graph"); + + var index = -1; + if (graphs != null) { + index = System.Array.IndexOf(graphs, graph); + if (index == -1) Debug.LogError("Graph doesn't exist"); + } + return index; + } + + #endregion + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarData.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarData.cs.meta new file mode 100644 index 0000000..eb5a46f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarData.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 38d211caa07cb44ef886481aa1cf755c +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarMath.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarMath.cs new file mode 100644 index 0000000..696037f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarMath.cs @@ -0,0 +1,1954 @@ +using UnityEngine; +using System.Collections.Generic; +using System; + +namespace Pathfinding { + using Pathfinding.Util; + using Unity.Mathematics; + using Unity.Burst; + using Pathfinding.Graphs.Navmesh; + + /// <summary>Contains various spline functions.</summary> + public static class AstarSplines { + public static Vector3 CatmullRom (Vector3 previous, Vector3 start, Vector3 end, Vector3 next, float elapsedTime) { + // References used: + // p.266 GemsV1 + // + // tension is often set to 0.5 but you can use any reasonable value: + // http://www.cs.cmu.edu/~462/projects/assn2/assn2/catmullRom.pdf + // + // bias and tension controls: + // http://local.wasp.uwa.edu.au/~pbourke/miscellaneous/interpolation/ + + float percentComplete = elapsedTime; + float percentCompleteSquared = percentComplete * percentComplete; + float percentCompleteCubed = percentCompleteSquared * percentComplete; + + return + previous * (-0.5F*percentCompleteCubed + + percentCompleteSquared - + 0.5F*percentComplete) + + + start * + (1.5F*percentCompleteCubed + + -2.5F*percentCompleteSquared + 1.0F) + + + end * + (-1.5F*percentCompleteCubed + + 2.0F*percentCompleteSquared + + 0.5F*percentComplete) + + + next * + (0.5F*percentCompleteCubed - + 0.5F*percentCompleteSquared); + } + + /// <summary>Returns a point on a cubic bezier curve. t is clamped between 0 and 1</summary> + public static Vector3 CubicBezier (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { + t = Mathf.Clamp01(t); + float t2 = 1-t; + return t2*t2*t2 * p0 + 3 * t2*t2 * t * p1 + 3 * t2 * t*t * p2 + t*t*t * p3; + } + + /// <summary>Returns the derivative for a point on a cubic bezier curve. t is clamped between 0 and 1</summary> + public static Vector3 CubicBezierDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { + t = Mathf.Clamp01(t); + float t2 = 1-t; + return 3*t2*t2*(p1-p0) + 6*t2*t*(p2 - p1) + 3*t*t*(p3 - p2); + } + + /// <summary>Returns the second derivative for a point on a cubic bezier curve. t is clamped between 0 and 1</summary> + public static Vector3 CubicBezierSecondDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { + t = Mathf.Clamp01(t); + float t2 = 1-t; + return 6*t2*(p2 - 2*p1 + p0) + 6*t*(p3 - 2*p2 + p1); + } + } + + /// <summary> + /// Various vector math utility functions. + /// Version: A lot of functions in the Polygon class have been moved to this class + /// the names have changed slightly and everything now consistently assumes a left handed + /// coordinate system now instead of sometimes using a left handed one and sometimes + /// using a right handed one. This is why the 'Left' methods in the Polygon class redirect + /// to methods named 'Right'. The functionality is exactly the same. + /// + /// Note the difference between segments and lines. Lines are infinitely + /// long but segments have only a finite length. + /// </summary> + public static class VectorMath { + /// <summary> + /// Complex number multiplication. + /// Returns: a * b + /// + /// Used to rotate vectors in an efficient way. + /// + /// See: https://en.wikipedia.org/wiki/Complex_number<see cref="Multiplication_and_division"/> + /// </summary> + public static Vector2 ComplexMultiply (Vector2 a, Vector2 b) { + return new Vector2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); + } + + /// <summary> + /// Complex number multiplication. + /// Returns: a * b + /// + /// Used to rotate vectors in an efficient way. + /// + /// See: https://en.wikipedia.org/wiki/Complex_number<see cref="Multiplication_and_division"/> + /// </summary> + public static float2 ComplexMultiply (float2 a, float2 b) { + return a.x*b + a.y*new float2(-b.y, b.x); + } + + /// <summary> + /// Complex number multiplication. + /// Returns: a * conjugate(b) + /// + /// Used to rotate vectors in an efficient way. + /// + /// See: https://en.wikipedia.org/wiki/Complex_number<see cref="Multiplication_and_division"/> + /// See: https://en.wikipedia.org/wiki/Complex_conjugate + /// </summary> + public static float2 ComplexMultiplyConjugate (float2 a, float2 b) { + return new float2(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y); + } + + /// <summary> + /// Complex number multiplication. + /// Returns: a * conjugate(b) + /// + /// Used to rotate vectors in an efficient way. + /// + /// See: https://en.wikipedia.org/wiki/Complex_number<see cref="Multiplication_and_division"/> + /// See: https://en.wikipedia.org/wiki/Complex_conjugate + /// </summary> + public static Vector2 ComplexMultiplyConjugate (Vector2 a, Vector2 b) { + return new Vector2(a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y); + } + + /// <summary> + /// Returns the closest point on the line. + /// The line is treated as infinite. + /// See: ClosestPointOnSegment + /// See: ClosestPointOnLineFactor + /// </summary> + public static Vector3 ClosestPointOnLine (Vector3 lineStart, Vector3 lineEnd, Vector3 point) { + Vector3 lineDirection = Vector3.Normalize(lineEnd - lineStart); + float dot = Vector3.Dot(point - lineStart, lineDirection); + + return lineStart + (dot*lineDirection); + } + + /// <summary> + /// Factor along the line which is closest to the point. + /// Returned value is in the range [0,1] if the point lies on the segment otherwise it just lies on the line. + /// The closest point can be calculated using (end-start)*factor + start. + /// + /// See: ClosestPointOnLine + /// See: ClosestPointOnSegment + /// </summary> + public static float ClosestPointOnLineFactor (Vector3 lineStart, Vector3 lineEnd, Vector3 point) { + var dir = lineEnd - lineStart; + float sqrMagn = dir.sqrMagnitude; + + if (sqrMagn <= 0.000001f) return 0; + + return Vector3.Dot(point - lineStart, dir) / sqrMagn; + } + + /// <summary> + /// Factor along the line which is closest to the point. + /// Returned value is in the range [0,1] if the point lies on the segment otherwise it just lies on the line. + /// The closest point can be calculated using (end-start)*factor + start + /// </summary> + public static float ClosestPointOnLineFactor (float3 lineStart, float3 lineEnd, float3 point) { + var lineDirection = lineEnd - lineStart; + var sqrMagn = math.dot(lineDirection, lineDirection); + return math.select(0, math.dot(point - lineStart, lineDirection) / sqrMagn, sqrMagn > 0.000001f); + } + + /// <summary> + /// Factor along the line which is closest to the point. + /// Returned value is in the range [0,1] if the point lies on the segment otherwise it just lies on the line. + /// The closest point can be calculated using (end-start)*factor + start + /// </summary> + public static float ClosestPointOnLineFactor (Int3 lineStart, Int3 lineEnd, Int3 point) { + var lineDirection = lineEnd - lineStart; + float magn = lineDirection.sqrMagnitude; + + float closestPoint = (float)Int3.DotLong(point - lineStart, lineDirection); + + if (magn != 0) closestPoint /= magn; + + return closestPoint; + } + + /// <summary> + /// Factor of the nearest point on the segment. + /// Returned value is in the range [0,1] if the point lies on the segment otherwise it just lies on the line. + /// The closest point can be calculated using (end-start)*factor + start; + /// </summary> + public static float ClosestPointOnLineFactor (Int2 lineStart, Int2 lineEnd, Int2 point) { + var lineDirection = lineEnd - lineStart; + double magn = lineDirection.sqrMagnitudeLong; + + double closestPoint = Int2.DotLong(point - lineStart, lineDirection); + + if (magn != 0) closestPoint /= magn; + + return (float)closestPoint; + } + + /// <summary> + /// Returns the closest point on the segment. + /// The segment is NOT treated as infinite. + /// See: ClosestPointOnLine + /// See: ClosestPointOnSegmentXZ + /// </summary> + public static Vector3 ClosestPointOnSegment (Vector3 lineStart, Vector3 lineEnd, Vector3 point) { + var dir = lineEnd - lineStart; + float sqrMagn = dir.sqrMagnitude; + + if (sqrMagn <= 0.000001) return lineStart; + + float factor = Vector3.Dot(point - lineStart, dir) / sqrMagn; + return lineStart + Mathf.Clamp01(factor)*dir; + } + + /// <summary> + /// Returns the closest point on the segment in the XZ plane. + /// The y coordinate of the result will be the same as the y coordinate of the point parameter. + /// + /// The segment is NOT treated as infinite. + /// See: ClosestPointOnSegment + /// See: ClosestPointOnLine + /// </summary> + public static Vector3 ClosestPointOnSegmentXZ (Vector3 lineStart, Vector3 lineEnd, Vector3 point) { + lineStart.y = point.y; + lineEnd.y = point.y; + Vector3 fullDirection = lineEnd-lineStart; + Vector3 fullDirection2 = fullDirection; + fullDirection2.y = 0; + float magn = fullDirection2.magnitude; + Vector3 lineDirection = magn > float.Epsilon ? fullDirection2/magn : Vector3.zero; + + float closestPoint = Vector3.Dot((point-lineStart), lineDirection); + return lineStart+(Mathf.Clamp(closestPoint, 0.0f, fullDirection2.magnitude)*lineDirection); + } + + /// <summary> + /// Returns the approximate shortest squared distance between x,z and the segment p-q. + /// The segment is not considered infinite. + /// This function is not entirely exact, but it is about twice as fast as DistancePointSegment2. + /// TODO: Is this actually approximate? It looks exact. + /// </summary> + public static float SqrDistancePointSegmentApproximate (int x, int z, int px, int pz, int qx, int qz) { + float pqx = (float)(qx - px); + float pqz = (float)(qz - pz); + float dx = (float)(x - px); + float dz = (float)(z - pz); + float d = pqx*pqx + pqz*pqz; + float t = pqx*dx + pqz*dz; + + if (d > 0) + t /= d; + if (t < 0) + t = 0; + else if (t > 1) + t = 1; + + dx = px + t*pqx - x; + dz = pz + t*pqz - z; + + return dx*dx + dz*dz; + } + + /// <summary> + /// Returns the approximate shortest squared distance between x,z and the segment p-q. + /// The segment is not considered infinite. + /// This function is not entirely exact, but it is about twice as fast as DistancePointSegment2. + /// TODO: Is this actually approximate? It looks exact. + /// </summary> + public static float SqrDistancePointSegmentApproximate (Int3 a, Int3 b, Int3 p) { + float pqx = (float)(b.x - a.x); + float pqz = (float)(b.z - a.z); + float dx = (float)(p.x - a.x); + float dz = (float)(p.z - a.z); + float d = pqx*pqx + pqz*pqz; + float t = pqx*dx + pqz*dz; + + if (d > 0) + t /= d; + if (t < 0) + t = 0; + else if (t > 1) + t = 1; + + dx = a.x + t*pqx - p.x; + dz = a.z + t*pqz - p.z; + + return dx*dx + dz*dz; + } + + /// <summary> + /// Returns the squared distance between p and the segment a-b. + /// The line is not considered infinite. + /// </summary> + public static float SqrDistancePointSegment (Vector3 a, Vector3 b, Vector3 p) { + var nearest = ClosestPointOnSegment(a, b, p); + + return (nearest-p).sqrMagnitude; + } + + /// <summary> + /// 3D minimum distance between 2 segments. + /// Input: two 3D line segments S1 and S2 + /// Returns: the shortest squared distance between S1 and S2 + /// </summary> + public static float SqrDistanceSegmentSegment (Vector3 s1, Vector3 e1, Vector3 s2, Vector3 e2) { + Vector3 dir1 = e1 - s1; + Vector3 dir2 = e2 - s2; + Vector3 startOffset = s1 - s2; + double dir1sq = Vector3.Dot(dir1, dir1); // always >= 0 + double b = Vector3.Dot(dir1, dir2); + double dir2sq = Vector3.Dot(dir2, dir2); // always >= 0 + double d = Vector3.Dot(dir1, startOffset); + double e = Vector3.Dot(dir2, startOffset); + double D = dir1sq*dir2sq - b*b; // always >= 0 + double sc, sN, sD = D; // sc = sN / sD, default sD = D >= 0 + double tc, tN, tD = D; // tc = tN / tD, default tD = D >= 0 + + // compute the line parameters of the two closest points + // D is approximately |dir1|^2|dir2|^2*(1-cos^2 alpha), where alpha is the angle between the lines + if (D < 0.000001 * dir1sq*dir2sq) { // the lines are almost parallel + sN = 0.0f; // force using point P0 on segment S1 + sD = 1.0f; // to prevent possible division by 0.0 later + tN = e; + tD = dir2sq; + } else { // get the closest points on the infinite lines + sN = (b*e - dir2sq*d); + tN = (dir1sq*e - b*d); + if (sN < 0.0) { // sc < 0 => the s=0 edge is visible + sN = 0.0; + tN = e; + tD = dir2sq; + } else if (sN > sD) { // sc > 1 => the s=1 edge is visible + sN = sD; + tN = e + b; + tD = dir2sq; + } + } + + if (tN < 0.0) { // tc < 0 => the t=0 edge is visible + tN = 0.0; + // recompute sc for this edge + if (-d < 0.0f) + sN = 0.0f; + else if (-d > dir1sq) + sN = sD; + else { + sN = -d; + sD = dir1sq; + } + } else if (tN > tD) { // tc > 1 => the t=1 edge is visible + tN = tD; + // recompute sc for this edge + if ((-d + b) < 0.0f) + sN = 0; + else if ((-d + b) > dir1sq) + sN = sD; + else { + sN = (-d + b); + sD = dir1sq; + } + } + + // finally do the division to get sc and tc + sc = (Math.Abs(sN) < 0.00001f ? 0.0 : sN / sD); + tc = (Math.Abs(tN) < 0.00001f ? 0.0 : tN / tD); + + // get the difference of the two closest points + Vector3 dP = startOffset + ((float)sc * dir1) - ((float)tc * dir2); // = S1(sc) - S2(tc) + + return dP.sqrMagnitude; // return the closest distance + } + + /// <summary> + /// Determinant of the 2x2 matrix [c1, c2]. + /// + /// This is useful for many things, like calculating distances between lines and points. + /// + /// Equivalent to Cross(new float3(c1, 0), new float 3(c2, 0)).z + /// </summary> + public static float Determinant (float2 c1, float2 c2) { + return c1.x*c2.y - c1.y*c2.x; + } + + /// <summary>Squared distance between two points in the XZ plane</summary> + public static float SqrDistanceXZ (Vector3 a, Vector3 b) { + var delta = a-b; + + return delta.x*delta.x+delta.z*delta.z; + } + + /// <summary> + /// Signed area of a triangle in the XZ plane multiplied by 2. + /// This will be negative for clockwise triangles and positive for counter-clockwise ones + /// </summary> + public static long SignedTriangleAreaTimes2XZ (Int3 a, Int3 b, Int3 c) { + return (long)(b.x - a.x) * (long)(c.z - a.z) - (long)(c.x - a.x) * (long)(b.z - a.z); + } + + /// <summary> + /// Signed area of a triangle in the XZ plane multiplied by 2. + /// This will be negative for clockwise triangles and positive for counter-clockwise ones. + /// </summary> + public static float SignedTriangleAreaTimes2XZ (Vector3 a, Vector3 b, Vector3 c) { + return (b.x - a.x) * (c.z - a.z) - (c.x - a.x) * (b.z - a.z); + } + + /// <summary> + /// Returns if p lies on the right side of the line a - b. + /// Uses XZ space. Does not return true if the points are colinear. + /// </summary> + public static bool RightXZ (Vector3 a, Vector3 b, Vector3 p) { + return (b.x - a.x) * (p.z - a.z) - (p.x - a.x) * (b.z - a.z) < -float.Epsilon; + } + + /// <summary> + /// Returns if p lies on the right side of the line a - b. + /// Uses XZ space. Does not return true if the points are colinear. + /// </summary> + public static bool RightXZ (Int3 a, Int3 b, Int3 p) { + return (long)(b.x - a.x) * (long)(p.z - a.z) - (long)(p.x - a.x) * (long)(b.z - a.z) < 0; + } + + /// <summary> + /// Returns which side of the line a - b that p lies on. + /// Uses XZ space. + /// </summary> + public static Side SideXZ (Int3 a, Int3 b, Int3 p) { + var s = (long)(b.x - a.x) * (long)(p.z - a.z) - (long)(p.x - a.x) * (long)(b.z - a.z); + + return s > 0 ? Side.Left : (s < 0 ? Side.Right : Side.Colinear); + } + + /// <summary> + /// Returns if p lies on the right side of the line a - b. + /// Also returns true if the points are colinear. + /// </summary> + public static bool RightOrColinear (Vector2 a, Vector2 b, Vector2 p) { + return (b.x - a.x) * (p.y - a.y) - (p.x - a.x) * (b.y - a.y) <= 0; + } + + /// <summary> + /// Returns if p lies on the right side of the line a - b. + /// Also returns true if the points are colinear. + /// </summary> + public static bool RightOrColinear (Int2 a, Int2 b, Int2 p) { + return (long)(b.x - a.x) * (long)(p.y - a.y) - (long)(p.x - a.x) * (long)(b.y - a.y) <= 0; + } + + /// <summary> + /// Returns if p lies on the left side of the line a - b. + /// Uses XZ space. Also returns true if the points are colinear. + /// </summary> + public static bool RightOrColinearXZ (Vector3 a, Vector3 b, Vector3 p) { + return (b.x - a.x) * (p.z - a.z) - (p.x - a.x) * (b.z - a.z) <= 0; + } + + /// <summary> + /// Returns if p lies on the left side of the line a - b. + /// Uses XZ space. Also returns true if the points are colinear. + /// </summary> + public static bool RightOrColinearXZ (Int3 a, Int3 b, Int3 p) { + return (long)(b.x - a.x) * (long)(p.z - a.z) - (long)(p.x - a.x) * (long)(b.z - a.z) <= 0; + } + + /// <summary> + /// Returns if the points a in a clockwise order. + /// Will return true even if the points are colinear or very slightly counter-clockwise + /// (if the signed area of the triangle formed by the points has an area less than or equals to float.Epsilon) + /// </summary> + public static bool IsClockwiseMarginXZ (Vector3 a, Vector3 b, Vector3 c) { + return (b.x-a.x)*(c.z-a.z)-(c.x-a.x)*(b.z-a.z) <= float.Epsilon; + } + + /// <summary>Returns if the points a in a clockwise order</summary> + public static bool IsClockwiseXZ (Vector3 a, Vector3 b, Vector3 c) { + return (b.x-a.x)*(c.z-a.z)-(c.x-a.x)*(b.z-a.z) < 0; + } + + /// <summary>Returns if the points a in a clockwise order</summary> + public static bool IsClockwiseXZ (Int3 a, Int3 b, Int3 c) { + return RightXZ(a, b, c); + } + + /// <summary>Returns true if the points a in a clockwise order or if they are colinear</summary> + public static bool IsClockwiseOrColinearXZ (Int3 a, Int3 b, Int3 c) { + return RightOrColinearXZ(a, b, c); + } + + /// <summary>Returns true if the points a in a clockwise order or if they are colinear</summary> + public static bool IsClockwiseOrColinear (Int2 a, Int2 b, Int2 c) { + return RightOrColinear(a, b, c); + } + + /// <summary>Returns if the points are colinear (lie on a straight line)</summary> + public static bool IsColinear (Vector3 a, Vector3 b, Vector3 c) { + var lhs = b - a; + var rhs = c - a; + // Take the cross product of lhs and rhs + // The magnitude of the cross product will be zero if the points a,b,c are colinear + float x = lhs.y * rhs.z - lhs.z * rhs.y; + float y = lhs.z * rhs.x - lhs.x * rhs.z; + float z = lhs.x * rhs.y - lhs.y * rhs.x; + float v = x*x + y*y + z*z; + float lengthsq = lhs.sqrMagnitude * rhs.sqrMagnitude; + + // Epsilon not chosen with much thought, just that float.Epsilon was a bit too small. + return v <= math.sqrt(lengthsq) * 0.0001f || lengthsq == 0.0f; + } + + /// <summary>Returns if the points are colinear (lie on a straight line)</summary> + public static bool IsColinear (Vector2 a, Vector2 b, Vector2 c) { + float v = (b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y); + + // Epsilon not chosen with much thought, just that float.Epsilon was a bit too small. + return v <= 0.0001f && v >= -0.0001f; + } + + /// <summary>Returns if the points are colinear (lie on a straight line)</summary> + public static bool IsColinearXZ (Int3 a, Int3 b, Int3 c) { + return (long)(b.x - a.x) * (long)(c.z - a.z) - (long)(c.x - a.x) * (long)(b.z - a.z) == 0; + } + + /// <summary>Returns if the points are colinear (lie on a straight line)</summary> + public static bool IsColinearXZ (Vector3 a, Vector3 b, Vector3 c) { + float v = (b.x-a.x)*(c.z-a.z)-(c.x-a.x)*(b.z-a.z); + + // Epsilon not chosen with much thought, just that float.Epsilon was a bit too small. + return v <= 0.0000001f && v >= -0.0000001f; + } + + /// <summary>Returns if the points are colinear (lie on a straight line)</summary> + public static bool IsColinearAlmostXZ (Int3 a, Int3 b, Int3 c) { + long v = (long)(b.x - a.x) * (long)(c.z - a.z) - (long)(c.x - a.x) * (long)(b.z - a.z); + + return v > -1 && v < 1; + } + + /// <summary> + /// Returns if the line segment start2 - end2 intersects the line segment start1 - end1. + /// If only the endpoints coincide, the result is undefined (may be true or false). + /// </summary> + public static bool SegmentsIntersect (Int2 start1, Int2 end1, Int2 start2, Int2 end2) { + return RightOrColinear(start1, end1, start2) != RightOrColinear(start1, end1, end2) && RightOrColinear(start2, end2, start1) != RightOrColinear(start2, end2, end1); + } + + /// <summary> + /// Returns if the line segment start2 - end2 intersects the line segment start1 - end1. + /// If only the endpoints coincide, the result is undefined (may be true or false). + /// + /// Note: XZ space + /// </summary> + public static bool SegmentsIntersectXZ (Int3 start1, Int3 end1, Int3 start2, Int3 end2) { + return RightOrColinearXZ(start1, end1, start2) != RightOrColinearXZ(start1, end1, end2) && RightOrColinearXZ(start2, end2, start1) != RightOrColinearXZ(start2, end2, end1); + } + + /// <summary> + /// Returns if the two line segments intersects. The lines are NOT treated as infinite (just for clarification) + /// See: IntersectionPoint + /// </summary> + public static bool SegmentsIntersectXZ (Vector3 start1, Vector3 end1, Vector3 start2, Vector3 end2) { + Vector3 dir1 = end1-start1; + Vector3 dir2 = end2-start2; + + float den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + return false; + } + + float nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + float nom2 = dir1.x*(start1.z-start2.z) - dir1.z * (start1.x - start2.x); + float u = nom/den; + float u2 = nom2/den; + + if (u < 0F || u > 1F || u2 < 0F || u2 > 1F) { + return false; + } + + return true; + } + + /// <summary> + /// Calculates the intersection points between a "capsule" (segment expanded by a radius), and a line. + /// + /// Returns: (t1, t2), the intersection points on the form lineStart + lineDir*t. Where t2 >= t1. If t2 < t1 then there are no intersections. + /// </summary> + /// <param name="capsuleStart">Center of the capsule's first circle</param> + /// <param name="capsuleDir">Main axis of the capsule. Must be normalized.</param> + /// <param name="capsuleLength">Distance betwen the capsule's circle centers.</param> + /// <param name="lineStart">A point on the line</param> + /// <param name="lineDir">The (normalized) direction of the line.</param> + /// <param name="radius">The radius of the circle.</param> + public static float2 CapsuleLineIntersectionFactors (float2 capsuleStart, float2 capsuleDir, float capsuleLength, float2 lineStart, float2 lineDir, float radius) { + var cosAlpha = math.dot(capsuleDir, lineDir); + var sinAlpha = math.sqrt(1.0f - cosAlpha*cosAlpha); + var tmin = float.PositiveInfinity; + var tmax = float.NegativeInfinity; + + if (LineCircleIntersectionFactors(lineStart - capsuleStart, lineDir, radius, out float t11, out float t12)) { + tmin = math.min(tmin, t11); + tmax = math.max(tmax, t12); + } + if (LineCircleIntersectionFactors(lineStart - (capsuleStart + capsuleDir*capsuleLength), lineDir, radius, out float t21, out float t22)) { + tmin = math.min(tmin, t21); + tmax = math.max(tmax, t22); + } + + if (LineLineIntersectionFactor(capsuleStart, capsuleDir, lineStart, lineDir, out float ucenter)) { + var normal = new float2(-capsuleDir.y, capsuleDir.x); + var offset = radius * cosAlpha / sinAlpha; + var side = math.sign(capsuleDir.y*lineDir.x - capsuleDir.x*lineDir.y); + var ustraight1 = ucenter + offset*side; + var ustraight2 = ucenter - offset*side; + if (ustraight1 >= 0 && ustraight1 <= capsuleLength) { + var p = capsuleStart + capsuleDir * ustraight1 - normal * radius; + var tstraight1 = math.dot(p - lineStart, lineDir); + tmin = math.min(tmin, tstraight1); + tmax = math.max(tmax, tstraight1); + } + if (ustraight2 >= 0 && ustraight2 <= capsuleLength) { + var p = capsuleStart + capsuleDir * ustraight2 + normal * radius; + var tstraight2 = math.dot(p - lineStart, lineDir); + tmin = math.min(tmin, tstraight2); + tmax = math.max(tmax, tstraight2); + } + } else { + // Parallel, or almost parallel. + // In this case we can just rely on the circle intersection checks. + } + + return new float2(tmin, tmax); + } + + /// <summary> + /// Calculates the point start1 + dir1*t where the two infinite lines intersect. + /// Returns false if the lines are close to parallel. + /// </summary> + public static bool LineLineIntersectionFactor (float2 start1, float2 dir1, float2 start2, float2 dir2, out float t) { + float den = dir2.y*dir1.x - dir2.x * dir1.y; + + if (math.abs(den) < 0.0001f) { + t = 0; + return false; + } + + float nom = dir2.x*(start1.y-start2.y) - dir2.y*(start1.x-start2.x); + t = nom/den; + return true; + } + + /// <summary> + /// Calculates the point start1 + dir1*factor1 == start2 + dir2*factor2 where the two infinite lines intersect. + /// Returns false if the lines are close to parallel. + /// </summary> + public static bool LineLineIntersectionFactors (float2 start1, float2 dir1, float2 start2, float2 dir2, out float factor1, out float factor2) { + float den = dir2.y*dir1.x - dir2.x * dir1.y; + + if (math.abs(den) < 0.0001f) { + factor1 = factor2 = 0; + return false; + } + + float nom1 = dir2.x*(start1.y-start2.y) - dir2.y*(start1.x-start2.x); + float nom2 = dir1.x*(start1.y-start2.y) - dir1.y*(start1.x - start2.x); + factor1 = nom1/den; + factor2 = nom2/den; + return true; + } + + /// <summary> + /// Intersection point between two infinite lines. + /// Note that start points and directions are taken as parameters instead of start and end points. + /// Lines are treated as infinite. If the lines are parallel 'start1' will be returned. + /// Intersections are calculated on the XZ plane. + /// + /// See: LineIntersectionPointXZ + /// </summary> + public static Vector3 LineDirIntersectionPointXZ (Vector3 start1, Vector3 dir1, Vector3 start2, Vector3 dir2) { + float den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + return start1; + } + + float nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + float u = nom/den; + + return start1 + dir1*u; + } + + /// <summary> + /// Intersection point between two infinite lines. + /// Note that start points and directions are taken as parameters instead of start and end points. + /// Lines are treated as infinite. If the lines are parallel 'start1' will be returned. + /// Intersections are calculated on the XZ plane. + /// + /// See: LineIntersectionPointXZ + /// </summary> + public static Vector3 LineDirIntersectionPointXZ (Vector3 start1, Vector3 dir1, Vector3 start2, Vector3 dir2, out bool intersects) { + float den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + intersects = false; + return start1; + } + + float nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + float u = nom/den; + + intersects = true; + return start1 + dir1*u; + } + + /// <summary> + /// Returns if the ray (start1, end1) intersects the segment (start2, end2). + /// false is returned if the lines are parallel. + /// Only the XZ coordinates are used. + /// TODO: Double check that this actually works + /// </summary> + public static bool RaySegmentIntersectXZ (Int3 start1, Int3 end1, Int3 start2, Int3 end2) { + Int3 dir1 = end1-start1; + Int3 dir2 = end2-start2; + + long den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + return false; + } + + long nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + long nom2 = dir1.x*(start1.z-start2.z) - dir1.z * (start1.x - start2.x); + + //factor1 < 0 + // If both have the same sign, then nom/den < 0 and thus the segment cuts the ray before the ray starts + if (!(nom < 0 ^ den < 0)) { + return false; + } + + //factor2 < 0 + if (!(nom2 < 0 ^ den < 0)) { + return false; + } + + if ((den >= 0 && nom2 > den) || (den < 0 && nom2 <= den)) { + return false; + } + + return true; + } + + /// <summary> + /// Returns the intersection factors for line 1 and line 2. The intersection factors is a distance along the line start - end where the other line intersects it. + /// <code> intersectionPoint = start1 + factor1 * (end1-start1) </code> + /// <code> intersectionPoint2 = start2 + factor2 * (end2-start2) </code> + /// Lines are treated as infinite. + /// false is returned if the lines are parallel and true if they are not. + /// Only the XZ coordinates are used. + /// </summary> + public static bool LineIntersectionFactorXZ (Int3 start1, Int3 end1, Int3 start2, Int3 end2, out float factor1, out float factor2) { + Int3 dir1 = end1-start1; + Int3 dir2 = end2-start2; + + long den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + factor1 = 0; + factor2 = 0; + return false; + } + + long nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + long nom2 = dir1.x*(start1.z-start2.z) - dir1.z * (start1.x - start2.x); + + factor1 = (float)nom/den; + factor2 = (float)nom2/den; + + return true; + } + + /// <summary> + /// Returns the intersection factors for line 1 and line 2. The intersection factors is a distance along the line start - end where the other line intersects it. + /// <code> intersectionPoint = start1 + factor1 * (end1-start1) </code> + /// <code> intersectionPoint2 = start2 + factor2 * (end2-start2) </code> + /// Lines are treated as infinite. + /// false is returned if the lines are parallel and true if they are not. + /// Only the XZ coordinates are used. + /// </summary> + public static bool LineIntersectionFactorXZ (Vector3 start1, Vector3 end1, Vector3 start2, Vector3 end2, out float factor1, out float factor2) { + Vector3 dir1 = end1-start1; + Vector3 dir2 = end2-start2; + + float den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den <= 0.00001f && den >= -0.00001f) { + factor1 = 0; + factor2 = 0; + return false; + } + + float nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + float nom2 = dir1.x*(start1.z-start2.z) - dir1.z * (start1.x - start2.x); + + float u = nom/den; + float u2 = nom2/den; + + factor1 = u; + factor2 = u2; + + return true; + } + + /// <summary> + /// Returns the intersection factor for line 1 with ray 2. + /// The intersection factors is a factor distance along the line start - end where the other line intersects it. + /// <code> intersectionPoint = start1 + factor * (end1-start1) </code> + /// Lines are treated as infinite. + /// + /// The second "line" is treated as a ray, meaning only matches on start2 or forwards towards end2 (and beyond) will be returned + /// If the point lies on the wrong side of the ray start, Nan will be returned. + /// + /// NaN is returned if the lines are parallel. + /// </summary> + public static float LineRayIntersectionFactorXZ (Int3 start1, Int3 end1, Int3 start2, Int3 end2) { + Int3 dir1 = end1-start1; + Int3 dir2 = end2-start2; + + int den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + return float.NaN; + } + + int nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + int nom2 = dir1.x*(start1.z-start2.z) - dir1.z * (start1.x - start2.x); + + if ((float)nom2/den < 0) { + return float.NaN; + } + return (float)nom/den; + } + + /// <summary> + /// Returns the intersection factor for line 1 with line 2. + /// The intersection factor is a distance along the line start1 - end1 where the line start2 - end2 intersects it. + /// <code> intersectionPoint = start1 + intersectionFactor * (end1-start1) </code>. + /// Lines are treated as infinite. + /// -1 is returned if the lines are parallel (note that this is a valid return value if they are not parallel too) + /// </summary> + public static float LineIntersectionFactorXZ (Vector3 start1, Vector3 end1, Vector3 start2, Vector3 end2) { + Vector3 dir1 = end1-start1; + Vector3 dir2 = end2-start2; + + float den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + return -1; + } + + float nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + float u = nom/den; + + return u; + } + + /// <summary>Returns the intersection point between the two lines. Lines are treated as infinite. start1 is returned if the lines are parallel</summary> + public static Vector3 LineIntersectionPointXZ (Vector3 start1, Vector3 end1, Vector3 start2, Vector3 end2) { + bool s; + + return LineIntersectionPointXZ(start1, end1, start2, end2, out s); + } + + /// <summary>Returns the intersection point between the two lines. Lines are treated as infinite. start1 is returned if the lines are parallel</summary> + public static Vector3 LineIntersectionPointXZ (Vector3 start1, Vector3 end1, Vector3 start2, Vector3 end2, out bool intersects) { + Vector3 dir1 = end1-start1; + Vector3 dir2 = end2-start2; + + float den = dir2.z*dir1.x - dir2.x * dir1.z; + + if (den == 0) { + intersects = false; + return start1; + } + + float nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + + float u = nom/den; + + intersects = true; + return start1 + dir1*u; + } + + /// <summary>Returns the intersection point between the two lines. Lines are treated as infinite. start1 is returned if the lines are parallel</summary> + public static Vector2 LineIntersectionPoint (Vector2 start1, Vector2 end1, Vector2 start2, Vector2 end2) { + bool s; + + return LineIntersectionPoint(start1, end1, start2, end2, out s); + } + + /// <summary>Returns the intersection point between the two lines. Lines are treated as infinite. start1 is returned if the lines are parallel</summary> + public static Vector2 LineIntersectionPoint (Vector2 start1, Vector2 end1, Vector2 start2, Vector2 end2, out bool intersects) { + Vector2 dir1 = end1-start1; + Vector2 dir2 = end2-start2; + + float den = dir2.y*dir1.x - dir2.x * dir1.y; + + if (den == 0) { + intersects = false; + return start1; + } + + float nom = dir2.x*(start1.y-start2.y)- dir2.y*(start1.x-start2.x); + + float u = nom/den; + + intersects = true; + return start1 + dir1*u; + } + + /// <summary> + /// Returns the intersection point between the two line segments in XZ space. + /// Lines are NOT treated as infinite. start1 is returned if the line segments do not intersect + /// The point will be returned along the line [start1, end1] (this matters only for the y coordinate). + /// </summary> + public static Vector3 SegmentIntersectionPointXZ (Vector3 start1, Vector3 end1, Vector3 start2, Vector3 end2, out bool intersects) { + Vector3 dir1 = end1-start1; + Vector3 dir2 = end2-start2; + + float den = dir2.z * dir1.x - dir2.x * dir1.z; + + if (den == 0) { + intersects = false; + return start1; + } + + float nom = dir2.x*(start1.z-start2.z)- dir2.z*(start1.x-start2.x); + float nom2 = dir1.x*(start1.z-start2.z) - dir1.z*(start1.x-start2.x); + float u = nom/den; + float u2 = nom2/den; + + if (u < 0F || u > 1F || u2 < 0F || u2 > 1F) { + intersects = false; + return start1; + } + + intersects = true; + return start1 + dir1*u; + } + + /// <summary> + /// Does the line segment intersect the bounding box. + /// The line is NOT treated as infinite. + /// \author Slightly modified code from http://www.3dkingdoms.com/weekly/weekly.php?a=21 + /// </summary> + public static bool SegmentIntersectsBounds (Bounds bounds, Vector3 a, Vector3 b) { + // Put segment in box space + a -= bounds.center; + b -= bounds.center; + + // Get line midpoint and extent + var LMid = (a + b) * 0.5F; + var L = (a - LMid); + var LExt = new Vector3(Math.Abs(L.x), Math.Abs(L.y), Math.Abs(L.z)); + + Vector3 extent = bounds.extents; + + // Use Separating Axis Test + // Separation vector from box center to segment center is LMid, since the line is in box space + if (Math.Abs(LMid.x) > extent.x + LExt.x) return false; + if (Math.Abs(LMid.y) > extent.y + LExt.y) return false; + if (Math.Abs(LMid.z) > extent.z + LExt.z) return false; + // Crossproducts of line and each axis + if (Math.Abs(LMid.y * L.z - LMid.z * L.y) > (extent.y * LExt.z + extent.z * LExt.y)) return false; + if (Math.Abs(LMid.x * L.z - LMid.z * L.x) > (extent.x * LExt.z + extent.z * LExt.x)) return false; + if (Math.Abs(LMid.x * L.y - LMid.y * L.x) > (extent.x * LExt.y + extent.y * LExt.x)) return false; + // No separating axis, the line intersects + return true; + } + + /// <summary> + /// Calculates the two intersection points (point + direction*t) on the line where it intersects with a circle at the origin. + /// + /// t1 will always be less than or equal to t2 if there are intersections. + /// + /// Returns false if there are no intersections. + /// </summary> + /// <param name="point">A point on the line</param> + /// <param name="direction">The normalized direction of the line</param> + /// <param name="radius">The radius of the circle at the origin.</param> + /// <param name="t1">The first intersection (if any).</param> + /// <param name="t2">The second intersection (if any).</param> + public static bool LineCircleIntersectionFactors (float2 point, float2 direction, float radius, out float t1, out float t2) { + // Distance from the closest point on the line (from the origin) to line.point + float dot = math.dot(point, direction); + // Squared distance from the origin to the closest point on the line + float distanceToLine = math.lengthsq(point) - dot*dot; + // Calculate the intersection of the line with the circle. + // This is the squared length of half the chord that intersects the circle. + float discriminant = radius*radius - distanceToLine; + + if (discriminant < 0.0f) { + // The line is completely outside the circle + t1 = float.PositiveInfinity; + t2 = float.NegativeInfinity; + return false; + } + + var sqrtDiscriminant = math.sqrt(discriminant); + t1 = -dot - sqrtDiscriminant; + t2 = -dot + sqrtDiscriminant; + return true; + } + + /// <summary> + /// Calculates the two intersection points (lerp(point1, point2, t)) on the segment where it intersects with a circle at the origin. + /// + /// t1 will always be less than or equal to t2 if there are intersections. + /// + /// Returns false if there are no intersections. + /// </summary> + /// <param name="point1">Start of the segment</param> + /// <param name="point2">End of the segment</param> + /// <param name="radiusSq">The squared radius of the circle at the origin.</param> + /// <param name="t1">The first intersection (if any). Between 0 and 1.</param> + /// <param name="t2">The second intersection (if any). Between 0 and 1.</param> + public static bool SegmentCircleIntersectionFactors (float2 point1, float2 point2, float radiusSq, out float t1, out float t2) { + // Distance from the closest point on the line (from the origin) to line.point + var dir = point2 - point1; + var dirSq = math.lengthsq(dir); + float dot = math.dot(point1, dir) / dirSq; + // Proportional to the squared distance from the origin to the closest point on the line + float distanceToLine = math.lengthsq(point1) / dirSq - dot*dot; + float discriminant = radiusSq/dirSq - distanceToLine; + + if (discriminant < 0.0f) { + // The line is completely outside the circle + t1 = float.PositiveInfinity; + t2 = float.NegativeInfinity; + return false; + } + + var sqrtDiscriminant = math.sqrt(discriminant); + t1 = -dot - sqrtDiscriminant; + t2 = -dot + sqrtDiscriminant; + t1 = math.max(0, t1); + t2 = math.min(1, t2); + + if (t1 >= 1 || t2 <= 0) return false; + return true; + } + + /// <summary> + /// Intersection of a line and a circle. + /// Returns the greatest t such that segmentStart+t*(segmentEnd-segmentStart) lies on the circle. + /// + /// In case the line does not intersect with the circle, the closest point on the line + /// to the circle will be returned. + /// + /// Note: Works for line and sphere in 3D space as well. + /// + /// See: http://mathworld.wolfram.com/Circle-LineIntersection.html + /// See: https://en.wikipedia.org/wiki/Intersection_(Euclidean_geometry)<see cref="A_line_and_a_circle"/> + /// </summary> + public static float LineCircleIntersectionFactor (Vector3 circleCenter, Vector3 linePoint1, Vector3 linePoint2, float radius) { + float segmentLength; + var normalizedDirection = Normalize(linePoint2 - linePoint1, out segmentLength); + var dirToStart = linePoint1 - circleCenter; + + var dot = Vector3.Dot(dirToStart, normalizedDirection); + var discriminant = dot * dot - (dirToStart.sqrMagnitude - radius*radius); + + if (discriminant < 0) { + // No intersection, pick closest point on segment + discriminant = 0; + } + + var t = -dot + Mathf.Sqrt(discriminant); + // Note: the default value of 1 is important for the PathInterpolator.MoveToCircleIntersection2D + // method to work properly. Maybe find some better abstraction where this default value is more obvious. + return segmentLength > 0.00001f ? t / segmentLength : 1f; + } + + /// <summary> + /// True if the matrix will reverse orientations of faces. + /// + /// Scaling by a negative value along an odd number of axes will reverse + /// the orientation of e.g faces on a mesh. This must be counter adjusted + /// by for example the recast rasterization system to be able to handle + /// meshes with negative scales properly. + /// + /// We can find out if they are flipped by finding out how the signed + /// volume of a unit cube is transformed when applying the matrix + /// + /// If the (signed) volume turns out to be negative + /// that also means that the orientation of it has been reversed. + /// + /// See: https://en.wikipedia.org/wiki/Normal_(geometry) + /// See: https://en.wikipedia.org/wiki/Parallelepiped + /// </summary> + public static bool ReversesFaceOrientations (Matrix4x4 matrix) { + var dX = matrix.MultiplyVector(new Vector3(1, 0, 0)); + var dY = matrix.MultiplyVector(new Vector3(0, 1, 0)); + var dZ = matrix.MultiplyVector(new Vector3(0, 0, 1)); + + // Calculate the signed volume of the parallelepiped + var volume = Vector3.Dot(Vector3.Cross(dX, dY), dZ); + + return volume < 0; + } + + /// <summary> + /// Normalize vector and also return the magnitude. + /// This is more efficient than calculating the magnitude and normalizing separately + /// </summary> + public static Vector3 Normalize (Vector3 v, out float magnitude) { + magnitude = v.magnitude; + // This is the same constant that Unity uses + if (magnitude > 1E-05f) { + return v / magnitude; + } else { + return Vector3.zero; + } + } + + /// <summary> + /// Normalize vector and also return the magnitude. + /// This is more efficient than calculating the magnitude and normalizing separately + /// </summary> + public static Vector2 Normalize (Vector2 v, out float magnitude) { + magnitude = v.magnitude; + // This is the same constant that Unity uses + if (magnitude > 1E-05f) { + return v / magnitude; + } else { + return Vector2.zero; + } + } + + /* Clamp magnitude along the X and Z axes. + * The y component will not be changed. + */ + public static Vector3 ClampMagnitudeXZ (Vector3 v, float maxMagnitude) { + float squaredMagnitudeXZ = v.x*v.x + v.z*v.z; + + if (squaredMagnitudeXZ > maxMagnitude*maxMagnitude && maxMagnitude > 0) { + var factor = maxMagnitude / Mathf.Sqrt(squaredMagnitudeXZ); + v.x *= factor; + v.z *= factor; + } + return v; + } + + /* Magnitude in the XZ plane */ + public static float MagnitudeXZ (Vector3 v) { + return Mathf.Sqrt(v.x*v.x + v.z*v.z); + } + + /// <summary> + /// Number of radians that this quaternion rotates around its axis of rotation. + /// Will be in the range [-PI, PI]. + /// + /// Note: A quaternion of q and -q represent the same rotation, but their axis of rotation point in opposite directions, so the angle will be different. + /// </summary> + public static float QuaternionAngle (quaternion rot) { + return 2 * math.atan2(math.length(rot.value.xyz), rot.value.w); + } + } + + /// <summary> + /// Utility functions for working with numbers and strings. + /// + /// See: Polygon + /// See: VectorMath + /// </summary> + public static class AstarMath { + static Unity.Mathematics.Random GlobalRandom = Unity.Mathematics.Random.CreateFromIndex(0); + static object GlobalRandomLock = new object(); + + public static float ThreadSafeRandomFloat () { + lock (GlobalRandomLock) { + return GlobalRandom.NextFloat(); + } + } + + public static float2 ThreadSafeRandomFloat2 () { + lock (GlobalRandomLock) { + return GlobalRandom.NextFloat2(); + } + } + + /// <summary>Converts a non-negative float to a long, saturating at long.MaxValue if the value is too large</summary> + public static long SaturatingConvertFloatToLong(float v) => v > (float)long.MaxValue ? long.MaxValue : (long)v; + + /// <summary>Maps a value between startMin and startMax to be between targetMin and targetMax</summary> + public static float MapTo (float startMin, float startMax, float targetMin, float targetMax, float value) { + return Mathf.Lerp(targetMin, targetMax, Mathf.InverseLerp(startMin, startMax, value)); + } + + /// <summary> + /// Returns bit number b from int a. The bit number is zero based. Relevant b values are from 0 to 31. + /// Equals to (a >> b) & 1 + /// </summary> + static int Bit (int a, int b) { + return (a >> b) & 1; + } + + /// <summary> + /// Returns a nice color from int i with alpha a. Got code from the open-source Recast project, works really well. + /// Seems like there are only 64 possible colors from studying the code + /// </summary> + public static Color IntToColor (int i, float a) { + int r = Bit(i, 2) + Bit(i, 3) * 2 + 1; + int g = Bit(i, 1) + Bit(i, 4) * 2 + 1; + int b = Bit(i, 0) + Bit(i, 5) * 2 + 1; + + return new Color(r*0.25F, g*0.25F, b*0.25F, a); + } + + /// <summary> + /// Converts an HSV color to an RGB color. + /// According to the algorithm described at http://en.wikipedia.org/wiki/HSL_and_HSV + /// + /// @author Wikipedia + /// @return the RGB representation of the color. + /// </summary> + public static Color HSVToRGB (float h, float s, float v) { + float r = 0, g = 0, b = 0; + + float Chroma = s * v; + float Hdash = h / 60.0f; + float X = Chroma * (1.0f - System.Math.Abs((Hdash % 2.0f) - 1.0f)); + + if (Hdash < 1.0f) { + r = Chroma; + g = X; + } else if (Hdash < 2.0f) { + r = X; + g = Chroma; + } else if (Hdash < 3.0f) { + g = Chroma; + b = X; + } else if (Hdash < 4.0f) { + g = X; + b = Chroma; + } else if (Hdash < 5.0f) { + r = X; + b = Chroma; + } else if (Hdash < 6.0f) { + r = Chroma; + b = X; + } + + float Min = v - Chroma; + + r += Min; + g += Min; + b += Min; + + return new Color(r, g, b); + } + + /// <summary> + /// Calculates the shortest difference between two given angles given in radians. + /// + /// The return value will be between -pi/2 and +pi/2. + /// </summary> + public static float DeltaAngle (float angle1, float angle2) { + float diff = (angle2 - angle1 + math.PI) % (2*math.PI) - math.PI; + return math.select(diff, diff + 2*math.PI, diff < -math.PI); + } + } + + /// <summary> + /// Utility functions for working with polygons, lines, and other vector math. + /// All functions which accepts Vector3s but work in 2D space uses the XZ space if nothing else is said. + /// + /// Version: A lot of functions in this class have been moved to the VectorMath class + /// the names have changed slightly and everything now consistently assumes a left handed + /// coordinate system now instead of sometimes using a left handed one and sometimes + /// using a right handed one. This is why the 'Left' methods redirect to methods + /// named 'Right'. The functionality is exactly the same. + /// </summary> + [BurstCompile] + public static class Polygon { + /// <summary> + /// Returns if the triangle ABC contains the point p in XZ space. + /// The triangle vertices are assumed to be laid out in clockwise order. + /// </summary> + public static bool ContainsPointXZ (Vector3 a, Vector3 b, Vector3 c, Vector3 p) { + return VectorMath.IsClockwiseMarginXZ(a, b, p) && VectorMath.IsClockwiseMarginXZ(b, c, p) && VectorMath.IsClockwiseMarginXZ(c, a, p); + } + + /// <summary> + /// Returns if the triangle ABC contains the point p. + /// The triangle vertices are assumed to be laid out in clockwise order. + /// </summary> + public static bool ContainsPointXZ (Int3 a, Int3 b, Int3 c, Int3 p) { + return VectorMath.IsClockwiseOrColinearXZ(a, b, p) && VectorMath.IsClockwiseOrColinearXZ(b, c, p) && VectorMath.IsClockwiseOrColinearXZ(c, a, p); + } + + /// <summary> + /// Returns if the triangle ABC contains the point p. + /// The triangle vertices are assumed to be laid out in clockwise order. + /// </summary> + public static bool ContainsPoint (Int2 a, Int2 b, Int2 c, Int2 p) { + return VectorMath.IsClockwiseOrColinear(a, b, p) && VectorMath.IsClockwiseOrColinear(b, c, p) && VectorMath.IsClockwiseOrColinear(c, a, p); + } + + /// <summary> + /// Checks if p is inside the polygon. + /// \author http://unifycommunity.com/wiki/index.php?title=PolyContainsPoint (Eric5h5) + /// </summary> + public static bool ContainsPoint (Vector2[] polyPoints, Vector2 p) { + int j = polyPoints.Length-1; + bool inside = false; + + for (int i = 0; i < polyPoints.Length; j = i++) { + if (((polyPoints[i].y <= p.y && p.y < polyPoints[j].y) || (polyPoints[j].y <= p.y && p.y < polyPoints[i].y)) && + (p.x < (polyPoints[j].x - polyPoints[i].x) * (p.y - polyPoints[i].y) / (polyPoints[j].y - polyPoints[i].y) + polyPoints[i].x)) + inside = !inside; + } + return inside; + } + + /// <summary> + /// Checks if p is inside the polygon (XZ space). + /// \author http://unifycommunity.com/wiki/index.php?title=PolyContainsPoint (Eric5h5) + /// </summary> + public static bool ContainsPointXZ (Vector3[] polyPoints, Vector3 p) { + int j = polyPoints.Length-1; + bool inside = false; + + for (int i = 0; i < polyPoints.Length; j = i++) { + if (((polyPoints[i].z <= p.z && p.z < polyPoints[j].z) || (polyPoints[j].z <= p.z && p.z < polyPoints[i].z)) && + (p.x < (polyPoints[j].x - polyPoints[i].x) * (p.z - polyPoints[i].z) / (polyPoints[j].z - polyPoints[i].z) + polyPoints[i].x)) + inside = !inside; + } + return inside; + } + + /// <summary> + /// Returns if the triangle contains the point p when projected on the movement plane. + /// The triangle vertices may be clockwise or counter-clockwise. + /// + /// This method is numerically robust, as in, if the point is contained in exactly one of two adjacent triangles, then this + /// function will return true for at least one of them (both if the point is exactly on the edge between them). + /// If it was less numerically robust, it could conceivably return false for both of them if the point was on the edge between them, which would be bad. + /// </summary> + [BurstCompile] + public static bool ContainsPoint (ref int3 aWorld, ref int3 bWorld, ref int3 cWorld, ref int3 pWorld, ref NativeMovementPlane movementPlane) { + // Extract the coordinate axes of the movement plane + var m = new float3x3(movementPlane.rotation.value); + var m2D = math.transpose(new float3x2(m.c0, m.c2)); + return ContainsPoint(ref aWorld, ref bWorld, ref cWorld, ref pWorld, in m2D); + } + + /// <summary> + /// Returns if the triangle contains the point p when projected on a plane using the given projection. + /// The triangle vertices may be clockwise or counter-clockwise. + /// + /// This method is numerically robust, as in, if the point is contained in exactly one of two adjacent triangles, then this + /// function will return true for at least one of them (both if the point is exactly on the edge between them). + /// If it was less numerically robust, it could conceivably return false for both of them if the point was on the edge between them, which would be bad. + /// </summary> + public static bool ContainsPoint (ref int3 aWorld, ref int3 bWorld, ref int3 cWorld, ref int3 pWorld, in float2x3 planeProjection) { + const int QUANTIZATION = 1024; + var m = new int2x3(planeProjection * QUANTIZATION); + // Project all the points onto the movement plane using SIMD + var xs = new int4(aWorld.x, bWorld.x, cWorld.x, pWorld.x); + var ys = new int4(aWorld.y, bWorld.y, cWorld.y, pWorld.y); + var zs = new int4(aWorld.z, bWorld.z, cWorld.z, pWorld.z); + // Subtract the first point from all the other points + // This ensures that large coordinates will not overflow due to using 32 bits here. + // Since we multiply all coordinates by QUANTIZATION, and Int3 coordinates are already multiplied by 1000, + // coordinates would otherwise be liable to start overflowing at unity world coordinates above around 2000. + // TODO: We could still get bad results if pWorld is very far away from the triangle (about 4000 units). + xs -= xs.x; + ys -= ys.x; + zs -= zs.x; + // Projected X and Y coordinates + var px = (xs * m.c0.x + ys * m.c1.x + zs * m.c2.x) / QUANTIZATION; + var py = (xs * m.c0.y + ys * m.c1.y + zs * m.c2.y) / QUANTIZATION; + + // Do 3 cross products to check if the point is inside the triangle + var v1 = px.yzx - px.xyz; + var v2 = py.www - py.xyz; + var v3 = px.www - px.xyz; + var v4 = py.yzx - py.xyz; + long check1 = (long)v1.x * (long)v2.x - (long)v3.x * (long)v4.x; + long check2 = (long)v1.y * (long)v2.y - (long)v3.y * (long)v4.y; + long check3 = (long)v1.z * (long)v2.z - (long)v3.z * (long)v4.z; + // Allow for both clockwise and counter-clockwise triangle layouts. + // This can be important sometimes on spherical worlds where the "upside-down" triangles + // will be seen as having the reverse winding order when projected onto a plane. + // We take care to include points right on the edge of the triangle. + return (check1 >= 0 & check2 >= 0 & check3 >= 0) | (check1 <= 0 & check2 <= 0 & check3 <= 0); + + // Note: It might be tempting to try to use SIMD-like code for this. But the following requires a lot more instructions, as it turns out. + // return math.all(new bool3(check1 >= 0, check2 >= 0, check3 >= 0)) || math.all(new bool3(check1 <= 0, check2 <= 0, check3 <= 0)); + } + + /// <summary> + /// Sample Y coordinate of the triangle (p1, p2, p3) at the point p in XZ space. + /// The y coordinate of p is ignored. + /// + /// Returns: The interpolated y coordinate unless the triangle is degenerate in which case a DivisionByZeroException will be thrown + /// + /// See: https://en.wikipedia.org/wiki/Barycentric_coordinate_system + /// </summary> + public static int SampleYCoordinateInTriangle (Int3 p1, Int3 p2, Int3 p3, Int3 p) { + double det = ((double)(p2.z - p3.z)) * (p1.x - p3.x) + ((double)(p3.x - p2.x)) * (p1.z - p3.z); + + double lambda1 = ((((double)(p2.z - p3.z)) * (p.x - p3.x) + ((double)(p3.x - p2.x)) * (p.z - p3.z)) / det); + double lambda2 = ((((double)(p3.z - p1.z)) * (p.x - p3.x) + ((double)(p1.x - p3.x)) * (p.z - p3.z)) / det); + + return (int)Math.Round(lambda1 * p1.y + lambda2 * p2.y + (1 - lambda1 - lambda2) * p3.y); + } + + /// <summary> + /// Calculates convex hull in XZ space for the points. + /// Implemented using the very simple Gift Wrapping Algorithm + /// which has a complexity of O(nh) where n is the number of points and h is the number of points on the hull, + /// so it is in the worst case quadratic. + /// </summary> + public static Vector3[] ConvexHullXZ (Vector3[] points) { + if (points.Length == 0) return new Vector3[0]; + + var hull = Pathfinding.Util.ListPool<Vector3>.Claim(); + + int pointOnHull = 0; + for (int i = 1; i < points.Length; i++) if (points[i].x < points[pointOnHull].x) pointOnHull = i; + + int startpoint = pointOnHull; + int counter = 0; + + do { + hull.Add(points[pointOnHull]); + int endpoint = 0; + for (int i = 0; i < points.Length; i++) if (endpoint == pointOnHull || !VectorMath.RightOrColinearXZ(points[pointOnHull], points[endpoint], points[i])) endpoint = i; + + pointOnHull = endpoint; + + counter++; + if (counter > 10000) { + Debug.LogWarning("Infinite Loop in Convex Hull Calculation"); + break; + } + } while (pointOnHull != startpoint); + + var result = hull.ToArray(); + + // Return to pool + Pathfinding.Util.ListPool<Vector3>.Release(hull); + return result; + } + + /// <summary> + /// Closest point on the triangle abc to the point p. + /// See: 'Real Time Collision Detection' by Christer Ericson, chapter 5.1, page 141 + /// </summary> + public static Vector2 ClosestPointOnTriangle (Vector2 a, Vector2 b, Vector2 c, Vector2 p) { + // Check if p is in vertex region outside A + var ab = b - a; + var ac = c - a; + var ap = p - a; + + var d1 = Vector2.Dot(ab, ap); + var d2 = Vector2.Dot(ac, ap); + + // Barycentric coordinates (1,0,0) + if (d1 <= 0 && d2 <= 0) { + return a; + } + + // Check if p is in vertex region outside B + var bp = p - b; + var d3 = Vector2.Dot(ab, bp); + var d4 = Vector2.Dot(ac, bp); + + // Barycentric coordinates (0,1,0) + if (d3 >= 0 && d4 <= d3) { + return b; + } + + // Check if p is in edge region outside AB, if so return a projection of p onto AB + if (d1 >= 0 && d3 <= 0) { + var vc = d1 * d4 - d3 * d2; + if (vc <= 0) { + // Barycentric coordinates (1-v, v, 0) + var v = d1 / (d1 - d3); + return a + ab*v; + } + } + + // Check if p is in vertex region outside C + var cp = p - c; + var d5 = Vector2.Dot(ab, cp); + var d6 = Vector2.Dot(ac, cp); + + // Barycentric coordinates (0,0,1) + if (d6 >= 0 && d5 <= d6) { + return c; + } + + // Check if p is in edge region of AC, if so return a projection of p onto AC + if (d2 >= 0 && d6 <= 0) { + var vb = d5 * d2 - d1 * d6; + if (vb <= 0) { + // Barycentric coordinates (1-v, 0, v) + var v = d2 / (d2 - d6); + return a + ac*v; + } + } + + // Check if p is in edge region of BC, if so return projection of p onto BC + if ((d4 - d3) >= 0 && (d5 - d6) >= 0) { + var va = d3 * d6 - d5 * d4; + if (va <= 0) { + var v = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + return b + (c - b) * v; + } + } + + return p; + } + + /// <summary> + /// Closest point on the triangle abc to the point p when seen from above. + /// See: 'Real Time Collision Detection' by Christer Ericson, chapter 5.1, page 141 + /// </summary> + public static Vector3 ClosestPointOnTriangleXZ (Vector3 a, Vector3 b, Vector3 c, Vector3 p) { + // Check if p is in vertex region outside A + var ab = new Vector2(b.x - a.x, b.z - a.z); + var ac = new Vector2(c.x - a.x, c.z - a.z); + var ap = new Vector2(p.x - a.x, p.z - a.z); + + var d1 = Vector2.Dot(ab, ap); + var d2 = Vector2.Dot(ac, ap); + + // Barycentric coordinates (1,0,0) + if (d1 <= 0 && d2 <= 0) { + return a; + } + + // Check if p is in vertex region outside B + var bp = new Vector2(p.x - b.x, p.z - b.z); + var d3 = Vector2.Dot(ab, bp); + var d4 = Vector2.Dot(ac, bp); + + // Barycentric coordinates (0,1,0) + if (d3 >= 0 && d4 <= d3) { + return b; + } + + // Check if p is in edge region outside AB, if so return a projection of p onto AB + var vc = d1 * d4 - d3 * d2; + if (d1 >= 0 && d3 <= 0 && vc <= 0) { + // Barycentric coordinates (1-v, v, 0) + var v = d1 / (d1 - d3); + return (1-v)*a + v*b; + } + + // Check if p is in vertex region outside C + var cp = new Vector2(p.x - c.x, p.z - c.z); + var d5 = Vector2.Dot(ab, cp); + var d6 = Vector2.Dot(ac, cp); + + // Barycentric coordinates (0,0,1) + if (d6 >= 0 && d5 <= d6) { + return c; + } + + // Check if p is in edge region of AC, if so return a projection of p onto AC + var vb = d5 * d2 - d1 * d6; + if (d2 >= 0 && d6 <= 0 && vb <= 0) { + // Barycentric coordinates (1-v, 0, v) + var v = d2 / (d2 - d6); + return (1-v)*a + v*c; + } + + // Check if p is in edge region of BC, if so return projection of p onto BC + var va = d3 * d6 - d5 * d4; + if ((d4 - d3) >= 0 && (d5 - d6) >= 0 && va <= 0) { + var v = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + return b + (c - b) * v; + } else { + // P is inside the face region. Compute the point using its barycentric coordinates (u, v, w) + // Note that the x and z coordinates will be exactly the same as P's x and z coordinates + var denom = 1f / (va + vb + vc); + var v = vb * denom; + var w = vc * denom; + + return new Vector3(p.x, (1 - v - w)*a.y + v*b.y + w*c.y, p.z); + } + } + + /// <summary> + /// Closest point on the triangle abc to the point p. + /// See: 'Real Time Collision Detection' by Christer Ericson, chapter 5.1, page 141 + /// </summary> + public static float3 ClosestPointOnTriangle (float3 a, float3 b, float3 c, float3 p) { + ClosestPointOnTriangleByRef(in a, in b, in c, in p, out var output); + return output; + } + + /// <summary> + /// Closest point on the triangle abc to the point p. + /// + /// Takes arguments by reference to be able to be burst-compiled. + /// + /// See: 'Real Time Collision Detection' by Christer Ericson, chapter 5.1, page 141 + /// + /// Returns: True if the point is inside the triangle, false otherwise, after the point has been projected on the plane that the triangle is in. + /// </summary> + [BurstCompile] + public static bool ClosestPointOnTriangleByRef (in float3 a, in float3 b, in float3 c, in float3 p, [NoAlias] out float3 output) { + // Check if p is in vertex region outside A + var ab = b - a; + var ac = c - a; + var ap = p - a; + + var d1 = math.dot(ab, ap); + var d2 = math.dot(ac, ap); + + // Barycentric coordinates (1,0,0) + if (d1 <= 0 && d2 <= 0) { + output = a; + return false; + } + + // Check if p is in vertex region outside B + var bp = p - b; + var d3 = math.dot(ab, bp); + var d4 = math.dot(ac, bp); + + // Barycentric coordinates (0,1,0) + if (d3 >= 0 && d4 <= d3) { + output = b; + return false; + } + + // Check if p is in edge region outside AB, if so return a projection of p onto AB + var vc = d1 * d4 - d3 * d2; + if (d1 >= 0 && d3 <= 0 && vc <= 0) { + // Barycentric coordinates (1-v, v, 0) + var v = d1 / (d1 - d3); + output = a + ab * v; + return false; + } + + // Check if p is in vertex region outside C + var cp = p - c; + var d5 = math.dot(ab, cp); + var d6 = math.dot(ac, cp); + + // Barycentric coordinates (0,0,1) + if (d6 >= 0 && d5 <= d6) { + output = c; + return false; + } + + // Check if p is in edge region of AC, if so return a projection of p onto AC + var vb = d5 * d2 - d1 * d6; + if (d2 >= 0 && d6 <= 0 && vb <= 0) { + // Barycentric coordinates (1-v, 0, v) + var v = d2 / (d2 - d6); + output = a + ac * v; + return false; + } + + // Check if p is in edge region of BC, if so return projection of p onto BC + var va = d3 * d6 - d5 * d4; + if ((d4 - d3) >= 0 && (d5 - d6) >= 0 && va <= 0) { + var v = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + output = b + (c - b) * v; + return false; + } else { + // P is inside the face region. Compute the point using its barycentric coordinates (u, v, w) + var denom = 1f / (va + vb + vc); + var v = vb * denom; + var w = vc * denom; + + // This is equal to: u*a + v*b + w*c, u = va*denom = 1 - v - w; + output = a + ab * v + ac * w; + return true; + } + } + + /// <summary> + /// Closest point on the triangle abc to the point p as barycentric coordinates. + /// + /// See: 'Real Time Collision Detection' by Christer Ericson, chapter 5.1, page 141 + /// </summary> + public static float3 ClosestPointOnTriangleBarycentric (float2 a, float2 b, float2 c, float2 p) { + // Check if p is in vertex region outside A + var ab = b - a; + var ac = c - a; + var ap = p - a; + + var d1 = math.dot(ab, ap); + var d2 = math.dot(ac, ap); + + // Barycentric coordinates (1,0,0) + if (d1 <= 0 && d2 <= 0) { + return new float3(1, 0, 0); + } + + // Check if p is in vertex region outside B + var bp = p - b; + var d3 = math.dot(ab, bp); + var d4 = math.dot(ac, bp); + + // Barycentric coordinates (0,1,0) + if (d3 >= 0 && d4 <= d3) { + return new float3(0, 1, 0); + } + + // Check if p is in edge region outside AB, if so return a projection of p onto AB + var vc = d1 * d4 - d3 * d2; + if (d1 >= 0 && d3 <= 0 && vc <= 0) { + // Barycentric coordinates (1-v, v, 0) + var v = d1 / (d1 - d3); + return new float3(1-v, v, 0); + } + + // Check if p is in vertex region outside C + var cp = p - c; + var d5 = math.dot(ab, cp); + var d6 = math.dot(ac, cp); + + // Barycentric coordinates (0,0,1) + if (d6 >= 0 && d5 <= d6) { + return new float3(0, 0, 1); + } + + // Check if p is in edge region of AC, if so return a projection of p onto AC + var vb = d5 * d2 - d1 * d6; + if (d2 >= 0 && d6 <= 0 && vb <= 0) { + // Barycentric coordinates (1-v, 0, v) + var v = d2 / (d2 - d6); + return new float3(1 - v, 0, v); + } + + // Check if p is in edge region of BC, if so return projection of p onto BC + var va = d3 * d6 - d5 * d4; + if ((d4 - d3) >= 0 && (d5 - d6) >= 0 && va <= 0) { + var v = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + return new float3(0, 1 - v, v); + } else { + // P is inside the face region. Compute the point using its barycentric coordinates (u, v, w) + var denom = 1f / (va + vb + vc); + var v = vb * denom; + var w = vc * denom; + return new float3(1 - v - w, v, w); + + // This is equal to: u*a + v*b + w*c, u = va*denom = 1 - v - w; + // return a + ab * v + ac * w; + } + } + + /// <summary> + /// Closest point on a triangle when one axis is scaled. + /// + /// Project the triangle onto the plane defined by the projection axis. + /// Then find the closest point on the triangle in the plane. + /// Calculate the distance to the closest point in the plane, call that D1. + /// Convert the closest point into 3D space, and calculate the distance to the + /// query point along the plane's normal, call that D2. + /// The final cost for a given point is D1 + D2 * distanceScaleAlongProjectionDirection. + /// + /// This will form a diamond shape of equivalent cost points around the query point (x). + /// The ratio of the width of this diamond to the height is equal to distanceScaleAlongProjectionDirection. + /// + /// ^ + /// / \ + /// / \ + /// / x \ + /// \ / + /// \ / + /// \ / + /// v + /// + /// See: <see cref="DistanceMetric.ClosestAsSeenFromAboveSoft(Vector3)"/> + /// </summary> + /// <param name="vi1">First vertex of the triangle, in graph space.</param> + /// <param name="vi2">Second vertex of the triangle, in graph space.</param> + /// <param name="vi3">Third vertex of the triangle, in graph space.</param> + /// <param name="projection">Projection parameters that are for example constructed from a movement plane.</param> + /// <param name="point">Point to find the closest point to.</param> + /// <param name="closest">Closest point on the triangle to the point.</param> + /// <param name="sqrDist">Squared cost from the point to the closest point on the triangle.</param> + /// <param name="distAlongProjection">Distance from the point to the closest point on the triangle along the projection axis.</param> + [BurstCompile] + public static void ClosestPointOnTriangleProjected (ref Int3 vi1, ref Int3 vi2, ref Int3 vi3, ref BBTree.ProjectionParams projection, ref float3 point, [NoAlias] out float3 closest, [NoAlias] out float sqrDist, [NoAlias] out float distAlongProjection) { + var v1 = (float3)vi1; + var v2 = (float3)vi2; + var v3 = (float3)vi3; + var v1proj = math.mul(projection.planeProjection, v1); + var v2proj = math.mul(projection.planeProjection, v2); + var v3proj = math.mul(projection.planeProjection, v3); + // TODO: Can be cached + var pointProj = math.mul(projection.planeProjection, point); + var closestBarycentric = ClosestPointOnTriangleBarycentric(v1proj, v2proj, v3proj, pointProj); + closest = v1*closestBarycentric.x + v2*closestBarycentric.y + v3*closestBarycentric.z; + var closestProj = v1proj*closestBarycentric.x + v2proj*closestBarycentric.y + v3proj*closestBarycentric.z; + distAlongProjection = math.abs(math.dot(closest - point, projection.projectionAxis)); + var distInPlane = math.length(closestProj - pointProj); + if (distInPlane < 0.01f) { + // If we are very close to being inside the triangle, + // check if we are actually inside the triangle using a more numerically robust method. + // If we are, set the in-plane-distance to 0. + // This is particularly important if distanceScaleAlongProjectionAxis is zero, + // as otherwise tie breaking may not work due to numerical issues. + var ci1 = (int3)vi1; + var ci2 = (int3)vi2; + var ci3 = (int3)vi3; + // wow, ugly + var pi = (int3)(Int3)(Vector3)point; + if (ContainsPoint(ref ci1, ref ci2, ref ci3, ref pi, in projection.planeProjection)) { + distInPlane = 0; + } + } + var dist = distInPlane + distAlongProjection*projection.distanceScaleAlongProjectionAxis; + sqrDist = dist*dist; + } + + /// <summary>Cached dictionary to avoid excessive allocations</summary> + static readonly Dictionary<Int3, int> cached_Int3_int_dict = new Dictionary<Int3, int>(); + + /// <summary> + /// Compress the mesh by removing duplicate vertices. + /// + /// Vertices that differ by only 1 along the y coordinate will also be merged together. + /// Warning: This function is not threadsafe. It uses some cached structures to reduce allocations. + /// </summary> + /// <param name="vertices">Vertices of the input mesh</param> + /// <param name="triangles">Triangles of the input mesh</param> + /// <param name="tags">Tags of the input mesh. One for each triangle.</param> + /// <param name="outVertices">Vertices of the output mesh.</param> + /// <param name="outTriangles">Triangles of the output mesh.</param> + /// <param name="outTags">Tags of the output mesh. One for each triangle.</param> + public static void CompressMesh (List<Int3> vertices, List<int> triangles, List<uint> tags, out Int3[] outVertices, out int[] outTriangles, out uint[] outTags) { + Dictionary<Int3, int> firstVerts = cached_Int3_int_dict; + + firstVerts.Clear(); + + // Use cached array to reduce memory allocations + int[] compressedPointers = ArrayPool<int>.Claim(vertices.Count); + + // Map positions to the first index they were encountered at + int count = 0; + for (int i = 0; i < vertices.Count; i++) { + // Check if the vertex position has already been added + // Also check one position up and one down because rounding errors can cause vertices + // that should end up in the same position to be offset 1 unit from each other + // TODO: Check along X and Z axes as well? + int ind; + if (!firstVerts.TryGetValue(vertices[i], out ind) && !firstVerts.TryGetValue(vertices[i] + new Int3(0, 1, 0), out ind) && !firstVerts.TryGetValue(vertices[i] + new Int3(0, -1, 0), out ind)) { + firstVerts.Add(vertices[i], count); + compressedPointers[i] = count; + vertices[count] = vertices[i]; + count++; + } else { + compressedPointers[i] = ind; + } + } + + // Create the triangle array or reuse the existing buffer + outTriangles = new int[triangles.Count]; + + // Remap the triangles to the new compressed indices + for (int i = 0; i < outTriangles.Length; i++) { + outTriangles[i] = compressedPointers[triangles[i]]; + } + + // Create the vertex array or reuse the existing buffer + outVertices = new Int3[count]; + + for (int i = 0; i < count; i++) + outVertices[i] = vertices[i]; + + ArrayPool<int>.Release(ref compressedPointers); + + outTags = tags.ToArray(); + } + + /// <summary> + /// Given a set of edges between vertices, follows those edges and returns them as chains and cycles. + /// + /// [Open online documentation to see images] + /// </summary> + /// <param name="outline">outline[a] = b if there is an edge from a to b.</param> + /// <param name="hasInEdge">hasInEdge should contain b if outline[a] = b for any key a.</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 image).</param> + public static void TraceContours (Dictionary<int, int> outline, HashSet<int> hasInEdge, System.Action<List<int>, bool> results) { + // Iterate through chains of the navmesh outline. + // I.e segments of the outline that are not loops + // we need to start these at the beginning of the chain. + // Then iterate over all the loops of the outline. + // Since they are loops, we can start at any point. + var obstacleVertices = ListPool<int>.Claim(); + var outlineKeys = ListPool<int>.Claim(); + + outlineKeys.AddRange(outline.Keys); + for (int k = 0; k <= 1; k++) { + bool cycles = k == 1; + for (int i = 0; i < outlineKeys.Count; i++) { + var startIndex = outlineKeys[i]; + + // Chains (not cycles) need to start at the start of the chain + // Cycles can start at any point + if (!cycles && hasInEdge.Contains(startIndex)) { + continue; + } + + var index = startIndex; + obstacleVertices.Clear(); + obstacleVertices.Add(index); + + while (outline.ContainsKey(index)) { + var next = outline[index]; + outline.Remove(index); + + obstacleVertices.Add(next); + + // We traversed a full cycle + if (next == startIndex) break; + + index = next; + } + + if (obstacleVertices.Count > 1) { + results(obstacleVertices, cycles); + } + } + } + + ListPool<int>.Release(ref outlineKeys); + ListPool<int>.Release(ref obstacleVertices); + } + + /// <summary>Divides each segment in the list into subSegments segments and fills the result list with the new points</summary> + public static void Subdivide (List<Vector3> points, List<Vector3> result, int subSegments) { + for (int i = 0; i < points.Count-1; i++) + for (int j = 0; j < subSegments; j++) + result.Add(Vector3.Lerp(points[i], points[i+1], j / (float)subSegments)); + + result.Add(points[points.Count-1]); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarMath.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarMath.cs.meta new file mode 100644 index 0000000..fe1bca0 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarMath.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 960fd9020b1f74f939fee737c3c0f491 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarPath.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarPath.cs new file mode 100644 index 0000000..1c5e6c0 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarPath.cs @@ -0,0 +1,2307 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using Pathfinding; +using Pathfinding.Drawing; +#if UNITY_5_5_OR_NEWER +using UnityEngine.Profiling; +using Pathfinding.Util; +using Pathfinding.Graphs.Navmesh; +using Pathfinding.Graphs.Util; +using Pathfinding.Jobs; +using Unity.Jobs; + + + +#endif + +#if NETFX_CORE +using Thread = Pathfinding.WindowsStore.Thread; +#else +using Thread = System.Threading.Thread; +#endif + +[ExecuteInEditMode] +[AddComponentMenu("Pathfinding/AstarPath")] +/// <summary> +/// Core component for the A* Pathfinding System. +/// This class handles all of the pathfinding system, calculates all paths and stores the info. +/// This class is a singleton class, meaning there should only exist at most one active instance of it in the scene. +/// It might be a bit hard to use directly, usually interfacing with the pathfinding system is done through the <see cref="Pathfinding.Seeker"/> class. +/// </summary> +[HelpURL("https://arongranberg.com/astar/documentation/stable/astarpath.html")] +public class AstarPath : VersionedMonoBehaviour { + /// <summary>The version number for the A* Pathfinding Project</summary> + public static readonly System.Version Version = new System.Version(5, 1, 1); + + /// <summary>Information about where the package was downloaded</summary> + public enum AstarDistribution { WebsiteDownload, AssetStore, PackageManager }; + + /// <summary>Used by the editor to guide the user to the correct place to download updates</summary> + public static readonly AstarDistribution Distribution = AstarDistribution.AssetStore; + + /// <summary> + /// Which branch of the A* Pathfinding Project is this release. + /// Used when checking for updates so that + /// users of the development versions can get notifications of development + /// updates. + /// </summary> + public static readonly string Branch = "master"; + + /// <summary>Holds all graph data</summary> + [UnityEngine.Serialization.FormerlySerializedAs("astarData")] + public AstarData data; + + /// <summary> + /// Returns the active AstarPath object in the scene. + /// Note: This is only set if the AstarPath object has been initialized (which happens in Awake). + /// </summary> +#if UNITY_4_6 || UNITY_4_3 + public static new AstarPath active; +#else + public static AstarPath active; +#endif + + /// <summary>Shortcut to Pathfinding.AstarData.graphs</summary> + public NavGraph[] graphs { + get { + return data.graphs; + } + } + + #region InspectorDebug + /// <summary> + /// Visualize graphs in the scene view (editor only). + /// [Open online documentation to see images] + /// </summary> + public bool showNavGraphs = true; + + /// <summary> + /// Toggle to show unwalkable nodes. + /// + /// Note: Only relevant in the editor + /// + /// See: <see cref="unwalkableNodeDebugSize"/> + /// </summary> + public bool showUnwalkableNodes = true; + + /// <summary> + /// The mode to use for drawing nodes in the sceneview. + /// + /// Note: Only relevant in the editor + /// + /// See: Pathfinding.GraphDebugMode + /// </summary> + public GraphDebugMode debugMode; + + /// <summary> + /// Low value to use for certain <see cref="debugMode"/> modes. + /// For example if <see cref="debugMode"/> is set to G, this value will determine when the node will be completely red. + /// + /// Note: Only relevant in the editor + /// + /// See: <see cref="debugRoof"/> + /// See: <see cref="debugMode"/> + /// </summary> + public float debugFloor = 0; + + /// <summary> + /// High value to use for certain <see cref="debugMode"/> modes. + /// For example if <see cref="debugMode"/> is set to G, this value will determine when the node will be completely green. + /// + /// For the penalty debug mode, the nodes will be colored green when they have a penalty less than <see cref="debugFloor"/> and red + /// when their penalty is greater or equal to this value and something between red and green otherwise. + /// + /// Note: Only relevant in the editor + /// + /// See: <see cref="debugFloor"/> + /// See: <see cref="debugMode"/> + /// </summary> + public float debugRoof = 20000; + + /// <summary> + /// If set, the <see cref="debugFloor"/> and <see cref="debugRoof"/> values will not be automatically recalculated. + /// + /// Note: Only relevant in the editor + /// </summary> + public bool manualDebugFloorRoof = false; + + + /// <summary> + /// If enabled, nodes will draw a line to their 'parent'. + /// This will show the search tree for the latest path. + /// + /// Note: Only relevant in the editor + /// + /// TODO: Add a showOnlyLastPath flag to indicate whether to draw every node or only the ones visited by the latest path. + /// </summary> + public bool showSearchTree = false; + + /// <summary> + /// Size of the red cubes shown in place of unwalkable nodes. + /// + /// Note: Only relevant in the editor. Does not apply to grid graphs. + /// See: <see cref="showUnwalkableNodes"/> + /// </summary> + public float unwalkableNodeDebugSize = 0.3F; + + /// <summary> + /// The amount of debugging messages. + /// Use less debugging to improve performance (a bit) or just to get rid of the Console spamming. + /// Use more debugging (heavy) if you want more information about what the pathfinding scripts are doing. + /// The InGame option will display the latest path log using in-game GUI. + /// + /// [Open online documentation to see images] + /// </summary> + public PathLog logPathResults = PathLog.Normal; + + #endregion + + #region InspectorSettings + /// <summary> + /// Maximum distance to search for nodes. + /// When searching for the nearest node to a point, this is the limit (in world units) for how far away it is allowed to be. + /// + /// This is relevant if you try to request a path to a point that cannot be reached and it thus has to search for + /// the closest node to that point which can be reached (which might be far away). If it cannot find a node within this distance + /// then the path will fail. + /// + /// [Open online documentation to see images] + /// + /// See: Pathfinding.NNConstraint.constrainDistance + /// </summary> + public float maxNearestNodeDistance = 100; + + /// <summary> + /// Max Nearest Node Distance Squared. + /// See: <see cref="maxNearestNodeDistance"/> + /// </summary> + public float maxNearestNodeDistanceSqr { + get { return maxNearestNodeDistance*maxNearestNodeDistance; } + } + + /// <summary> + /// If true, all graphs will be scanned during Awake. + /// If you disable this, you will have to call <see cref="Scan"/> yourself to enable pathfinding. + /// Alternatively you could load a saved graph from a file. + /// + /// If a startup cache has been generated (see save-load-graphs) (view in online documentation for working links), it always takes priority to load that instead of scanning the graphs. + /// + /// This can be useful to enable if you want to scan your graphs asynchronously, or if you have a procedural world which has not been created yet + /// at the start of the game. + /// + /// See: <see cref="Scan"/> + /// See: <see cref="ScanAsync"/> + /// </summary> + public bool scanOnStartup = true; + + /// <summary> + /// Do a full GetNearest search for all graphs. + /// Additional searches will normally only be done on the graph which in the first fast search seemed to have the closest node. + /// With this setting on, additional searches will be done on all graphs since the first check is not always completely accurate. + /// More technically: GetNearestForce on all graphs will be called if true, otherwise only on the one graph which's GetNearest search returned the best node. + /// Usually faster when disabled, but higher quality searches when enabled. + /// Note: For the PointGraph this setting doesn't matter much as it has only one search mode. + /// </summary> + [System.Obsolete("This setting has been removed. It is now always true", true)] + public bool fullGetNearestSearch = false; + + /// <summary> + /// Prioritize graphs. + /// Graphs will be prioritized based on their order in the inspector. + /// The first graph which has a node closer than <see cref="prioritizeGraphsLimit"/> will be chosen instead of searching all graphs. + /// + /// Deprecated: This setting has been removed. It was always a bit of a hack. Use NNConstraint.graphMask if you want to choose which graphs are searched. + /// </summary> + [System.Obsolete("This setting has been removed. It was always a bit of a hack. Use NNConstraint.graphMask if you want to choose which graphs are searched.", true)] + public bool prioritizeGraphs = false; + + /// <summary> + /// Distance limit for <see cref="prioritizeGraphs"/>. + /// See: <see cref="prioritizeGraphs"/> + /// + /// Deprecated: This setting has been removed. It was always a bit of a hack. Use NNConstraint.graphMask if you want to choose which graphs are searched. + /// </summary> + [System.Obsolete("This setting has been removed. It was always a bit of a hack. Use NNConstraint.graphMask if you want to choose which graphs are searched.", true)] + public float prioritizeGraphsLimit = 1F; + + /// <summary> + /// Reference to the color settings for this AstarPath object. + /// Color settings include for example which color the nodes should be in, in the sceneview. + /// </summary> + public AstarColor colorSettings; + + /// <summary> + /// Stored tag names. + /// See: AstarPath.FindTagNames + /// See: AstarPath.GetTagNames + /// </summary> + [SerializeField] + protected string[] tagNames = null; + + /// <summary> + /// The distance function to use as a heuristic. + /// The heuristic, often referred to as just 'H' is the estimated cost from a node to the target. + /// Different heuristics affect how the path picks which one to follow from multiple possible with the same length + /// See: <see cref="Pathfinding.Heuristic"/> for more details and descriptions of the different modes. + /// See: <a href="https://en.wikipedia.org/wiki/Admissible_heuristic">Wikipedia: Admissible heuristic</a> + /// See: <a href="https://en.wikipedia.org/wiki/A*_search_algorithm">Wikipedia: A* search algorithm</a> + /// See: <a href="https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm">Wikipedia: Dijkstra's Algorithm</a> + /// + /// Warning: Reducing the heuristic scale below 1, or disabling the heuristic, can significantly increase the cpu cost for pathfinding, especially for large graphs. + /// </summary> + public Heuristic heuristic = Heuristic.Euclidean; + + /// <summary> + /// The scale of the heuristic. + /// If a value lower than 1 is used, the pathfinder will search more nodes (slower). + /// If 0 is used, the pathfinding algorithm will be reduced to dijkstra's algorithm. This is equivalent to setting <see cref="heuristic"/> to None. + /// If a value larger than 1 is used the pathfinding will (usually) be faster because it expands fewer nodes, but the paths may no longer be the optimal (i.e the shortest possible paths). + /// + /// Usually you should leave this to the default value of 1. + /// + /// Warning: Reducing the heuristic scale below 1, or disabling the heuristic, can significantly increase the cpu cost for pathfinding, especially for large graphs. + /// + /// See: https://en.wikipedia.org/wiki/Admissible_heuristic + /// See: https://en.wikipedia.org/wiki/A*_search_algorithm + /// See: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm + /// </summary> + public float heuristicScale = 1F; + + /// <summary> + /// Number of pathfinding threads to use. + /// Multithreading puts pathfinding in another thread, this is great for performance on 2+ core computers since the framerate will barely be affected by the pathfinding at all. + /// - None indicates that the pathfinding is run in the Unity thread as a coroutine + /// - Automatic will try to adjust the number of threads to the number of cores and memory on the computer. + /// Less than 512mb of memory or a single core computer will make it revert to using no multithreading. + /// + /// It is recommended that you use one of the "Auto" settings that are available. + /// The reason is that even if your computer might be beefy and have 8 cores. + /// Other computers might only be quad core or dual core in which case they will not benefit from more than + /// 1 or 3 threads respectively (you usually want to leave one core for the unity thread). + /// If you use more threads than the number of cores on the computer it is mostly just wasting memory, it will not run any faster. + /// The extra memory usage is not trivially small. Each thread needs to keep a small amount of data for each node in all the graphs. + /// It is not the full graph data but it is proportional to the number of nodes. + /// The automatic settings will inspect the machine it is running on and use that to determine the number of threads so that no memory is wasted. + /// + /// The exception is if you only have one (or maybe two characters) active at time. Then you should probably just go with one thread always since it is very unlikely + /// that you will need the extra throughput given by more threads. Keep in mind that more threads primarily increases throughput by calculating different paths on different + /// threads, it will not calculate individual paths any faster. + /// + /// Note that if you are modifying the pathfinding core scripts or if you are directly modifying graph data without using any of the + /// safe wrappers (like <see cref="AddWorkItem)"/> multithreading can cause strange errors and pathfinding stopping to work if you are not careful. + /// For basic usage (not modding the pathfinding core) it should be safe. + /// + /// Note: WebGL does not support threads at all (since javascript is single-threaded) so no threads will be used on that platform. + /// + /// See: CalculateThreadCount + /// </summary> + public ThreadCount threadCount = ThreadCount.One; + + /// <summary> + /// Max number of milliseconds to spend each frame for pathfinding. + /// At least 500 nodes will be searched each frame (if there are that many to search). + /// When using multithreading this value is irrelevant. + /// </summary> + public float maxFrameTime = 1F; + + /// <summary> + /// Throttle graph updates and batch them to improve performance. + /// If toggled, graph updates will batched and executed less often (specified by <see cref="graphUpdateBatchingInterval)"/>. + /// + /// This can have a positive impact on pathfinding throughput since the pathfinding threads do not need + /// to be stopped as often, and it reduces the overhead per graph update. + /// All graph updates are still applied however, they are just batched together so that more of them are + /// applied at the same time. + /// + /// However do not use this if you want minimal latency between a graph update being requested + /// and it being applied. + /// + /// This only applies to graph updates requested using the <see cref="UpdateGraphs"/> method. Not those requested + /// using <see cref="AddWorkItem"/>. + /// + /// If you want to apply graph updates immediately at some point, you can call <see cref="FlushGraphUpdates"/>. + /// + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public bool batchGraphUpdates = false; + + /// <summary> + /// Minimum number of seconds between each batch of graph updates. + /// If <see cref="batchGraphUpdates"/> is true, this defines the minimum number of seconds between each batch of graph updates. + /// + /// This can have a positive impact on pathfinding throughput since the pathfinding threads do not need + /// to be stopped as often, and it reduces the overhead per graph update. + /// All graph updates are still applied however, they are just batched together so that more of them are + /// applied at the same time. + /// + /// Do not use this if you want minimal latency between a graph update being requested + /// and it being applied. + /// + /// This only applies to graph updates requested using the <see cref="UpdateGraphs"/> method. Not those requested + /// using <see cref="AddWorkItem"/>. + /// + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public float graphUpdateBatchingInterval = 0.2F; + + #endregion + + #region DebugVariables +#if ProfileAstar + /// <summary> + /// How many paths has been computed this run. From application start. + /// Debugging variable + /// </summary> + public static int PathsCompleted = 0; + + public static System.Int64 TotalSearchedNodes = 0; + public static System.Int64 TotalSearchTime = 0; +#endif + + /// <summary> + /// The time it took for the last call to Scan() to complete. + /// Used to prevent automatically rescanning the graphs too often (editor only) + /// </summary> + public float lastScanTime { get; private set; } + + /// <summary> + /// The path to debug using gizmos. + /// This is the path handler used to calculate the last path. + /// It is used in the editor to draw debug information using gizmos. + /// </summary> + [System.NonSerialized] + public PathHandler debugPathData; + + /// <summary>The path ID to debug using gizmos</summary> + [System.NonSerialized] + public ushort debugPathID; + + /// <summary> + /// Debug string from the last completed path. + /// Will be updated if <see cref="logPathResults"/> == PathLog.InGame + /// </summary> + string inGameDebugPath; + + #endregion + + #region StatusVariables + + /// <summary> + /// Backing field for <see cref="isScanning"/>. + /// Cannot use an auto-property because they cannot be marked with System.NonSerialized. + /// </summary> + [System.NonSerialized] + bool isScanningBacking; + + /// <summary> + /// Set while any graphs are being scanned. + /// It will be true up until the FloodFill is done. + /// + /// Note: Not to be confused with graph updates. + /// + /// Used to better support Graph Update Objects called for example in OnPostScan + /// + /// See: IsAnyGraphUpdateQueued + /// See: IsAnyGraphUpdateInProgress + /// </summary> + public bool isScanning { get { return isScanningBacking; } private set { isScanningBacking = value; } } + + /// <summary> + /// Number of parallel pathfinders. + /// Returns the number of concurrent processes which can calculate paths at once. + /// When using multithreading, this will be the number of threads, if not using multithreading it is always 1 (since only 1 coroutine is used). + /// See: IsUsingMultithreading + /// </summary> + public int NumParallelThreads { + get { + return pathProcessor.NumThreads; + } + } + + /// <summary> + /// Returns whether or not multithreading is used. + /// \exception System.Exception Is thrown when it could not be decided if multithreading was used or not. + /// This should not happen if pathfinding is set up correctly. + /// Note: This uses info about if threads are running right now, it does not use info from the settings on the A* object. + /// </summary> + public bool IsUsingMultithreading { + get { + return pathProcessor.IsUsingMultithreading; + } + } + + /// <summary> + /// Returns if any graph updates are waiting to be applied. + /// Note: This is false while the updates are being performed. + /// Note: This does *not* includes other types of work items such as navmesh cutting or anything added by <see cref="AddWorkItem"/>. + /// </summary> + public bool IsAnyGraphUpdateQueued { get { return graphUpdates.IsAnyGraphUpdateQueued; } } + + /// <summary> + /// Returns if any graph updates are being calculated right now. + /// Note: This does *not* includes other types of work items such as navmesh cutting or anything added by <see cref="AddWorkItem"/>. + /// + /// See: IsAnyWorkItemInProgress + /// </summary> + public bool IsAnyGraphUpdateInProgress { get { return graphUpdates.IsAnyGraphUpdateInProgress; } } + + /// <summary> + /// Returns if any work items are in progress right now. + /// Note: This includes pretty much all types of graph updates. + /// Such as normal graph updates, navmesh cutting and anything added by <see cref="AddWorkItem"/>. + /// </summary> + public bool IsAnyWorkItemInProgress { get { return workItems.workItemsInProgress; } } + + /// <summary> + /// Returns if this code is currently being exectuted inside a work item. + /// Note: This includes pretty much all types of graph updates. + /// Such as normal graph updates, navmesh cutting and anything added by <see cref="AddWorkItem"/>. + /// + /// In contrast to <see cref="IsAnyWorkItemInProgress"/> this is only true when work item code is being executed, it is not + /// true in-between the updates to a work item that takes several frames to complete. + /// </summary> + internal bool IsInsideWorkItem { get { return workItems.workItemsInProgressRightNow; } } + + #endregion + + #region Callbacks + /// <summary> + /// Called on Awake before anything else is done. + /// This is called at the start of the Awake call, right after <see cref="active"/> has been set, but this is the only thing that has been done. + /// Use this when you want to set up default settings for an AstarPath component created during runtime since some settings can only be changed in Awake + /// (such as multithreading related stuff) + /// <code> + /// // Create a new AstarPath object on Start and apply some default settings + /// public void Start () { + /// AstarPath.OnAwakeSettings += ApplySettings; + /// AstarPath astar = gameObject.AddComponent<AstarPath>(); + /// } + /// + /// public void ApplySettings () { + /// // Unregister from the delegate + /// AstarPath.OnAwakeSettings -= ApplySettings; + /// // For example threadCount should not be changed after the Awake call + /// // so here's the only place to set it if you create the component during runtime + /// AstarPath.active.threadCount = ThreadCount.One; + /// } + /// </code> + /// </summary> + public static System.Action OnAwakeSettings; + + /// <summary>Called for each graph before they are scanned. In most cases it is recommended to create a custom class which inherits from Pathfinding.GraphModifier instead.</summary> + public static OnGraphDelegate OnGraphPreScan; + + /// <summary>Called for each graph after they have been scanned. All other graphs might not have been scanned yet. In most cases it is recommended to create a custom class which inherits from Pathfinding.GraphModifier instead.</summary> + public static OnGraphDelegate OnGraphPostScan; + + /// <summary>Called for each path before searching. Be careful when using multithreading since this will be called from a different thread.</summary> + public static OnPathDelegate OnPathPreSearch; + + /// <summary>Called for each path after searching. Be careful when using multithreading since this will be called from a different thread.</summary> + public static OnPathDelegate OnPathPostSearch; + + /// <summary>Called before starting the scanning. In most cases it is recommended to create a custom class which inherits from Pathfinding.GraphModifier instead.</summary> + public static OnScanDelegate OnPreScan; + + /// <summary>Called after scanning. This is called before applying links, flood-filling the graphs and other post processing. In most cases it is recommended to create a custom class which inherits from Pathfinding.GraphModifier instead.</summary> + public static OnScanDelegate OnPostScan; + + /// <summary>Called after scanning has completed fully. This is called as the last thing in the Scan function. In most cases it is recommended to create a custom class which inherits from Pathfinding.GraphModifier instead.</summary> + public static OnScanDelegate OnLatePostScan; + + /// <summary>Called when any graphs are updated. Register to for example recalculate the path whenever a graph changes. In most cases it is recommended to create a custom class which inherits from Pathfinding.GraphModifier instead.</summary> + public static OnScanDelegate OnGraphsUpdated; + + /// <summary> + /// Called when pathID overflows 65536 and resets back to zero. + /// Note: This callback will be cleared every time it is called, so if you want to register to it repeatedly, register to it directly on receiving the callback as well. + /// </summary> + public static System.Action On65KOverflow; + + /// <summary> + /// Called right after callbacks on paths have been called. + /// + /// A path's callback function runs on the main thread when the path has been calculated. + /// This is done in batches for all paths that have finished their calculation since the last frame. + /// This event will trigger right after a batch of callbacks have been called. + /// + /// If you do not want to use individual path callbacks, you can use this instead to poll all pending paths + /// and see which ones have completed. This is better than doing it in e.g. the Update loop, because + /// here you will have a guarantee that all calculated paths are still valid. + /// Immediately after this callback has finished, other things may invalidate calculated paths, like for example + /// graph updates. + /// + /// This is used by the ECS integration to update all entities' pending paths, without having to store + /// a callback for each agent, and also to avoid the ECS synchronization overhead that having individual + /// callbacks would entail. + /// </summary> + public static System.Action OnPathsCalculated; + + #endregion + + #region MemoryStructures + + /// <summary>Processes graph updates</summary> + readonly GraphUpdateProcessor graphUpdates; + + /// <summary>Holds a hierarchical graph to speed up some queries like if there is a path between two nodes</summary> + internal readonly HierarchicalGraph hierarchicalGraph; + + /// <summary>Holds all active off-mesh links</summary> + public readonly OffMeshLinks offMeshLinks; + + /// <summary> + /// Handles navmesh cuts. + /// See: <see cref="Pathfinding.NavmeshCut"/> + /// </summary> + public NavmeshUpdates navmeshUpdates = new NavmeshUpdates(); + + /// <summary>Processes work items</summary> + readonly WorkItemProcessor workItems; + + /// <summary>Holds all paths waiting to be calculated and calculates them</summary> + readonly PathProcessor pathProcessor; + + /// <summary>Holds global node data that cannot be stored in individual graphs</summary> + internal GlobalNodeStorage nodeStorage; + + /// <summary> + /// Global read-write lock for graph data. + /// + /// Graph data is always consistent from the main-thread's perspective, but if you are using jobs to read from graph data, you may need this. + /// + /// A write lock is held automatically... + /// - During graph updates. During async graph updates, the lock is only held once per frame while the graph update is actually running, not for the whole duration. + /// - During work items. Async work items work similarly to graph updates, the lock is only held once per frame while the work item is actually running. + /// - When <see cref="GraphModifier"/> events run. + /// - When graph related callbacks, such as <see cref="OnGraphsUpdated"/>, run. + /// - During the last step of a graph's scanning process. See <see cref="ScanningStage"/>. + /// + /// To use e.g. AstarPath.active.GetNearest from an ECS job, you'll need to acquire a read lock first, and make sure the lock is only released when the job is finished. + /// + /// <code> + /// var readLock = AstarPath.active.LockGraphDataForReading(); + /// var handle = new MyJob { + /// // ... + /// }.Schedule(readLock.dependency); + /// readLock.UnlockAfter(handle); + /// </code> + /// + /// See: <see cref="LockGraphDataForReading"/> + /// </summary> + RWLock graphDataLock = new RWLock(); + + bool graphUpdateRoutineRunning = false; + + /// <summary>Makes sure QueueGraphUpdates will not queue multiple graph update orders</summary> + bool graphUpdatesWorkItemAdded = false; + + /// <summary> + /// Time the last graph update was done. + /// Used to group together frequent graph updates to batches + /// </summary> + float lastGraphUpdate = -9999F; + + /// <summary>Held if any work items are currently queued</summary> + PathProcessor.GraphUpdateLock workItemLock; + + /// <summary>Holds all completed paths waiting to be returned to where they were requested</summary> + internal readonly PathReturnQueue pathReturnQueue; + + /// <summary> + /// Holds settings for heuristic optimization. + /// See: heuristic-opt (view in online documentation for working links) + /// </summary> + public EuclideanEmbedding euclideanEmbedding = new EuclideanEmbedding(); + + #endregion + + /// <summary> + /// Shows or hides graph inspectors. + /// Used internally by the editor + /// </summary> + public bool showGraphs = false; + + /// <summary> + /// The next unused Path ID. + /// Incremented for every call to GetNextPathID + /// </summary> + private ushort nextFreePathID = 1; + + private AstarPath () { + pathReturnQueue = new PathReturnQueue(this, () => { + if (OnPathsCalculated != null) OnPathsCalculated(); + }); + + // Make sure that the pathProcessor and node storage is never null + nodeStorage = new GlobalNodeStorage(this); + hierarchicalGraph = new HierarchicalGraph(nodeStorage); + pathProcessor = new PathProcessor(this, pathReturnQueue, 1, false); + offMeshLinks = new OffMeshLinks(this); + + workItems = new WorkItemProcessor(this); + graphUpdates = new GraphUpdateProcessor(this); + navmeshUpdates.astar = this; + data = new AstarData(this); + + // Forward graphUpdates.OnGraphsUpdated to AstarPath.OnGraphsUpdated + workItems.OnGraphsUpdated += () => { + if (OnGraphsUpdated != null) { + try { + OnGraphsUpdated(this); + } catch (System.Exception e) { + Debug.LogException(e); + } + } + }; + + pathProcessor.OnPathPreSearch += path => { + var tmp = OnPathPreSearch; + if (tmp != null) tmp(path); + }; + + pathProcessor.OnPathPostSearch += path => { + LogPathResults(path); + var tmp = OnPathPostSearch; + if (tmp != null) tmp(path); + }; + + // Sent every time the path queue is unblocked + pathProcessor.OnQueueUnblocked += () => { + if (euclideanEmbedding.dirty) { + euclideanEmbedding.RecalculateCosts(); + } + }; + } + + /// <summary> + /// Returns tag names. + /// Makes sure that the tag names array is not null and of length 32. + /// If it is null or not of length 32, it creates a new array and fills it with 0,1,2,3,4 etc... + /// See: AstarPath.FindTagNames + /// </summary> + public string[] GetTagNames () { + if (tagNames == null || tagNames.Length != 32) { + tagNames = new string[32]; + for (int i = 0; i < tagNames.Length; i++) { + tagNames[i] = ""+i; + } + tagNames[0] = "Basic Ground"; + } + return tagNames; + } + + /// <summary> + /// Used outside of play mode to initialize the AstarPath object even if it has not been selected in the inspector yet. + /// This will set the <see cref="active"/> property and deserialize all graphs. + /// + /// This is useful if you want to do changes to the graphs in the editor outside of play mode, but cannot be sure that the graphs have been deserialized yet. + /// In play mode this method does nothing. + /// </summary> + public static void FindAstarPath () { + if (Application.isPlaying) return; + if (active == null) active = UnityCompatibility.FindAnyObjectByType<AstarPath>(); + if (active != null && (active.data.graphs == null || active.data.graphs.Length == 0)) active.data.DeserializeGraphs(); + } + + /// <summary> + /// Tries to find an AstarPath object and return tag names. + /// If an AstarPath object cannot be found, it returns an array of length 1 with an error message. + /// See: AstarPath.GetTagNames + /// </summary> + public static string[] FindTagNames () { + FindAstarPath(); + return active != null? active.GetTagNames () : new string[1] { "There is no AstarPath component in the scene" }; + } + + /// <summary>Returns the next free path ID</summary> + internal ushort GetNextPathID () { + if (nextFreePathID == 0) { + nextFreePathID++; + + if (On65KOverflow != null) { + System.Action tmp = On65KOverflow; + On65KOverflow = null; + tmp(); + } + } + return nextFreePathID++; + } + + void RecalculateDebugLimits () { +#if UNITY_EDITOR + debugFloor = float.PositiveInfinity; + debugRoof = float.NegativeInfinity; + + bool ignoreSearchTree = !showSearchTree || debugPathData == null; + UnsafeSpan<GlobalNodeStorage.DebugPathNode> debugPathNodes; + if (debugPathData != null && debugPathData.threadID < active.nodeStorage.pathfindingThreadData.Length) debugPathNodes = active.nodeStorage.pathfindingThreadData[debugPathData.threadID].debugPathNodes; + else debugPathNodes = default; + + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null && graphs[i].drawGizmos) { + graphs[i].GetNodes(node => { + if (node.Walkable && (ignoreSearchTree || Pathfinding.Util.GraphGizmoHelper.InSearchTree(node, debugPathNodes, debugPathID))) { + float value; + if (debugMode == GraphDebugMode.Penalty) { + value = node.Penalty; + } else if (debugPathNodes.Length > 0) { + var rnode = debugPathNodes[node.NodeIndex]; + switch (debugMode) { + case GraphDebugMode.F: + value = rnode.g + rnode.h; + break; + case GraphDebugMode.G: + value = rnode.g; + break; + default: + case GraphDebugMode.H: + value = rnode.h; + break; + } + } else { + value = 0; + } + debugFloor = Mathf.Min(debugFloor, value); + debugRoof = Mathf.Max(debugRoof, value); + } + }); + } + } + + if (float.IsInfinity(debugFloor)) { + debugFloor = 0; + debugRoof = 1; + } + + // Make sure they are not identical, that will cause the color interpolation to fail + if (debugRoof-debugFloor < 1) debugRoof += 1; +#else + debugFloor = 0; + debugRoof = 1; +#endif + } + + RedrawScope redrawScope; + + /// <summary>Calls OnDrawGizmos on graph generators</summary> + public override void DrawGizmos () { + if (active != this || graphs == null) { + return; + } + + colorSettings.PushToStatic(this); + + if (!redrawScope.isValid) redrawScope = DrawingManager.GetRedrawScope(gameObject); + + if (!workItems.workItemsInProgress && !isScanning) { + // When updating graphs, graph info might not be valid, + // and we cannot render anything during those frames. + // Therefore we use a redraw scope which will continue drawing + // until we dispose it. + redrawScope.Rewind(); + if (showNavGraphs && !manualDebugFloorRoof) { + RecalculateDebugLimits(); + } + + Profiler.BeginSample("Graph.OnDrawGizmos"); + // Loop through all graphs and draw their gizmos + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null && graphs[i].drawGizmos) + graphs[i].OnDrawGizmos(DrawingManager.instance.gizmos, showNavGraphs, redrawScope); + } + Profiler.EndSample(); + + if (showNavGraphs) { + euclideanEmbedding.OnDrawGizmos(); + if (debugMode == GraphDebugMode.HierarchicalNode) hierarchicalGraph.OnDrawGizmos(DrawingManager.instance.gizmos, redrawScope); + if (debugMode == GraphDebugMode.NavmeshBorderObstacles) hierarchicalGraph.navmeshEdges.OnDrawGizmos(DrawingManager.instance.gizmos, redrawScope); + } + } + } + +#if !ASTAR_NO_GUI + /// <summary> + /// Draws the InGame debugging (if enabled) + /// See: <see cref="logPathResults"/> PathLog + /// </summary> + private void OnGUI () { + if (logPathResults == PathLog.InGame && inGameDebugPath != "") { + GUI.Label(new Rect(5, 5, 400, 600), inGameDebugPath); + } + } +#endif + + /// <summary> + /// Prints path results to the log. What it prints can be controled using <see cref="logPathResults"/>. + /// See: <see cref="logPathResults"/> + /// See: PathLog + /// See: Pathfinding.Path.DebugString + /// </summary> + private void LogPathResults (Path path) { + if (logPathResults != PathLog.None && (path.error || logPathResults != PathLog.OnlyErrors)) { + string debug = (path as IPathInternals).DebugString(logPathResults); + + if (logPathResults == PathLog.InGame) { + inGameDebugPath = debug; + } else if (path.error) { + Debug.LogWarning(debug); + } else { + Debug.Log(debug); + } + } + } + + /// <summary> + /// Checks if any work items need to be executed + /// then runs pathfinding for a while (if not using multithreading because + /// then the calculation happens in other threads) + /// and then returns any calculated paths to the + /// scripts that requested them. + /// + /// See: PerformBlockingActions + /// See: PathProcessor.TickNonMultithreaded + /// See: PathReturnQueue.ReturnPaths + /// </summary> + private void Update () { + // This class uses the [ExecuteInEditMode] attribute + // So Update is called even when not playing + // Don't do anything when not in play mode + if (!Application.isPlaying) return; + + navmeshUpdates.Update(); + + // Execute blocking actions such as graph updates + // when not scanning + if (!isScanning) { + PerformBlockingActions(); + } + + // Calculates paths when not using multithreading + if (!pathProcessor.IsUsingMultithreading) pathProcessor.TickNonMultithreaded(); + + // Return calculated paths + pathReturnQueue.ReturnPaths(true); + } + + private void PerformBlockingActions (bool force = false) { + if (workItemLock.Held && pathProcessor.queue.allReceiversBlocked) { + // Return all paths before starting blocking actions + // since these might change the graph and make returned paths invalid (at least the nodes) + pathReturnQueue.ReturnPaths(false); + + Profiler.BeginSample("Work Items"); + if (workItems.ProcessWorkItemsForUpdate(force)) { + // At this stage there are no more work items, resume pathfinding threads + workItemLock.Release(); + } + Profiler.EndSample(); + } + } + + /// <summary> + /// Add a work item to be processed when pathfinding is paused. + /// Convenience method that is equivalent to + /// <code> + /// AddWorkItem(new AstarWorkItem(callback)); + /// </code> + /// + /// See: <see cref="AddWorkItem(AstarWorkItem)"/> + /// </summary> + public void AddWorkItem (System.Action callback) { + AddWorkItem(new AstarWorkItem(callback)); + } + + /// <summary> + /// Add a work item to be processed when pathfinding is paused. + /// Convenience method that is equivalent to + /// <code> + /// AddWorkItem(new AstarWorkItem(callback)); + /// </code> + /// + /// See: <see cref="AddWorkItem(AstarWorkItem)"/> + /// </summary> + public void AddWorkItem (System.Action<IWorkItemContext> callback) { + AddWorkItem(new AstarWorkItem(callback)); + } + + /// <summary> + /// Add a work item to be processed when pathfinding is paused. + /// + /// The work item will be executed when it is safe to update nodes. This is defined as between the path searches. + /// When using more threads than one, calling this often might decrease pathfinding performance due to a lot of idling in the threads. + /// Not performance as in it will use much CPU power, but performance as in the number of paths per second will probably go down + /// (though your framerate might actually increase a tiny bit). + /// + /// You should only call this function from the main unity thread (i.e normal game code). + /// + /// <code> + /// AstarPath.active.AddWorkItem(new AstarWorkItem(() => { + /// // Safe to update graphs here + /// var node = AstarPath.active.GetNearest(transform.position).node; + /// node.Walkable = false; + /// })); + /// </code> + /// + /// <code> + /// AstarPath.active.AddWorkItem(() => { + /// // Safe to update graphs here + /// var node = AstarPath.active.GetNearest(transform.position).node; + /// node.position = (Int3)transform.position; + /// }); + /// </code> + /// + /// See: <see cref="FlushWorkItems"/> + /// </summary> + public void AddWorkItem (AstarWorkItem item) { + workItems.AddWorkItem(item); + + // Make sure pathfinding is stopped and work items are processed + if (!workItemLock.Held) { + workItemLock = PausePathfindingSoon(); + } + +#if UNITY_EDITOR + // If not playing, execute instantly + if (!Application.isPlaying) { + FlushWorkItems(); + } +#endif + } + + #region GraphUpdateMethods + + /// <summary> + /// Will apply queued graph updates as soon as possible, regardless of <see cref="batchGraphUpdates"/>. + /// Calling this multiple times will not create multiple callbacks. + /// This function is useful if you are limiting graph updates, but you want a specific graph update to be applied as soon as possible regardless of the time limit. + /// Note that this does not block until the updates are done, it merely bypasses the <see cref="batchGraphUpdates"/> time limit. + /// + /// See: <see cref="FlushGraphUpdates"/> + /// </summary> + public void QueueGraphUpdates () { + if (!graphUpdatesWorkItemAdded) { + graphUpdatesWorkItemAdded = true; + var workItem = graphUpdates.GetWorkItem(); + + // Add a new work item which first + // sets the graphUpdatesWorkItemAdded flag to false + // and then processes the graph updates + AddWorkItem(new AstarWorkItem(context => { + graphUpdatesWorkItemAdded = false; + lastGraphUpdate = Time.realtimeSinceStartup; + + workItem.initWithContext(context); + }, workItem.updateWithContext)); + } + } + + /// <summary> + /// Waits a moment with updating graphs. + /// If batchGraphUpdates is set, we want to keep some space between them to let pathfinding threads running and then calculate all queued calls at once + /// </summary> + IEnumerator DelayedGraphUpdate () { + graphUpdateRoutineRunning = true; + + yield return new WaitForSeconds(graphUpdateBatchingInterval-(Time.realtimeSinceStartup-lastGraphUpdate)); + QueueGraphUpdates(); + graphUpdateRoutineRunning = false; + } + + /// <summary> + /// Update all graphs within bounds after delay seconds. + /// The graphs will be updated as soon as possible. + /// + /// See: FlushGraphUpdates + /// See: batchGraphUpdates + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public void UpdateGraphs (Bounds bounds, float delay) { + UpdateGraphs(new GraphUpdateObject(bounds), delay); + } + + /// <summary> + /// Update all graphs using the GraphUpdateObject after delay seconds. + /// This can be used to, e.g make all nodes in a region unwalkable, or set them to a higher penalty. + /// + /// See: FlushGraphUpdates + /// See: batchGraphUpdates + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public void UpdateGraphs (GraphUpdateObject ob, float delay) { + StartCoroutine(UpdateGraphsInternal(ob, delay)); + } + + /// <summary>Update all graphs using the GraphUpdateObject after delay seconds</summary> + IEnumerator UpdateGraphsInternal (GraphUpdateObject ob, float delay) { + yield return new WaitForSeconds(delay); + UpdateGraphs(ob); + } + + /// <summary> + /// Update all graphs within bounds. + /// The graphs will be updated as soon as possible. + /// + /// This is equivalent to + /// <code> + /// UpdateGraphs(new GraphUpdateObject(bounds)); + /// </code> + /// + /// See: FlushGraphUpdates + /// See: batchGraphUpdates + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public void UpdateGraphs (Bounds bounds) { + UpdateGraphs(new GraphUpdateObject(bounds)); + } + + /// <summary> + /// Update all graphs using the GraphUpdateObject. + /// This can be used to, e.g make all nodes in a region unwalkable, or set them to a higher penalty. + /// The graphs will be updated as soon as possible (with respect to <see cref="batchGraphUpdates)"/> + /// + /// See: FlushGraphUpdates + /// See: batchGraphUpdates + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public void UpdateGraphs (GraphUpdateObject ob) { + if (ob.internalStage != GraphUpdateObject.STAGE_CREATED) { + throw new System.Exception("You are trying to update graphs using the same graph update object twice. Please create a new GraphUpdateObject instead."); + } + ob.internalStage = GraphUpdateObject.STAGE_PENDING; + graphUpdates.AddToQueue(ob); + + // If we should limit graph updates, start a coroutine which waits until we should update graphs + if (batchGraphUpdates && Time.realtimeSinceStartup-lastGraphUpdate < graphUpdateBatchingInterval) { + if (!graphUpdateRoutineRunning) { + StartCoroutine(DelayedGraphUpdate()); + } + } else { + // Otherwise, graph updates should be carried out as soon as possible + QueueGraphUpdates(); + } + } + + /// <summary> + /// Forces graph updates to complete in a single frame. + /// This will force the pathfinding threads to finish calculating the path they are currently calculating (if any) and then pause. + /// When all threads have paused, graph updates will be performed. + /// Warning: Using this very often (many times per second) can reduce your fps due to a lot of threads waiting for one another. + /// But you probably wont have to worry about that. + /// + /// Note: This is almost identical to <see cref="FlushWorkItems"/>, but added for more descriptive name. + /// This function will also override any time limit delays for graph updates. + /// This is because graph updates are implemented using work items. + /// So calling this function will also execute any other work items (if any are queued). + /// + /// Will not do anything if there are no graph updates queued (not even execute other work items). + /// </summary> + public void FlushGraphUpdates () { + if (IsAnyGraphUpdateQueued || IsAnyGraphUpdateInProgress) { + QueueGraphUpdates(); + FlushWorkItems(); + } + } + + #endregion + + /// <summary> + /// Forces work items to complete in a single frame. + /// This will force all work items to run immidiately. + /// This will force the pathfinding threads to finish calculating the path they are currently calculating (if any) and then pause. + /// When all threads have paused, work items will be executed (which can be e.g graph updates). + /// + /// Warning: Using this very often (many times per second) can reduce your fps due to a lot of threads waiting for one another. + /// But you probably wont have to worry about that + /// + /// Note: This is almost (note almost) identical to <see cref="FlushGraphUpdates"/>, but added for more descriptive name. + /// + /// Will not do anything if there are no queued work items waiting to run. + /// </summary> + public void FlushWorkItems () { + if (workItems.anyQueued || workItems.workItemsInProgress) { + var graphLock = PausePathfinding(); + PerformBlockingActions(true); + graphLock.Release(); + } + } + + /// <summary> + /// Calculates number of threads to use. + /// If count is not Automatic, simply returns count casted to an int. + /// Returns: An int specifying how many threads to use, 0 means a coroutine should be used for pathfinding instead of a separate thread. + /// + /// If count is set to Automatic it will return a value based on the number of processors and memory for the current system. + /// If memory is <= 512MB or logical cores are <= 1, it will return 0. If memory is <= 1024 it will clamp threads to max 2. + /// Otherwise it will return the number of logical cores clamped to 6. + /// + /// When running on WebGL this method always returns 0 + /// </summary> + public static int CalculateThreadCount (ThreadCount count) { +#if UNITY_WEBGL + return 0; +#else + if (count == ThreadCount.AutomaticLowLoad || count == ThreadCount.AutomaticHighLoad) { +#if ASTARDEBUG + Debug.Log(SystemInfo.systemMemorySize + " " + SystemInfo.processorCount + " " + SystemInfo.processorType); +#endif + + int logicalCores = Mathf.Max(1, SystemInfo.processorCount); + int memory = SystemInfo.systemMemorySize; + + if (memory <= 0) { + Debug.LogError("Machine reporting that is has <= 0 bytes of RAM. This is definitely not true, assuming 1 GiB"); + memory = 1024; + } + + if (logicalCores <= 1) return 0; + + if (memory <= 512) return 0; + + if (count == ThreadCount.AutomaticHighLoad) { + if (memory <= 1024) logicalCores = System.Math.Min(logicalCores, 2); + } else { + //Always run at at most processorCount-1 threads (one core reserved for unity thread). + // Many computers use hyperthreading, so dividing by two is used to remove the hyperthreading cores, pathfinding + // doesn't scale well past the number of physical cores anyway + logicalCores /= 2; + logicalCores = Mathf.Max(1, logicalCores); + + if (memory <= 1024) logicalCores = System.Math.Min(logicalCores, 2); + + logicalCores = System.Math.Min(logicalCores, 6); + } + + return logicalCores; + } else { + int val = (int)count; + return val; + } +#endif + } + + /// <summary>Initializes the <see cref="pathProcessor"/> field</summary> + void InitializePathProcessor () { + int numThreads = CalculateThreadCount(threadCount); + + // Outside of play mode everything is synchronous, so no threads are used. + if (!Application.isPlaying) numThreads = 0; + + + int numProcessors = Mathf.Max(numThreads, 1); + bool multithreaded = numThreads > 0; + pathProcessor.StopThreads(); + pathProcessor.SetThreadCount(numProcessors, multithreaded); + } + + /// <summary>Does simple error checking</summary> + internal void VerifyIntegrity () { + if (data.graphs == null) { + data.graphs = new NavGraph[0]; + data.UpdateShortcuts(); + } + } + + /// <summary>\cond internal</summary> + /// <summary> + /// Internal method to make sure <see cref="active"/> is set to this object and that <see cref="data"/> is not null. + /// Also calls OnEnable for the <see cref="colorSettings"/> and initializes data.userConnections if it wasn't initialized before + /// + /// Warning: This is mostly for use internally by the system. + /// </summary> + public void ConfigureReferencesInternal () { + colorSettings = colorSettings ?? new AstarColor(); + colorSettings.PushToStatic(this); + } + /// <summary>\endcond</summary> + + /// <summary> + /// Initializes the AstarData class. + /// Searches for graph types, calls Awake on <see cref="data"/> and on all graphs + /// + /// See: AstarData.FindGraphTypes + /// </summary> + void InitializeGraphs () { + data.FindGraphTypes(); + data.OnEnable(); + data.UpdateShortcuts(); + } + + void ShutdownPathfindingThreads () { + // Block until the pathfinding threads have + // completed their current path calculation + var graphLock = PausePathfinding(); + + navmeshUpdates.OnDisable(); + + euclideanEmbedding.dirty = false; + + // Discard all queued graph updates. Graph updates that are already in progress will still be allowed to finish, + // as they may be allocating unmanaged data which we don't know how to safely deallocate. + graphUpdates.DiscardQueued(); + + // TODO: Add unit test that verifies that work items that are added will always complete + // Ensure work items complete before disabling this component. + // This is important because work items may allocate temporary unmanaged memory, so we cannot just forget about them. + FlushWorkItems(); + + if (logPathResults == PathLog.Heavy) + Debug.Log("Processing Possible Work Items"); + + // Try to join pathfinding threads + pathProcessor.StopThreads(); + + if (logPathResults == PathLog.Heavy) + Debug.Log("Returning Paths"); + + + // Return all paths + pathReturnQueue.ReturnPaths(false); + graphLock.Release(); + euclideanEmbedding.OnDisable(); + } + + bool hasScannedGraphAtStartup = false; + + /// <summary> + /// Called after this component is enabled. + /// + /// Unless the component has already been activated in Awake, this method should: + /// - Ensure the singleton holds (setting <see cref="active"/> to this). + /// - Make sure all subsystems that were disabled in OnDisable are again enabled. + /// - This includes starting pathfinding threads. + /// </summary> + void OnEnable () { + // If the component gets re-enabled during runtime. + // Note that the first time the component loads, then Awake will run first + // and will already have set the #active field. + // In the editor, OnDisable -> OnEnable will be called when an undo or redo event happens (both in and outside of play mode). + if (active != null) { + if (active != this && Application.isPlaying) { + if (this.enabled) { + Debug.LogWarning("Another A* component is already in the scene. More than one A* component cannot be active at the same time. Disabling this one.", this); + } + enabled = false; + } + return; + } + + // Very important to set this. Ensures the singleton pattern holds + active = this; + + // Disable GUILayout to gain some performance, it is not used in the OnGUI call + useGUILayout = false; + + if (OnAwakeSettings != null) { + OnAwakeSettings(); + } + + hierarchicalGraph.OnEnable(); + + // To make sure all graph modifiers have been enabled before scan (to avoid script execution order issues) + GraphModifier.FindAllModifiers(); + RelevantGraphSurface.FindAllGraphSurfaces(); + + ConfigureReferencesInternal(); + + // This will load the graph settings, or whole initialized graphs from the cache, if one has been supplied. + data.OnEnable(); + + // Flush work items, possibly added when loading the graph data + FlushWorkItems(); + + euclideanEmbedding.dirty = true; + + InitializePathProcessor(); + + // This class uses the [ExecuteInEditMode] attribute + // So OnEnable is called even when not playing + // Don't scan the graphs unless we are in play mode + if (Application.isPlaying) { + navmeshUpdates.OnEnable(); + + // Scan the graphs if #scanOnStartup is enabled, and we have not loaded a graph cache already. + // We only do this the first time the AstarPath component is enabled. + if (scanOnStartup && !hasScannedGraphAtStartup && (!data.cacheStartup || data.file_cachedStartup == null)) { + hasScannedGraphAtStartup = true; + Scan(); + } + } + } + + + /// <summary> + /// Cleans up graphs to avoid memory leaks. + /// + /// This is called by Unity when: + /// - The component is explicitly disabled in play mode or editor mode. + /// - When the component is about to be destroyed + /// - Including when the game stops + /// - When an undo/redo event takes place (Unity will first disable the component and then enable it again). + /// + /// During edit and play mode this method should: + /// - Destroy all node data (but not the graphs themselves) + /// - Dispose all unmanaged data + /// - Shutdown pathfinding threads if they are running (any pending path requests are left in the queue) + /// </summary> + void OnDisable () { + redrawScope.Dispose(); + if (active == this) { + // Ensure there are no jobs running that might read or write graph data + graphDataLock.WriteSync().Unlock(); + + ShutdownPathfindingThreads(); + + // We need to call dispose data here because in the editor the OnDestroy + // method is not called but OnDisable is. It is vital that graph data + // is destroyed even in the editor (e.g. when going from edit mode to play mode) + // because a lot of data is stored as NativeArrays which need to be disposed. + + // There is also another case where this is important. When the unity + // editor is configured to stop play mode after recompiling scripts + // it seems to not call OnDestroy (or at least not reliably across all versions of Unity). + // So we need to ensure we dispose of all the data during OnDisable. + data.DestroyAllNodes(); + data.DisposeUnmanagedData(); + hierarchicalGraph.OnDisable(); + nodeStorage.OnDisable(); + offMeshLinks.OnDisable(); + active = null; + } + } + + /// <summary> + /// Clears up variables and other stuff, destroys graphs. + /// Note that when destroying an AstarPath object, all static variables such as callbacks will be cleared. + /// </summary> + void OnDestroy () { + if (logPathResults == PathLog.Heavy) + Debug.Log("AstarPath Component Destroyed - Cleaning Up Pathfinding Data"); + + // active has already been set to null during OnDisable. + // We temporarily make this object the active one just during the destruction. + var prevActive = active; + active = this; + + ShutdownPathfindingThreads(); + + pathProcessor.Dispose(); + + if (logPathResults == PathLog.Heavy) + Debug.Log("Destroying Graphs"); + + // Clean up graph data + // Data may be null if this object was never enabled because another A* instance existed. + if (data != null) data.OnDestroy(); + + active = prevActive; + + if (logPathResults == PathLog.Heavy) + Debug.Log("Cleaning up variables"); + + // Clear variables up, static variables are good to clean up, otherwise the next scene might get weird data + + if (active == this) { + // Clear all callbacks + OnAwakeSettings = null; + OnGraphPreScan = null; + OnGraphPostScan = null; + OnPathPreSearch = null; + OnPathPostSearch = null; + OnPreScan = null; + OnPostScan = null; + OnLatePostScan = null; + On65KOverflow = null; + OnGraphsUpdated = null; + + active = null; + } + } + + #region ScanMethods + + /// <summary> + /// Allocate a bunch of nodes at once. + /// This is faster than allocating each individual node separately and it can be done in a separate thread by using jobs. + /// + /// <code> + /// var nodes = new PointNode[128]; + /// var job = AstarPath.active.AllocateNodes(nodes, 128, () => new PointNode(), 1); + /// + /// job.Complete(); + /// </code> + /// + /// See: <see cref="InitializeNode"/> + /// </summary> + /// <param name="result">Node array to fill</param> + /// <param name="count">How many nodes to allocate</param> + /// <param name="createNode">Delegate which creates a node. () => new T(). Note that new T(AstarPath.active) should *not* be used as that will cause the node to be initialized twice.</param> + /// <param name="variantsPerNode">How many variants of the node to allocate. Should be the same as \reflink{GraphNode.PathNodeVariants} for this node type.</param> + public Unity.Jobs.JobHandle AllocateNodes<T>(T[] result, int count, System.Func<T> createNode, uint variantsPerNode) where T : GraphNode { + if (!pathProcessor.queue.allReceiversBlocked) { + throw new System.Exception("Trying to initialize a node when it is not safe to initialize any nodes. Must be done during a graph update. See http://arongranberg.com/astar/docs/graph-updates.html#direct"); + } + return nodeStorage.AllocateNodesJob(result, count, createNode, variantsPerNode); + } + + /// <summary> + /// Initializes temporary path data for a node. + /// + /// Use like: InitializeNode(new PointNode()) + /// + /// See: <see cref="AstarPath.AllocateNodes"/> + /// </summary> + internal void InitializeNode (GraphNode node) { + if (!pathProcessor.queue.allReceiversBlocked) { + throw new System.Exception("Trying to initialize a node when it is not safe to initialize any nodes. Must be done during a graph update. See http://arongranberg.com/astar/docs/graph-updates.html#direct"); + } + nodeStorage.InitializeNode(node); + } + + internal void InitializeNodes (GraphNode[] nodes) { + if (!pathProcessor.queue.allReceiversBlocked) { + throw new System.Exception("Trying to initialize a node when it is not safe to initialize any nodes. Must be done during a graph update. See http://arongranberg.com/astar/docs/graph-updates.html#direct"); + } + + for (int i = 0; i < nodes.Length; i++) nodeStorage.InitializeNode(nodes[i]); + } + + /// <summary> + /// Internal method to destroy a given node. + /// This is to be called after the node has been disconnected from the graph so that it cannot be reached from any other nodes. + /// It should only be called during graph updates, that is when the pathfinding threads are either not running or paused. + /// + /// Warning: This method should not be called by user code. It is used internally by the system. + /// </summary> + internal void DestroyNode (GraphNode node) { + nodeStorage.DestroyNode(node); + } + + /// <summary> + /// Blocks until all pathfinding threads are paused and blocked. + /// + /// <code> + /// var graphLock = AstarPath.active.PausePathfinding(); + /// // Here we can modify the graphs safely. For example by increasing the penalty of a node + /// AstarPath.active.data.gridGraph.GetNode(0, 0).Penalty += 1000; + /// + /// // Allow pathfinding to resume + /// graphLock.Release(); + /// </code> + /// + /// Returns: A lock object. You need to call <see cref="Pathfinding.PathProcessor.GraphUpdateLock.Release"/> on that object to allow pathfinding to resume. + /// Note: In most cases this should not be called from user code. Use the <see cref="AddWorkItem"/> method instead. + /// + /// See: <see cref="AddWorkItem"/> + /// </summary> + public PathProcessor.GraphUpdateLock PausePathfinding () { + // Ensure there are no jobs running that might read or write graph data, + // as this method is typically used right before one modifies graph data. + graphDataLock.WriteSync().Unlock(); + return pathProcessor.PausePathfinding(true); + } + + /// <summary> + /// Blocks the path queue so that e.g work items can be performed. + /// + /// Pathfinding threads will stop accepting new path requests and will finish the ones they are currently calculating asynchronously. + /// When the lock is released, the pathfinding threads will resume as normal. + /// + /// Note: You are unlikely to need to use this method. It is primarily for internal use. + /// </summary> + public PathProcessor.GraphUpdateLock PausePathfindingSoon () { + return pathProcessor.PausePathfinding(false); + } + + /// <summary> + /// Scans a particular graph. + /// Calling this method will recalculate the specified graph from scratch. + /// This method is pretty slow (depending on graph type and graph complexity of course), so it is advisable to use + /// smaller graph updates whenever possible. + /// + /// <code> + /// // Recalculate all graphs + /// AstarPath.active.Scan(); + /// + /// // Recalculate only the first grid graph + /// var graphToScan = AstarPath.active.data.gridGraph; + /// AstarPath.active.Scan(graphToScan); + /// + /// // Recalculate only the first and third graphs + /// var graphsToScan = new [] { AstarPath.active.data.graphs[0], AstarPath.active.data.graphs[2] }; + /// AstarPath.active.Scan(graphsToScan); + /// </code> + /// + /// See: graph-updates (view in online documentation for working links) + /// See: ScanAsync + /// </summary> + public void Scan (NavGraph graphToScan) { + if (graphToScan == null) throw new System.ArgumentNullException(); + Scan(new NavGraph[] { graphToScan }); + } + + /// <summary> + /// Scans all specified graphs. + /// + /// Calling this method will recalculate all specified graphs (or all graphs if the graphsToScan parameter is null) from scratch. + /// This method is pretty slow (depending on graph type and graph complexity of course), so it is advisable to use + /// smaller graph updates whenever possible. + /// + /// <code> + /// // Recalculate all graphs + /// AstarPath.active.Scan(); + /// + /// // Recalculate only the first grid graph + /// var graphToScan = AstarPath.active.data.gridGraph; + /// AstarPath.active.Scan(graphToScan); + /// + /// // Recalculate only the first and third graphs + /// var graphsToScan = new [] { AstarPath.active.data.graphs[0], AstarPath.active.data.graphs[2] }; + /// AstarPath.active.Scan(graphsToScan); + /// </code> + /// + /// See: graph-updates (view in online documentation for working links) + /// See: ScanAsync + /// </summary> + /// <param name="graphsToScan">The graphs to scan. If this parameter is null then all graphs will be scanned</param> + public void Scan (NavGraph[] graphsToScan = null) { + var prevStage = (ScanningStage)(-1); + + Profiler.BeginSample("Scan"); + Profiler.BeginSample("Init"); + foreach (var p in ScanInternal(graphsToScan, false)) { + if (prevStage != p.stage) { + Profiler.EndSample(); + Profiler.BeginSample(p.stage.ToString()); +#if !NETFX_CORE && UNITY_EDITOR + // Log progress to the console + System.Console.WriteLine(p.stage); +#endif + prevStage = p.stage; + } + } + Profiler.EndSample(); + Profiler.EndSample(); + } + + /// <summary> + /// Scans a particular graph asynchronously. This is a IEnumerable, you can loop through it to get the progress + /// + /// You can scan graphs asyncronously by yielding when you iterate through the returned IEnumerable. + /// Note that this does not guarantee a good framerate, but it will allow you + /// to at least show a progress bar while scanning. + /// + /// <code> + /// IEnumerator Start () { + /// foreach (Progress progress in AstarPath.active.ScanAsync()) { + /// Debug.Log("Scanning... " + progress.ToString()); + /// yield return null; + /// } + /// } + /// </code> + /// + /// See: Scan + /// </summary> + public IEnumerable<Progress> ScanAsync (NavGraph graphToScan) { + if (graphToScan == null) throw new System.ArgumentNullException(); + return ScanAsync(new NavGraph[] { graphToScan }); + } + + /// <summary> + /// Scans all specified graphs asynchronously. This is a IEnumerable, you can loop through it to get the progress + /// + /// You can scan graphs asyncronously by yielding when you loop through the progress. + /// Note that this does not guarantee a good framerate, but it will allow you + /// to at least show a progress bar during scanning. + /// + /// <code> + /// IEnumerator Start () { + /// foreach (Progress progress in AstarPath.active.ScanAsync()) { + /// Debug.Log("Scanning... " + progress.ToString()); + /// yield return null; + /// } + /// } + /// </code> + /// + /// Note: If the graphs are already scanned, doing an async scan will temporarily cause increased memory usage, since two copies of the graphs will be kept in memory during the async scan. + /// This may not be desirable on some platforms. A non-async scan will not cause this temporary increased memory usage. + /// + /// See: Scan + /// </summary> + /// <param name="graphsToScan">The graphs to scan. If this parameter is null then all graphs will be scanned</param> + public IEnumerable<Progress> ScanAsync (NavGraph[] graphsToScan = null) { + return ScanInternal(graphsToScan, true); + } + + class DummyGraphUpdateContext : IGraphUpdateContext { + public void DirtyBounds (Bounds bounds) {} + } + + IEnumerable<Progress> ScanInternal (NavGraph[] graphsToScan, bool async) { + if (graphsToScan == null) graphsToScan = graphs; + + if (graphsToScan == null || graphsToScan.Length == 0) { + yield break; + } + + if (isScanning) throw new System.InvalidOperationException("Another async scan is already running"); + + // Guard to ensure the A* object is always enabled if the graphs have any valid data. + // This is because otherwise the OnDisable method will not be called and some unmanaged data + // in NativeArrays may end up leaking. + if (!enabled) throw new System.InvalidOperationException("The AstarPath object must be enabled to scan graphs"); + if (active != this) throw new System.InvalidOperationException("The AstarPath object is not enabled in a scene"); + + isScanning = true; + + VerifyIntegrity(); + + var graphUpdateLock = PausePathfinding(); + + // Make sure all paths that are in the queue to be returned + // are returned immediately + // Some modifiers (e.g the funnel modifier) rely on + // the nodes being valid when the path is returned + pathReturnQueue.ReturnPaths(false); + + // Ensure all graph updates that are in progress get completed immediately. + // Graph updates that are in progress may use graph data, and we don't want to re-scan the graphs under their feet. + workItems.ProcessWorkItemsForScan(true); + + if (!Application.isPlaying) { + data.FindGraphTypes(); + GraphModifier.FindAllModifiers(); + } + + + yield return new Progress(0.05F, ScanningStage.PreProcessingGraphs); + + + { + var writeLock2 = graphDataLock.WriteSync(); + if (OnPreScan != null) { + OnPreScan(this); + } + + GraphModifier.TriggerEvent(GraphModifier.EventType.PreScan); + GraphModifier.TriggerEvent(GraphModifier.EventType.PreUpdate); + writeLock2.Unlock(); + } + + data.LockGraphStructure(); + + // Make sure the physics engine data is up to date. + // Scanning graphs may use physics methods and it is very confusing if they + // do not always pick up the latest changes made to the scene. + Physics.SyncTransforms(); + Physics2D.SyncTransforms(); + + var watch = System.Diagnostics.Stopwatch.StartNew(); + + // Destroy previous nodes, unless we are doing an async scan. + // We do not want the graphs to be in an invalid state during the async scan, + // so we cannot eagerly destroy them here. + // This means that during an async scan we may have two copies of the graphs in memory. + // Most of the data will be destroyed at the end of the async scan, but some memory will + // still be reserved. So a non-async scan is more memory efficient. + if (!async) { + var writeLock2 = graphDataLock.WriteSync(); + Profiler.BeginSample("Destroy previous nodes"); + for (int i = 0; i < graphsToScan.Length; i++) { + if (graphsToScan[i] != null) { + ((IGraphInternals)graphsToScan[i]).DestroyAllNodes(); + } + } + Profiler.EndSample(); + writeLock2.Unlock(); + } + + if (OnGraphPreScan != null) { + var writeLock2 = graphDataLock.WriteSync(); + for (int i = 0; i < graphsToScan.Length; i++) { + if (graphsToScan[i] != null) OnGraphPreScan(graphsToScan[i]); + } + writeLock2.Unlock(); + } + + // Loop through all graphs and start scanning them + var promises = new IGraphUpdatePromise[graphsToScan.Length]; + var iterators = new IEnumerator<JobHandle>[graphsToScan.Length]; + for (int i = 0; i < graphsToScan.Length; i++) { + if (graphsToScan[i] != null) { + promises[i] = ((IGraphInternals)graphsToScan[i]).ScanInternal(async); + iterators[i] = promises[i].Prepare(); + } + } + + // Scan all graphs concurrently by progressing all scanning iterators. + // If the graphs use the job system internally (like the grid, recast and navmesh graphs), + // then multiple graphs will even be scanned in parallel. + var it = ProgressScanningIteratorsConcurrently(iterators, promises, async); + while (true) { + try { + if (!it.MoveNext()) break; + } catch { + isScanning = false; + data.UnlockGraphStructure(); + graphUpdateLock.Release(); + throw; + } + yield return it.Current.MapTo(0.1f, 0.8f); + } + + yield return new Progress(0.95f, ScanningStage.FinishingScans); + + // Now we proceed with the last step of each graph's scanning process + // This part will make the results of the scan visible to the rest of the game. + // As a consequence, we must make sure to *not* yield anymore after this point, + // since that would make the rest of the game run while the graphs may be in an invalid state. + var writeLock = graphDataLock.WriteSync(); + + var ctx = new DummyGraphUpdateContext(); + for (int i = 0; i < promises.Length; i++) { + try { + if (promises[i] != null) { + Profiler.BeginSample("Finalizing " + graphsToScan[i].GetType().Name); + promises[i].Apply(ctx); + Profiler.EndSample(); + } + } catch { + isScanning = false; + data.UnlockGraphStructure(); + graphUpdateLock.Release(); + writeLock.Unlock(); + throw; + } + } + + for (int i = 0; i < graphsToScan.Length; i++) { + if (graphsToScan[i] != null) { + if (OnGraphPostScan != null) { + OnGraphPostScan(graphsToScan[i]); + } + // Notify the off mesh links subsystem that graphs have been recalculated, and we may need to recalculate off mesh links. + // But skip this for the link graph, since that's the graph that holds the off mesh link nodes themselves. + if (!(graphsToScan[i] is LinkGraph)) offMeshLinks.DirtyBounds(graphsToScan[i].bounds); + } + } + + // Unlock the graph structure here so that e.g. off-mesh-links can add the point graph required for them to work + data.UnlockGraphStructure(); + + // Graph Modifiers and the OnGraphsUpdated callback may modify graphs arbitrarily, so this also needs to be inside the write lock + if (OnPostScan != null) { + OnPostScan(this); + } + GraphModifier.TriggerEvent(GraphModifier.EventType.PostScan); + + // This lock may not be held if there are no work items pending + if (workItemLock.Held) { + Profiler.BeginSample("Work Items"); + // Note that this never sends PostUpdate (or similar) events. Those are sent below instead. + workItems.ProcessWorkItemsForScan(true); + Profiler.EndSample(); + workItemLock.Release(); + } + + offMeshLinks.Refresh(); + + GraphModifier.TriggerEvent(GraphModifier.EventType.PostUpdateBeforeAreaRecalculation); + + // Recalculate connected components synchronously + hierarchicalGraph.RecalculateIfNecessary(); + + // Scanning a graph *is* a type of update + GraphModifier.TriggerEvent(GraphModifier.EventType.PostUpdate); + if (OnGraphsUpdated != null) OnGraphsUpdated(this); + + // Signal that we have stopped scanning here + isScanning = false; + + if (OnLatePostScan != null) OnLatePostScan(this); + GraphModifier.TriggerEvent(GraphModifier.EventType.LatePostScan); + + writeLock.Unlock(); + + euclideanEmbedding.dirty = true; + euclideanEmbedding.RecalculatePivots(); + + // Perform any blocking actions + FlushWorkItems(); + // Resume pathfinding threads + graphUpdateLock.Release(); + + watch.Stop(); + lastScanTime = (float)watch.Elapsed.TotalSeconds; + + if (logPathResults != PathLog.None && logPathResults != PathLog.OnlyErrors) { + Debug.Log("Scanned graphs in " + (lastScanTime*1000).ToString("0") + " ms"); + } + } + + internal static IEnumerator<Progress> ProgressScanningIteratorsConcurrently (IEnumerator<JobHandle>[] iterators, IGraphUpdatePromise[] promises, bool async) { + while (true) { + int firstNonFinished = -1; + bool mainThreadWork = false; + for (int i = 0; i < iterators.Length; i++) { + var it = iterators[i]; + if (it == null) continue; + if (async) { + if (it.Current.IsCompleted) { + // If the job completed (maybe because a real job completed, or because the iterator returned a dummy JobHandle), then it must be doing some work on the main thread. + // In that case, we shouldn't sleep or yield while waiting. + mainThreadWork = true; + it.Current.Complete(); + } else { + if (firstNonFinished == -1) firstNonFinished = i; + continue; + } + } else { + it.Current.Complete(); + } + + Profiler.BeginSample("Preparing"); + if (it.MoveNext()) { + if (firstNonFinished == -1) firstNonFinished = i; + } else iterators[i] = null; + Profiler.EndSample(); + } + + if (firstNonFinished != -1) { + if (async) { + // If main thread work is happening, then we are ok with progressing the iterators as often as possible + if (!mainThreadWork) { + // Ensure that we won't be completely busy spinning if the user waits on an async scan in a tight loop + System.Threading.Thread.Yield(); + } + + // Just used for progress information + // This graph will advance the progress bar from minp to maxp + float minp = (float)firstNonFinished/iterators.Length; + float maxp = (float)(firstNonFinished+0.95F)/iterators.Length; + yield return new Progress(Mathf.Lerp(minp, maxp, promises[firstNonFinished].Progress), ScanningStage.ScanningGraph, firstNonFinished, iterators.Length); + } + } else { + break; + } + } + } + + #endregion + + internal void DirtyBounds (Bounds bounds) { + offMeshLinks.DirtyBounds(bounds); + workItems.DirtyGraphs(); + } + + private static int waitForPathDepth = 0; + + /// <summary> + /// Blocks until the path has been calculated. + /// + /// Normally it takes a few frames for a path to be calculated and returned. + /// This function will ensure that the path will be calculated when this function returns + /// and that the callback for that path has been called. + /// + /// If requesting a lot of paths in one go and waiting for the last one to complete, + /// it will calculate most of the paths in the queue (only most if using multithreading, all if not using multithreading). + /// + /// Use this function only if you really need to. + /// There is a point to spreading path calculations out over several frames. + /// It smoothes out the framerate and makes sure requesting a large + /// number of paths at the same time does not cause lag. + /// + /// Note: Graph updates and other callbacks might get called during the execution of this function. + /// + /// When the pathfinder is shutting down. I.e in OnDestroy, this function will not do anything. + /// + /// Throws: Exception if pathfinding is not initialized properly for this scene (most likely no AstarPath object exists) + /// or if the path has not been started yet. + /// Also throws an exception if critical errors occur such as when the pathfinding threads have crashed (which should not happen in normal cases). + /// This prevents an infinite loop while waiting for the path. + /// + /// See: Pathfinding.Path.WaitForPath + /// See: Pathfinding.Path.BlockUntilCalculated + /// </summary> + /// <param name="path">The path to wait for. The path must be started, otherwise an exception will be thrown.</param> + public static void BlockUntilCalculated (Path path) { + if (active == null) + throw new System.Exception("Pathfinding is not correctly initialized in this scene (yet?). " + + "AstarPath.active is null.\nDo not call this function in Awake"); + + if (path == null) throw new System.ArgumentNullException(nameof(path)); + + if (active.pathProcessor.queue.isClosed) return; + + if (path.PipelineState == PathState.Created) { + throw new System.Exception("The specified path has not been started yet."); + } + + waitForPathDepth++; + + if (waitForPathDepth == 5) { + Debug.LogError("You are calling the BlockUntilCalculated function recursively (maybe from a path callback). Please don't do this."); + } + + if (path.PipelineState < PathState.ReturnQueue) { + if (active.IsUsingMultithreading) { + while (path.PipelineState < PathState.ReturnQueue) { + if (active.pathProcessor.queue.isClosed) { + waitForPathDepth--; + throw new System.Exception("Pathfinding Threads seem to have crashed."); + } + + // Wait for threads to calculate paths + Thread.Sleep(1); + active.PerformBlockingActions(true); + } + } else { + while (path.PipelineState < PathState.ReturnQueue) { + if (active.pathProcessor.queue.isEmpty && path.PipelineState != PathState.Processing) { + waitForPathDepth--; + throw new System.Exception("Critical error. Path Queue is empty but the path state is '" + path.PipelineState + "'"); + } + + // Calculate some paths + active.pathProcessor.TickNonMultithreaded(); + active.PerformBlockingActions(true); + } + } + } + + active.pathReturnQueue.ReturnPaths(false); + waitForPathDepth--; + } + + /// <summary> + /// Adds the path to a queue so that it will be calculated as soon as possible. + /// The callback specified when constructing the path will be called when the path has been calculated. + /// Usually you should use the Seeker component instead of calling this function directly. + /// + /// <code> + /// // There must be an AstarPath instance in the scene + /// if (AstarPath.active == null) return; + /// + /// // We can calculate multiple paths asynchronously + /// for (int i = 0; i < 10; i++) { + /// var path = ABPath.Construct(transform.position, transform.position+transform.forward*i*10, OnPathComplete); + /// + /// // Calculate the path by using the AstarPath component directly + /// AstarPath.StartPath(path); + /// } + /// </code> + /// </summary> + /// <param name="path">The path that should be enqueued.</param> + /// <param name="pushToFront">If true, the path will be pushed to the front of the queue, bypassing all waiting paths and making it the next path to be calculated. + /// This can be useful if you have a path which you want to prioritize over all others. Be careful to not overuse it though. + /// If too many paths are put in the front of the queue often, this can lead to normal paths having to wait a very long time before being calculated.</param> + /// <param name="assumeInPlayMode">Typically path.BlockUntilCalculated will be called when not in play mode. However, the play mode check will not work if + /// you call this from a separate thread, or a job. In that case you can set this to true to skip the check.</param> + public static void StartPath (Path path, bool pushToFront = false, bool assumeInPlayMode = false) { + // Copy to local variable to avoid multithreading issues + var astar = active; + + if (System.Object.ReferenceEquals(astar, null)) { + Debug.LogError("There is no AstarPath object in the scene or it has not been initialized yet"); + return; + } + + if (path.PipelineState != PathState.Created) { + throw new System.Exception("The path has an invalid state. Expected " + PathState.Created + " found " + path.PipelineState + "\n" + + "Make sure you are not requesting the same path twice"); + } + + if (astar.pathProcessor.queue.isClosed) { + path.FailWithError("No new paths are accepted"); + return; + } + + if (astar.graphs == null || astar.graphs.Length == 0) { + Debug.LogError("There are no graphs in the scene"); + path.FailWithError("There are no graphs in the scene"); + Debug.LogError(path.errorLog); + return; + } + + path.Claim(astar); + + // Will increment p.state to PathState.PathQueue + ((IPathInternals)path).AdvanceState(PathState.PathQueue); + if (pushToFront) { + astar.pathProcessor.queue.PushFront(path); + } else { + astar.pathProcessor.queue.Push(path); + } + + // Outside of play mode, all path requests are synchronous. + // However, inside a job we cannot check this, because Unity will throw an exception. + // But luckily pretty much all jobs will run in game mode anyway. So we assume that if we are in a job, we are in game mode. + if (!assumeInPlayMode && !Unity.Jobs.LowLevel.Unsafe.JobsUtility.IsExecutingJob && !Application.isPlaying) { + BlockUntilCalculated(path); + } + } + + /// <summary> + /// Cached NNConstraint.None to avoid unnecessary allocations. + /// This should ideally be fixed by making NNConstraint an immutable class/struct. + /// </summary> + static readonly NNConstraint NNConstraintNone = NNConstraint.None; + + /// <summary> + /// Returns the nearest node to a position. + /// This method will search through all graphs and query them for the closest node to this position, and then it will return the closest one of those. + /// + /// Equivalent to GetNearest(position, NNConstraint.None). + /// + /// <code> + /// // Find the closest node to this GameObject's position + /// GraphNode node = AstarPath.active.GetNearest(transform.position).node; + /// + /// if (node.Walkable) { + /// // Yay, the node is walkable, we can place a tower here or something + /// } + /// </code> + /// + /// See: Pathfinding.NNConstraint + /// </summary> + public NNInfo GetNearest (Vector3 position) { + return GetNearest(position, null); + } + + /// <summary> + /// Returns the nearest node to a point using the specified NNConstraint. + /// + /// Searches through all graphs for their nearest nodes to the specified position and picks the closest one. + /// The NNConstraint can be used to specify constraints on which nodes can be chosen such as only picking walkable nodes. + /// + /// <code> + /// GraphNode node = AstarPath.active.GetNearest(transform.position, NNConstraint.Walkable).node; + /// </code> + /// + /// <code> + /// var constraint = NNConstraint.None; + /// + /// // Constrain the search to walkable nodes only + /// constraint.constrainWalkability = true; + /// constraint.walkable = true; + /// + /// // Constrain the search to only nodes with tag 3 or tag 5 + /// // The 'tags' field is a bitmask + /// constraint.constrainTags = true; + /// constraint.tags = (1 << 3) | (1 << 5); + /// + /// var info = AstarPath.active.GetNearest(transform.position, constraint); + /// var node = info.node; + /// var closestPoint = info.position; + /// </code> + /// + /// See: <see cref="NNConstraint"/> + /// </summary> + /// <param name="position">The point to find nodes close to</param> + /// <param name="constraint">The constraint which determines which graphs and nodes are acceptable to search on. May be null, in which case all nodes will be considered acceptable.</param> + public NNInfo GetNearest (Vector3 position, NNConstraint constraint) { + // Cache property lookups + var graphs = this.graphs; + var maxNearestNodeDistanceSqr = constraint == null || constraint.constrainDistance ? this.maxNearestNodeDistanceSqr : float.PositiveInfinity; + NNInfo nearestNode = NNInfo.Empty; + + if (graphs == null || graphs.Length == 0) return nearestNode; + + // Use a fast path in case there is only one graph. + // This improves performance by about 10% when there is only one graph. + if (graphs.Length == 1) { + var graph = graphs[0]; + if (graph == null || (constraint != null && !constraint.SuitableGraph(0, graph))) { + return nearestNode; + } + + nearestNode = graph.GetNearest(position, constraint, maxNearestNodeDistanceSqr); + UnityEngine.Assertions.Assert.IsTrue(nearestNode.node == null || nearestNode.distanceCostSqr <= maxNearestNodeDistanceSqr); + } else { + UnsafeSpan<(float, int)> distances; + unsafe { + // The number of graphs is limited to GraphNode.MaxGraphIndex (256), + // and typically there are only a few graphs, so allocating this on the stack is fine. + var distancesPtr = stackalloc (float, int)[graphs.Length]; + distances = new UnsafeSpan<(float, int)>(distancesPtr, graphs.Length); + } + + // Iterate through all graphs and find a lower bound on the distance to the nearest node. + // We then sort these distances and run the full get nearest search on the graphs in order of increasing distance. + // This is an optimization to avoid running the full get nearest search on graphs which are far away. + int numCandidateGraphs = 0; + for (int i = 0; i < graphs.Length; i++) { + NavGraph graph = graphs[i]; + + // Check if this graph should be searched + if (graph == null || (constraint != null && !constraint.SuitableGraph(i, graph))) { + continue; + } + var lowerBound = graph.NearestNodeDistanceSqrLowerBound(position, constraint); + if (lowerBound > maxNearestNodeDistanceSqr) continue; + + distances[numCandidateGraphs++] = (lowerBound, i); + } + distances = distances.Slice(0, numCandidateGraphs); + distances.Sort(); + for (int i = 0; i < distances.Length; i++) { + if (distances[i].Item1 > maxNearestNodeDistanceSqr) break; + var graph = graphs[distances[i].Item2]; + NNInfo nnInfo = graph.GetNearest(position, constraint, maxNearestNodeDistanceSqr); + if (nnInfo.distanceCostSqr < maxNearestNodeDistanceSqr) { + maxNearestNodeDistanceSqr = nnInfo.distanceCostSqr; + nearestNode = nnInfo; + } + } + } + return nearestNode; + } + + /// <summary> + /// Returns the node closest to the ray (slow). + /// Warning: This function is brute-force and very slow, use with caution + /// </summary> + public GraphNode GetNearest (Ray ray) { + if (graphs == null) return null; + + float minDist = Mathf.Infinity; + GraphNode nearestNode = null; + + Vector3 lineDirection = ray.direction; + Vector3 lineOrigin = ray.origin; + + for (int i = 0; i < graphs.Length; i++) { + NavGraph graph = graphs[i]; + + graph.GetNodes(node => { + Vector3 pos = (Vector3)node.position; + Vector3 p = lineOrigin+(Vector3.Dot(pos-lineOrigin, lineDirection)*lineDirection); + + float tmp = Mathf.Abs(p.x-pos.x); + tmp *= tmp; + if (tmp > minDist) return; + + tmp = Mathf.Abs(p.z-pos.z); + tmp *= tmp; + if (tmp > minDist) return; + + float dist = (p-pos).sqrMagnitude; + + if (dist < minDist) { + minDist = dist; + nearestNode = node; + } + }); + } + + return nearestNode; + } + + /// <summary> + /// Captures a snapshot of a part of the graphs, to allow restoring it later. + /// + /// This is useful if you want to do a graph update, but you want to be able to restore the graph to the previous state. + /// + /// The snapshot will capture enough information to restore the graphs, assuming the world only changed within the given bounding box. + /// This means the captured region may be larger than the bounding box. + /// + /// <b>Limitations:</b> + /// - Currently, the <see cref="GridGraph"/> and <see cref="LayerGridGraph"/> supports snapshots. Other graph types do not support it. + /// - The graph must not change its dimensions or other core parameters between the time the snapshot is taken and the time it is restored. + /// - Custom node connections may not be preserved. Unless they are added as off-mesh links using e.g. a <see cref="NodeLink2"/> component. + /// - The snapshot must not be captured during a work item, graph update or when the graphs are being scanned, as the graphs may not be in a consistent state during those times. + /// + /// See: <see cref="GraphUpdateUtilities.UpdateGraphsNoBlock"/>, which uses this method internally. + /// See: <see cref="NavGraph.Snapshot"/> + /// + /// Note: You must dispose the returned snapshot when you are done with it, to avoid leaking memory. + /// </summary> + public GraphSnapshot Snapshot (Bounds bounds, GraphMask graphMask) { + Profiler.BeginSample("Capturing Graph Snapshot"); + var inner = new List<IGraphSnapshot>(); + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] != null && graphMask.Contains(i)) { + var s = graphs[i].Snapshot(bounds); + if (s != null) inner.Add(s); + } + } + Profiler.EndSample(); + return new GraphSnapshot(inner); + } + + /// <summary> + /// Allows you to access read-only graph data in jobs safely. + /// + /// You can for example use AstarPath.active.GetNearest(...) in a job. + /// + /// Using <see cref="AstarPath.StartPath"/> is always safe to use in jobs even without calling this method. + /// + /// When a graph update, work item, or graph scan would start, it will first block on the given dependency + /// to ensure no race conditions occur. + /// + /// If you do not call this method, then a graph update might start in the middle of your job, causing race conditions + /// and all manner of other hard-to-diagnose bugs. + /// + /// <code> + /// var readLock = AstarPath.active.LockGraphDataForReading(); + /// var handle = new MyJob { + /// // ... + /// }.Schedule(readLock.dependency); + /// readLock.UnlockAfter(handle); + /// </code> + /// + /// See: <see cref="LockGraphDataForWriting"/> + /// See: <see cref="graphDataLock"/> + /// </summary> + public RWLock.ReadLockAsync LockGraphDataForReading() => graphDataLock.Read(); + + /// <summary> + /// Aquires an exclusive lock on the graph data asynchronously. + /// This is used when graphs want to modify graph data. + /// + /// This is a low-level primitive, usually you do not need to use this method. + /// + /// <code> + /// var readLock = AstarPath.active.LockGraphDataForReading(); + /// var handle = new MyJob { + /// // ... + /// }.Schedule(readLock.dependency); + /// readLock.UnlockAfter(handle); + /// </code> + /// + /// See: <see cref="LockGraphDataForReading"/> + /// See: <see cref="graphDataLock"/> + /// </summary> + public RWLock.WriteLockAsync LockGraphDataForWriting() => graphDataLock.Write(); + + /// <summary> + /// Aquires an exclusive lock on the graph data. + /// This is used when graphs want to modify graph data. + /// + /// This is a low-level primitive, usually you do not need to use this method. + /// + /// <code> + /// var readLock = AstarPath.active.LockGraphDataForReading(); + /// var handle = new MyJob { + /// // ... + /// }.Schedule(readLock.dependency); + /// readLock.UnlockAfter(handle); + /// </code> + /// + /// See: <see cref="LockGraphDataForReading"/> + /// See: <see cref="graphDataLock"/> + /// </summary> + public RWLock.LockSync LockGraphDataForWritingSync() => graphDataLock.WriteSync(); + + /// <summary> + /// Obstacle data for navmesh edges. + /// + /// This can be used to get information about the edge/borders of the navmesh. + /// It can also be queried in burst jobs. Just make sure you release the read lock after you are done with it. + /// + /// Note: This is not a method that you are likely to need to use. + /// It is used internally for things like local avoidance. + /// </summary> + public NavmeshEdges.NavmeshBorderData GetNavmeshBorderData(out RWLock.CombinedReadLockAsync readLock) => hierarchicalGraph.navmeshEdges.GetNavmeshEdgeData(out readLock); +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarPath.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarPath.cs.meta new file mode 100644 index 0000000..f5172e5 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/AstarPath.cs.meta @@ -0,0 +1,16 @@ +fileFormatVersion: 2 +guid: 78396926cbbfc4ac3b48fc5fc34a87d1 +labels: +- Pathfinder +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - gizmoSurfaceMaterial: {fileID: 2100000, guid: 5ce51318bbfb1466188b929a68a6bd3a, + type: 2} + - gizmoLineMaterial: {fileID: 2100000, guid: 91035448860ba4e708919485c73f7edc, type: 2} + executionOrder: -10000 + icon: {fileID: 2800000, guid: 1620f833be5302149a071c06944d3e9b, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections.meta new file mode 100644 index 0000000..7bf7a0b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2b2120e89e2185f4bbaa86f8099157df +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/AstarMemory.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/AstarMemory.cs new file mode 100644 index 0000000..086e3cb --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/AstarMemory.cs @@ -0,0 +1,97 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace Pathfinding.Util { + /// <summary>Various utilities for handling arrays and memory</summary> + public static class Memory { + /// <summary> + /// Returns a new array with at most length newLength. + /// The array will contain a copy of all elements of arr up to but excluding the index newLength. + /// </summary> + public static T[] ShrinkArray<T>(T[] arr, int newLength) { + newLength = Math.Min(newLength, arr.Length); + var shrunkArr = new T[newLength]; + Array.Copy(arr, shrunkArr, newLength); + return shrunkArr; + } + + /// <summary>Swaps the variables a and b</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static void Swap<T>(ref T a, ref T b) { + T tmp = a; + + a = b; + b = tmp; + } + + public static void Realloc<T>(ref NativeArray<T> arr, int newSize, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory) where T : struct { + if (arr.IsCreated && arr.Length >= newSize) return; + + var newArr = new NativeArray<T>(newSize, allocator, options); + if (arr.IsCreated) { + // Copy over old data + NativeArray<T>.Copy(arr, newArr, arr.Length); + arr.Dispose(); + } + arr = newArr; + } + + public static void Realloc<T>(ref T[] arr, int newSize) { + if (arr == null) { + arr = new T[newSize]; + } else if (newSize > arr.Length) { + var newArr = new T[newSize]; + arr.CopyTo(newArr, 0); + arr = newArr; + } + } + + public static T[] UnsafeAppendBufferToArray<T>(UnsafeAppendBuffer src) where T : unmanaged { + var elementCount = src.Length / UnsafeUtility.SizeOf<T>(); + var dst = new T[elementCount]; + + unsafe { + var gCHandle = System.Runtime.InteropServices.GCHandle.Alloc(dst, System.Runtime.InteropServices.GCHandleType.Pinned); + System.IntPtr value = gCHandle.AddrOfPinnedObject(); + UnsafeUtility.MemCpy((byte*)(void*)value, src.Ptr, (long)elementCount * (long)UnsafeUtility.SizeOf<T>()); + gCHandle.Free(); + } + return dst; + } + + public static void Rotate3DArray<T>(T[] arr, int3 size, int dx, int dz) { + int width = size.x; + int height = size.y; + int depth = size.z; + dx = dx % width; + dz = dz % depth; + if (dx != 0) { + if (dx < 0) dx = width + dx; + var tmp = ArrayPool<T>.Claim(dx); + for (int y = 0; y < height; y++) { + var offset = y * width * depth; + for (int z = 0; z < depth; z++) { + Array.Copy(arr, offset + z * width + width - dx, tmp, 0, dx); + Array.Copy(arr, offset + z * width, arr, offset + z * width + dx, width - dx); + Array.Copy(tmp, 0, arr, offset + z * width, dx); + } + } + ArrayPool<T>.Release(ref tmp); + } + + if (dz != 0) { + if (dz < 0) dz = depth + dz; + var tmp = ArrayPool<T>.Claim(dz * width); + for (int y = 0; y < height; y++) { + var offset = y * width * depth; + Array.Copy(arr, offset + (depth - dz) * width, tmp, 0, dz * width); + Array.Copy(arr, offset, arr, offset + dz * width, (depth - dz) * width); + Array.Copy(tmp, 0, arr, offset, dz * width); + } + ArrayPool<T>.Release(ref tmp); + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/AstarMemory.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/AstarMemory.cs.meta new file mode 100644 index 0000000..2b61ad2 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/AstarMemory.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9bdecddfdfec947eb8ed96282e4b1fe1 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs new file mode 100644 index 0000000..d76a67d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs @@ -0,0 +1,359 @@ +// #define VALIDATE_BINARY_HEAP +#pragma warning disable 162 +#pragma warning disable 429 +using Unity.Mathematics; +using Unity.Collections; +using Unity.Burst; +using Unity.Burst.CompilerServices; + +namespace Pathfinding { + using Pathfinding.Util; + + /// <summary> + /// Binary heap implementation. + /// Binary heaps are really fast for ordering nodes in a way that + /// makes it possible to get the node with the lowest F score. + /// Also known as a priority queue. + /// + /// This has actually been rewritten as a 4-ary heap + /// for performance, but it's the same principle. + /// + /// See: http://en.wikipedia.org/wiki/Binary_heap + /// See: https://en.wikipedia.org/wiki/D-ary_heap + /// </summary> + [BurstCompile] + public struct BinaryHeap { + /// <summary>Number of items in the tree</summary> + public int numberOfItems; + + /// <summary>The tree will grow by at least this factor every time it is expanded</summary> + public const float GrowthFactor = 2; + + /// <summary> + /// Number of children of each node in the tree. + /// Different values have been tested and 4 has been empirically found to perform the best. + /// See: https://en.wikipedia.org/wiki/D-ary_heap + /// </summary> + const int D = 4; + + /// <summary> + /// Sort nodes by G score if there is a tie when comparing the F score. + /// Disabling this will improve pathfinding performance with around 2.5% + /// but may break ties between paths that have the same length in a less + /// desirable manner (only relevant for grid graphs). + /// </summary> + const bool SortGScores = true; + + public const ushort NotInHeap = 0xFFFF; + + /// <summary>Internal backing array for the heap</summary> + private UnsafeSpan<HeapNode> heap; + + /// <summary>True if the heap does not contain any elements</summary> + public bool isEmpty => numberOfItems <= 0; + + /// <summary>Item in the heap</summary> + private struct HeapNode { + public uint pathNodeIndex; + /// <summary>Bitpacked F and G scores</summary> + public ulong sortKey; + + public HeapNode (uint pathNodeIndex, uint g, uint f) { + this.pathNodeIndex = pathNodeIndex; + this.sortKey = ((ulong)f << 32) | (ulong)g; + } + + public uint F { + get => (uint)(sortKey >> 32); + set => sortKey = (sortKey & 0xFFFFFFFFUL) | ((ulong)value << 32); + } + + public uint G => (uint)sortKey; + } + + /// <summary> + /// Rounds up v so that it has remainder 1 when divided by D. + /// I.e it is of the form n*D + 1 where n is any non-negative integer. + /// </summary> + static int RoundUpToNextMultipleMod1 (int v) { + // I have a feeling there is a nicer way to do this + return v + (4 - ((v-1) % D)) % D; + } + + /// <summary>Create a new heap with the specified initial capacity</summary> + public BinaryHeap (int capacity) { + // Make sure the size has remainder 1 when divided by D + // This allows us to always guarantee that indices used in the Remove method + // will never throw out of bounds exceptions + capacity = RoundUpToNextMultipleMod1(capacity); + + heap = new UnsafeSpan<HeapNode>(Unity.Collections.Allocator.Persistent, capacity); + numberOfItems = 0; + } + + public void Dispose () { + unsafe { + AllocatorManager.Free<HeapNode>(Allocator.Persistent, heap.ptr, heap.Length); + } + } + + /// <summary>Removes all elements from the heap</summary> + public void Clear (UnsafeSpan<PathNode> pathNodes) { + // Clear all heap indices, and references to the heap nodes + for (int i = 0; i < numberOfItems; i++) { + pathNodes[heap[i].pathNodeIndex].heapIndex = NotInHeap; + } + + numberOfItems = 0; + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public uint GetPathNodeIndex(int heapIndex) => heap[heapIndex].pathNodeIndex; + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public uint GetG(int heapIndex) => heap[heapIndex].G; + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public uint GetF(int heapIndex) => heap[heapIndex].F; + + public void SetH (int heapIndex, uint h) { + heap[heapIndex].F = heap[heapIndex].G + h; + } + + /// <summary>Expands to a larger backing array when the current one is too small</summary> + static void Expand (ref UnsafeSpan<HeapNode> heap) { + // 65533 == 1 mod 4 and slightly smaller than 1<<16 = 65536 + int newSize = math.max(heap.Length+4, math.min(65533, (int)math.round(heap.Length*GrowthFactor))); + + // Make sure the size has remainder 1 when divided by D + // This allows us to always guarantee that indices used in the Remove method + // will never throw out of bounds exceptions + newSize = RoundUpToNextMultipleMod1(newSize); + + // Check if the heap is really large + // Also note that heaps larger than this are not supported + // since PathNode.heapIndex is a ushort and can only store + // values up to 65535 (NotInHeap = 65535 is reserved however) + if (newSize > (1<<16) - 2) { + throw new System.Exception("Binary Heap Size really large (>65534). A heap size this large is probably the cause of pathfinding running in an infinite loop. "); + } + + var newHeap = new UnsafeSpan<HeapNode>(Unity.Collections.Allocator.Persistent, newSize); + newHeap.CopyFrom(heap); + unsafe { + AllocatorManager.Free<HeapNode>(Allocator.Persistent, heap.ptr, heap.Length); + } +#if ASTARDEBUG + UnityEngine.Debug.Log("Resizing binary heap to "+newSize); +#endif + heap = newHeap; + } + + /// <summary>Adds a node to the heap</summary> + public void Add (UnsafeSpan<PathNode> nodes, uint pathNodeIndex, uint g, uint f) { + Add(ref this, ref nodes, pathNodeIndex, g, f); + } + + [BurstCompile] + static void Add (ref BinaryHeap binaryHeap, ref UnsafeSpan<PathNode> nodes, uint pathNodeIndex, uint g, uint f) { + ref var numberOfItems = ref binaryHeap.numberOfItems; + ref var heap = ref binaryHeap.heap; + + // Check if node is already in the heap + ref var node = ref nodes[pathNodeIndex]; + if (node.heapIndex != NotInHeap) { + var activeNode = new HeapNode(pathNodeIndex, g, f); + UnityEngine.Assertions.Assert.AreEqual(activeNode.pathNodeIndex, pathNodeIndex); + DecreaseKey(heap, nodes, activeNode, node.heapIndex); + Validate(ref nodes, ref binaryHeap); + } else { + if (numberOfItems == heap.Length) { + Expand(ref heap); + } + Validate(ref nodes, ref binaryHeap); + + DecreaseKey(heap, nodes, new HeapNode(pathNodeIndex, g, f), (ushort)numberOfItems); + numberOfItems++; + Validate(ref nodes, ref binaryHeap); + } + } + + static void DecreaseKey (UnsafeSpan<HeapNode> heap, UnsafeSpan<PathNode> nodes, HeapNode node, ushort index) { + // This is where 'obj' is in the binary heap logically speaking + // (for performance reasons we don't actually store it there until + // we know the final index, that's just a waste of CPU cycles) + uint bubbleIndex = index; + + while (bubbleIndex != 0) { + // Parent node of the bubble node + uint parentIndex = (bubbleIndex-1) / D; + + Hint.Assume(parentIndex < heap.length); + Hint.Assume(bubbleIndex < heap.length); + if (node.sortKey < heap[parentIndex].sortKey) { + // Swap the bubble node and parent node + // (we don't really need to store the bubble node until we know the final index though + // so we do that after the loop instead) + heap[bubbleIndex] = heap[parentIndex]; + nodes[heap[bubbleIndex].pathNodeIndex].heapIndex = (ushort)bubbleIndex; + bubbleIndex = parentIndex; + } else { + break; + } + } + + Hint.Assume(bubbleIndex < heap.length); + heap[bubbleIndex] = node; + nodes[node.pathNodeIndex].heapIndex = (ushort)bubbleIndex; + } + + /// <summary>Returns the node with the lowest F score from the heap</summary> + public uint Remove (UnsafeSpan<PathNode> nodes, out uint g, out uint f) { + return Remove(ref nodes, ref this, out g, out f); + } + + [BurstCompile] + static uint Remove (ref UnsafeSpan<PathNode> nodes, ref BinaryHeap binaryHeap, [NoAlias] out uint removedG, [NoAlias] out uint removedF) { + ref var numberOfItems = ref binaryHeap.numberOfItems; + var heap = binaryHeap.heap; + + if (numberOfItems == 0) { + throw new System.InvalidOperationException("Removing item from empty heap"); + } + + // This is the smallest item in the heap. + // Mark it as removed from the heap. + Hint.Assume(0UL < heap.length); + uint returnIndex = heap[0].pathNodeIndex; + nodes[returnIndex].heapIndex = NotInHeap; + removedG = heap[0].G; + removedF = heap[0].F; + + numberOfItems--; + if (numberOfItems == 0) { + return returnIndex; + } + + // Last item in the heap array + Hint.Assume((uint)numberOfItems < heap.length); + var swapItem = heap[numberOfItems]; + uint swapIndex = 0; + ulong comparisonKey = swapItem.sortKey; + + // Trickle upwards + while (true) { + var parent = swapIndex; + uint pd = parent * D + 1; + + // If this holds, then the indices used + // below are guaranteed to not throw an index out of bounds + // exception since we choose the size of the array in that way + if (pd < numberOfItems) { + // Find the child node with the smallest F score, or if equal, the smallest G score. + // The sorting key is the tuple (F,G). + // However, to be able to easily get the smallest index, we steal the lowest 2 bits of G + // and use it for the child index (0..3) instead. + // This means that tie-breaking will not be perfect, but in all practical cases it will + // yield exactly the same result since G scores typically differ by more than 4 anyway. + Hint.Assume(pd+0 < heap.length); + ulong l0 = (heap[pd+0].sortKey & ~0x3UL) | 0; + Hint.Assume(pd+1 < heap.length); + ulong l1 = (heap[pd+1].sortKey & ~0x3UL) | 1; + Hint.Assume(pd+2 < heap.length); + ulong l2 = (heap[pd+2].sortKey & ~0x3UL) | 2; + Hint.Assume(pd+3 < heap.length); + ulong l3 = (heap[pd+3].sortKey & ~0x3UL) | 3; + + ulong smallest = l0; + // Not all children may exist, so we need to check that the index is valid + if (pd+1 < numberOfItems) smallest = math.min(smallest, l1); + if (pd+2 < numberOfItems) smallest = math.min(smallest, l2); + if (pd+3 < numberOfItems) smallest = math.min(smallest, l3); + + if (smallest < comparisonKey) { + swapIndex = pd + (uint)(smallest & 0x3UL); + + // One if the parent's children are smaller or equal, swap them + // (actually we are just pretenting we swapped them, we hold the swapItem + // in local variable and only assign it once we know the final index) + Hint.Assume(parent < heap.length); + Hint.Assume(swapIndex < heap.length); + heap[parent] = heap[swapIndex]; + Hint.Assume(swapIndex < heap.length); + nodes[heap[swapIndex].pathNodeIndex].heapIndex = (ushort)parent; + } else { + break; + } + } else { + break; + } + } + + // Assign element to the final position + Hint.Assume(swapIndex < heap.length); + heap[swapIndex] = swapItem; + nodes[swapItem.pathNodeIndex].heapIndex = (ushort)swapIndex; + + // For debugging + Validate(ref nodes, ref binaryHeap); + + return returnIndex; + } + + [System.Diagnostics.Conditional("VALIDATE_BINARY_HEAP")] + static void Validate (ref UnsafeSpan<PathNode> nodes, ref BinaryHeap binaryHeap) { + for (int i = 1; i < binaryHeap.numberOfItems; i++) { + int parentIndex = (i-1)/D; + if (binaryHeap.heap[parentIndex].F > binaryHeap.heap[i].F) { + throw new System.Exception("Invalid state at " + i + ":" + parentIndex + " ( " + binaryHeap.heap[parentIndex].F + " > " + binaryHeap.heap[i].F + " ) "); + } + + if (binaryHeap.heap[parentIndex].sortKey > binaryHeap.heap[i].sortKey) { + throw new System.Exception("Invalid state at " + i + ":" + parentIndex + " ( " + binaryHeap.heap[parentIndex].F + " > " + binaryHeap.heap[i].F + " ) "); + } + + if (nodes[binaryHeap.heap[i].pathNodeIndex].heapIndex != i) { + throw new System.Exception("Invalid heap index"); + } + } + } + + /// <summary> + /// Rebuilds the heap by trickeling down all items. + /// Usually called after the hTarget on a path has been changed + /// </summary> + public void Rebuild (UnsafeSpan<PathNode> nodes) { +#if ASTARDEBUG + int changes = 0; +#endif + + for (int i = 2; i < numberOfItems; i++) { + int bubbleIndex = i; + var node = heap[i]; + uint nodeF = node.F; + while (bubbleIndex != 1) { + int parentIndex = bubbleIndex / D; + + if (nodeF < heap[parentIndex].F) { + heap[bubbleIndex] = heap[parentIndex]; + nodes[heap[bubbleIndex].pathNodeIndex].heapIndex = (ushort)bubbleIndex; + + heap[parentIndex] = node; + nodes[heap[parentIndex].pathNodeIndex].heapIndex = (ushort)parentIndex; + + bubbleIndex = parentIndex; +#if ASTARDEBUG + changes++; +#endif + } else { + break; + } + } + } + +#if ASTARDEBUG + UnityEngine.Debug.Log("+++ Rebuilt Heap - "+changes+" changes +++"); +#endif + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs.meta new file mode 100644 index 0000000..0ba2009 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: eb4299e8747f44ad2b4e086752108ea3 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/CircularBuffer.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/CircularBuffer.cs new file mode 100644 index 0000000..37b88d2 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/CircularBuffer.cs @@ -0,0 +1,244 @@ +using System.Collections; +using System.Collections.Generic; +using Pathfinding.Util; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine.Assertions; + +namespace Pathfinding.Util { + /// <summary> + /// Implements an efficient circular buffer that can be appended to in both directions. + /// + /// See: <see cref="NativeCircularBuffer"/> + /// </summary> + public struct CircularBuffer<T> : IReadOnlyList<T>, IReadOnlyCollection<T> { + internal T[] data; + internal int head; + int length; + + /// <summary>Number of items in the buffer</summary> + public readonly int Length => length; + /// <summary>Absolute index of the first item in the buffer, may be negative or greater than <see cref="Length"/></summary> + public readonly int AbsoluteStartIndex => head; + /// <summary>Absolute index of the last item in the buffer, may be negative or greater than <see cref="Length"/></summary> + public readonly int AbsoluteEndIndex => head + length - 1; + + /// <summary>First item in the buffer, throws if the buffer is empty</summary> + public readonly ref T First => ref data[head & (data.Length-1)]; + + /// <summary>Last item in the buffer, throws if the buffer is empty</summary> + public readonly ref T Last => ref data[(head+length-1) & (data.Length-1)]; + + readonly int IReadOnlyCollection<T>.Count { + get { + return length; + } + } + + /// <summary>Create a new buffer with the given capacity</summary> + public CircularBuffer(int initialCapacity) { + data = ArrayPool<T>.Claim(initialCapacity); + head = 0; + length = 0; + } + + /// <summary> + /// Create a new buffer using the given array as an internal store. + /// This will take ownership of the given array. + /// </summary> + public CircularBuffer(T[] backingArray) { + data = backingArray; + head = 0; + length = 0; + } + + /// <summary>Resets the buffer's length to zero. Does not clear the current allocation</summary> + public void Clear () { + length = 0; + head = 0; + } + + /// <summary>Appends a list of items to the end of the buffer</summary> + public void AddRange (List<T> items) { + // TODO: Can be optimized + for (int i = 0; i < items.Count; i++) PushEnd(items[i]); + } + + /// <summary>Pushes a new item to the start of the buffer</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void PushStart (T item) { + if (data == null || length >= data.Length) Grow(); + length += 1; + head -= 1; + this[0] = item; + } + + /// <summary>Pushes a new item to the end of the buffer</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void PushEnd (T item) { + if (data == null || length >= data.Length) Grow(); + length += 1; + this[length-1] = item; + } + + /// <summary>Pushes a new item to the start or the end of the buffer</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void Push (bool toStart, T item) { + if (toStart) PushStart(item); + else PushEnd(item); + } + + /// <summary>Removes and returns the first element</summary> + public T PopStart () { + if (length == 0) throw new System.InvalidOperationException(); + var r = this[0]; + head++; + length--; + return r; + } + + /// <summary>Removes and returns the last element</summary> + public T PopEnd () { + if (length == 0) throw new System.InvalidOperationException(); + var r = this[length-1]; + length--; + return r; + } + + /// <summary>Pops either from the start or from the end of the buffer</summary> + public T Pop (bool fromStart) { + if (fromStart) return PopStart(); + else return PopEnd(); + } + + /// <summary>Return either the first element or the last element</summary> + public readonly T GetBoundaryValue (bool start) { + return GetAbsolute(start ? AbsoluteStartIndex : AbsoluteEndIndex); + } + + /// <summary>Inserts an item at the given absolute index</summary> + public void InsertAbsolute (int index, T item) { + SpliceUninitializedAbsolute(index, 0, 1); + data[index & (data.Length - 1)] = item; + } + + /// <summary>Removes toRemove items from the buffer, starting at startIndex, and then inserts the toInsert items at startIndex</summary> + public void Splice (int startIndex, int toRemove, List<T> toInsert) { + SpliceAbsolute(startIndex + head, toRemove, toInsert); + } + + /// <summary>Like <see cref="Splice"/>, but startIndex is an absolute index</summary> + public void SpliceAbsolute (int startIndex, int toRemove, List<T> toInsert) { + if (toInsert == null) { + SpliceUninitializedAbsolute(startIndex, toRemove, 0); + } else { + SpliceUninitializedAbsolute(startIndex, toRemove, toInsert.Count); + for (int i = 0; i < toInsert.Count; i++) data[(startIndex + i) & (data.Length - 1)] = toInsert[i]; + } + } + + /// <summary>Like <see cref="Splice"/>, but the newly inserted items are left in an uninitialized state</summary> + public void SpliceUninitialized (int startIndex, int toRemove, int toInsert) { + SpliceUninitializedAbsolute(startIndex + head, toRemove, toInsert); + } + + /// <summary>Like <see cref="SpliceUninitialized"/>, but startIndex is an absolute index</summary> + public void SpliceUninitializedAbsolute (int startIndex, int toRemove, int toInsert) { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (startIndex - head < 0 || startIndex + toRemove - head > length) throw new System.ArgumentOutOfRangeException(); +#endif + var itemsToAdd = toInsert - toRemove; + while (this.length + itemsToAdd > this.data.Length) Grow(); + + // move items [startIndex+length .. end] itemsToAdd steps forward in the array + MoveAbsolute(startIndex + toRemove, AbsoluteEndIndex, itemsToAdd); + this.length += itemsToAdd; + } + + void MoveAbsolute (int startIndex, int endIndex, int deltaIndex) { + if (deltaIndex > 0) { + for (int i = endIndex; i >= startIndex; i--) data[(i+deltaIndex) & (data.Length-1)] = data[i & (data.Length-1)]; + } else if (deltaIndex < 0) { + for (int i = startIndex; i <= endIndex; i++) data[(i+deltaIndex) & (data.Length-1)] = data[i & (data.Length-1)]; + } + } + + /// <summary>Indexes the buffer, with index 0 being the first element</summary> + public T this[int index] { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + readonly get { +#if UNITY_EDITOR + if ((uint)index >= length) throw new System.ArgumentOutOfRangeException(); +#endif + return data[(index+head) & (data.Length-1)]; + } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + set { +#if UNITY_EDITOR + if ((uint)index >= length) throw new System.ArgumentOutOfRangeException(); +#endif + data[(index+head) & (data.Length-1)] = value; + } + } + + /// <summary> + /// Indexes the buffer using absolute indices. + /// When pushing to and popping from the buffer, the absolute indices do not change. + /// So e.g. after doing PushStart(x) on an empty buffer, GetAbsolute(-1) will get the newly pushed element. + /// </summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public readonly T GetAbsolute (int index) { +#if UNITY_EDITOR + if ((uint)(index - head) >= length) throw new System.ArgumentOutOfRangeException(); +#endif + return data[index & (data.Length-1)]; + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public readonly void SetAbsolute (int index, T value) { +#if UNITY_EDITOR + if ((uint)(index - head) >= length) throw new System.ArgumentOutOfRangeException(); +#endif + data[index & (data.Length-1)] = value; + } + + void Grow () { + var newData = ArrayPool<T>.Claim(System.Math.Max(4, data != null ? data.Length*2 : 0)); + if (data != null) { + var inOrderItems = data.Length - (head & (data.Length-1)); + System.Array.Copy(data, head & (data.Length-1), newData, head & (newData.Length - 1), inOrderItems); + var wraparoundItems = length - inOrderItems; + if (wraparoundItems > 0) System.Array.Copy(data, 0, newData, (head + inOrderItems) & (newData.Length - 1), wraparoundItems); + ArrayPool<T>.Release(ref data); + } + data = newData; + } + + /// <summary>Release the backing array of this buffer back into an array pool</summary> + public void Pool () { + ArrayPool<T>.Release(ref data); + length = 0; + head = 0; + } + + public IEnumerator<T> GetEnumerator () { + for (int i = 0; i < length; i++) { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator () { + for (int i = 0; i < length; i++) { + yield return this[i]; + } + } + + public CircularBuffer<T> Clone () { + return new CircularBuffer<T> { + data = data != null ? (T[])data.Clone() : null, + length = length, + head = head + }; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/CircularBuffer.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/CircularBuffer.cs.meta new file mode 100644 index 0000000..ea6fc4e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/CircularBuffer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c76961280d816f42a709dc7150208c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/HierarchicalBitset.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/HierarchicalBitset.cs new file mode 100644 index 0000000..ed23e94 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/HierarchicalBitset.cs @@ -0,0 +1,288 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine.Assertions; + +namespace Pathfinding.Util { + /// <summary> + /// Thread-safe hierarchical bitset. + /// + /// Stores an array of bits. Each bit can be set or cleared individually from any thread. + /// + /// Note: Setting the capacity is not thread-safe, nor is iterating over the bitset while it is being modified. + /// </summary> + [BurstCompile] + public struct HierarchicalBitset { + UnsafeSpan<ulong> l1; + UnsafeSpan<ulong> l2; + UnsafeSpan<ulong> l3; + Allocator allocator; + + const int Log64 = 6; + + public HierarchicalBitset (int size, Allocator allocator) { + this.allocator = allocator; + l1 = new UnsafeSpan<ulong>(allocator, (size + 64 - 1) >> Log64); + l2 = new UnsafeSpan<ulong>(allocator, (size + (64*64 - 1)) >> Log64 >> Log64); + l3 = new UnsafeSpan<ulong>(allocator, (size + (64*64*64 - 1)) >> Log64 >> Log64 >> Log64); + l1.FillZeros(); + l2.FillZeros(); + l3.FillZeros(); + } + + public bool IsCreated => Capacity > 0; + + public void Dispose () { + l1.Free(allocator); + l2.Free(allocator); + l3.Free(allocator); + this = default; + } + + public int Capacity { + get { + return l1.Length << Log64; + } + set { + if (value < Capacity) throw new System.ArgumentException("Shrinking the bitset is not supported"); + if (value == Capacity) return; + var b = new HierarchicalBitset(value, allocator); + + // Copy the old data + l1.CopyTo(b.l1); + l2.CopyTo(b.l2); + l3.CopyTo(b.l3); + + Dispose(); + this = b; + } + } + + /// <summary>Number of set bits in the bitset</summary> + public int Count () { + int count = 0; + for (int i = 0; i < l1.Length; i++) { + count += math.countbits(l1[i]); + } + return count; + } + + /// <summary>True if the bitset is empty</summary> + public bool IsEmpty { + get { + for (int i = 0; i < l3.Length; i++) { + if (l3[i] != 0) return false; + } + return true; + } + } + + /// <summary>Clear all bits</summary> + public void Clear () { + // TODO: Optimize? + l1.FillZeros(); + l2.FillZeros(); + l3.FillZeros(); + } + + public void GetIndices (NativeList<int> result) { + var buffer = new NativeArray<int>(256, Allocator.Temp); + var iter = GetIterator(buffer.AsUnsafeSpan()); + while (iter.MoveNext()) { + var span = iter.Current; + for (int i = 0; i < span.Length; i++) { + result.Add(span[i]); + } + } + } + + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + static bool SetAtomic (ref UnsafeSpan<ulong> span, int index) { + var cellIndex = index >> Log64; + var currentValue = span[cellIndex]; + // Note: 1 << index will only use the lower 6 bits of index + if ((currentValue & (1UL << index)) != 0) { + // Bit already set + return true; + } + + // TODO: Use Interlocked.Or in newer .net versions + while (true) { + var actualValue = (ulong)System.Threading.Interlocked.CompareExchange(ref UnsafeUtility.As<ulong, long>(ref span[cellIndex]), (long)(currentValue | (1UL << index)), (long)currentValue); + if (actualValue != currentValue) currentValue = actualValue; + else break; + } + return false; + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + static bool ResetAtomic (ref UnsafeSpan<ulong> span, int index) { + var cellIndex = index >> Log64; + var currentValue = span[cellIndex]; + // Note: 1 << index will only use the lower 6 bits of index + if ((currentValue & (1UL << index)) == 0) { + // Bit already cleared + return true; + } + + // TODO: Use Interlocked.Or in newer .net versions + while (true) { + var actualValue = (ulong)System.Threading.Interlocked.CompareExchange(ref UnsafeUtility.As<ulong, long>(ref span[cellIndex]), (long)(currentValue & ~(1UL << index)), (long)currentValue); + if (actualValue != currentValue) currentValue = actualValue; + else break; + } + return false; + } + + /// <summary>Get the value of a bit</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool Get (int index) { + // Note: 1 << index will only use the lower 6 bits of index + return (l1[index >> Log64] & (1UL << index)) != 0; + } + + /// <summary>Set a given bit to 1</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void Set (int index) { + if (SetAtomic(ref l1, index)) return; + SetAtomic(ref l2, index >> Log64); + SetAtomic(ref l3, index >> (2*Log64)); + } + + /// <summary>Set a given bit to 0</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void Reset (int index) { + if (ResetAtomic(ref l1, index)) return; + if (l1[index >> Log64] == 0) ResetAtomic(ref l2, index >> Log64); + if (l2[index >> (2*Log64)] == 0) ResetAtomic(ref l3, index >> (2*Log64)); + } + + /// <summary>Get an iterator over all set bits.</summary> + /// <param name="scratchBuffer">A buffer to use for temporary storage. A slice of this buffer will be returned on each iteration, filled with the indices of the set bits.</param> + public Iterator GetIterator (UnsafeSpan<int> scratchBuffer) { + return new Iterator(this, scratchBuffer); + } + + [BurstCompile] + public struct Iterator : IEnumerator<UnsafeSpan<int> >, IEnumerable<UnsafeSpan<int> > { + HierarchicalBitset bitSet; + UnsafeSpan<int> result; + int resultCount; + int l3index; + int l3bitIndex; + int l2bitIndex; + + public UnsafeSpan<int> Current => result.Slice(0, resultCount); + + object IEnumerator.Current => throw new System.NotImplementedException(); + + public void Reset() => throw new System.NotImplementedException(); + + public void Dispose () {} + + public IEnumerator<UnsafeSpan<int> > GetEnumerator() => this; + + IEnumerator IEnumerable.GetEnumerator() => throw new System.NotImplementedException(); + + static int l2index(int l3index, int l3bitIndex) => (l3index << Log64) + l3bitIndex; + static int l1index(int l2index, int l2bitIndex) => (l2index << Log64) + l2bitIndex; + + public Iterator (HierarchicalBitset bitSet, UnsafeSpan<int> result) { + this.bitSet = bitSet; + this.result = result; + resultCount = 0; + l3index = 0; + l3bitIndex = 0; + l2bitIndex = 0; + if (result.Length < 128) { + // Minimum is actually 64, but that can be very inefficient + throw new System.ArgumentException("Result array must be at least 128 elements long"); + } + } + + public bool MoveNext () { + return MoveNextBurst(ref this); + } + + [BurstCompile] + public static bool MoveNextBurst (ref Iterator iter) { + return iter.MoveNextInternal(); + } + + // Inline + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + bool MoveNextInternal () { + // Store various data in local variables to avoid writing them to memory every time they are updated + uint resultCount = 0; + int l3index = this.l3index; + int l3bitIndex = this.l3bitIndex; + int l2bitIndex = this.l2bitIndex; + Assert.IsTrue(l2bitIndex < 64 && l3bitIndex < 64); + + for (; l3index < bitSet.l3.length; l3index++) { + // Get the L3 cell, and mask out all bits we have already visited + var l3cell = bitSet.l3[l3index] & (~0UL << l3bitIndex); + if (l3cell == 0) continue; + + while (l3cell != 0) { + // Find the next set bit in the L3 cell + l3bitIndex = math.tzcnt(l3cell); + + // Nest check for level 2 + int l2index = Iterator.l2index(l3index, l3bitIndex); + // The l2 cell is guaranteed to be non-zero, even after masking out the bits we have already visited + var l2cell = bitSet.l2[l2index] & (~0UL << l2bitIndex); + Assert.AreNotEqual(0, l2cell); + + while (l2cell != 0) { + l2bitIndex = math.tzcnt(l2cell); + // Stop the loop if we have almost filled the result array + // Each L1 cell may contain up to 64 set bits + if (resultCount + 64 > result.Length) { + this.resultCount = (int)resultCount; + this.l3index = l3index; + this.l3bitIndex = l3bitIndex; + this.l2bitIndex = l2bitIndex; + return true; + } + + int l1index = Iterator.l1index(l2index, l2bitIndex); + var l1cell = bitSet.l1[l1index]; + int l1indexStart = l1index << Log64; + Assert.AreNotEqual(0, l1cell); + + while (l1cell != 0) { + var l1bitIndex = math.tzcnt(l1cell); + l1cell &= l1cell - 1UL; // clear lowest bit + int index = l1indexStart + l1bitIndex; + Unity.Burst.CompilerServices.Hint.Assume(resultCount < (uint)result.Length); + result[resultCount++] = index; + } + + l2cell &= l2cell - 1UL; + } + + // Skip a bit at the L3 level + l3cell &= l3cell - 1UL; // clear lowest bit + // Enter new L2 level + l2bitIndex = 0; + } + + l2bitIndex = 0; + l3bitIndex = 0; + } + + this.resultCount = (int)resultCount; + this.l3index = l3index; + this.l3bitIndex = l3bitIndex; + this.l2bitIndex = l2bitIndex; + return resultCount > 0; + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/HierarchicalBitset.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/HierarchicalBitset.cs.meta new file mode 100644 index 0000000..e49aade --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/HierarchicalBitset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c9a4ed21e5f87d40aba2fcc4e01146d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/NativeCircularBuffer.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/NativeCircularBuffer.cs new file mode 100644 index 0000000..6f4eb4a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/NativeCircularBuffer.cs @@ -0,0 +1,318 @@ +using System.Collections; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine.Assertions; + +namespace Pathfinding.Util { + /// <summary> + /// Implements an efficient circular buffer that can be appended to in both directions. + /// + /// See: <see cref="CircularBuffer"/> + /// </summary> + public struct NativeCircularBuffer<T> : IReadOnlyList<T>, IReadOnlyCollection<T> where T : unmanaged { + [NativeDisableUnsafePtrRestriction] + internal unsafe T* data; + internal int head; + int length; + /// <summary>Capacity of the allocation minus 1. Invariant: (a power of two) minus 1</summary> + int capacityMask; + + /// <summary>The allocator used to create the internal buffer.</summary> + public AllocatorManager.AllocatorHandle Allocator; + /// <summary>Number of items in the buffer</summary> + public readonly int Length => length; + /// <summary>Absolute index of the first item in the buffer, may be negative or greater than <see cref="Length"/></summary> + public readonly int AbsoluteStartIndex => head; + /// <summary>Absolute index of the last item in the buffer, may be negative or greater than <see cref="Length"/></summary> + public readonly int AbsoluteEndIndex => head + length - 1; + + /// <summary>First item in the buffer throws if the buffer is empty</summary> + public readonly ref T First { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get { + unsafe { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length == 0) throw new System.InvalidOperationException(); +#endif + return ref data[head & capacityMask]; + } + } + } + + /// <summary>Last item in the buffer, throws if the buffer is empty</summary> + public readonly ref T Last { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length == 0) throw new System.InvalidOperationException(); +#endif + unsafe { return ref data[(head+length-1) & capacityMask]; } + } + } + + readonly int IReadOnlyCollection<T>.Count => Length; + + public readonly bool IsCreated { + get { + unsafe { + return data != null; + } + } + } + + /// <summary>Create a new empty buffer</summary> + + public NativeCircularBuffer(AllocatorManager.AllocatorHandle allocator) { + unsafe { + data = null; + } + Allocator = allocator; + capacityMask = -1; + head = 0; + length = 0; + } + + /// <summary>Create a new buffer with the given capacity</summary> + public NativeCircularBuffer(int initialCapacity, AllocatorManager.AllocatorHandle allocator) { + initialCapacity = math.ceilpow2(initialCapacity); + unsafe { + data = AllocatorManager.Allocate<T>(allocator, initialCapacity); + capacityMask = initialCapacity - 1; + } + Allocator = allocator; + head = 0; + length = 0; + } + + unsafe public NativeCircularBuffer(CircularBuffer<T> buffer, out ulong gcHandle) : this(buffer.data, buffer.head, buffer.Length, out gcHandle) {} + + unsafe public NativeCircularBuffer(T[] data, int head, int length, out ulong gcHandle) { + Assert.IsTrue((data.Length & (data.Length - 1)) == 0); + Assert.IsTrue(length <= data.Length); + unsafe { + this.data = (T*)UnsafeUtility.PinGCArrayAndGetDataAddress(data, out gcHandle); + } + this.capacityMask = data.Length - 1; + this.head = head; + this.length = length; + Allocator = Unity.Collections.Allocator.None; + } + + /// <summary>Resets the buffer's length to zero. Does not clear the current allocation</summary> + public void Clear () { + length = 0; + head = 0; + } + + /// <summary>Appends a list of items to the end of the buffer</summary> + public void AddRange (List<T> items) { + // TODO: Can be optimized + for (int i = 0; i < items.Count; i++) PushEnd(items[i]); + } + + /// <summary>Pushes a new item to the start of the buffer</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void PushStart (T item) { + if (length > capacityMask) Grow(); + length += 1; + head -= 1; + this[0] = item; + } + + /// <summary>Pushes a new item to the end of the buffer</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void PushEnd (T item) { + if (length > capacityMask) Grow(); + length += 1; + this[length-1] = item; + } + + /// <summary>Pushes a new item to the start or the end of the buffer</summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void Push (bool toStart, T item) { + if (toStart) PushStart(item); + else PushEnd(item); + } + + /// <summary>Removes and returns the first element</summary> + public T PopStart () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length == 0) throw new System.InvalidOperationException(); +#endif + var r = this[0]; + head++; + length--; + return r; + } + + /// <summary>Removes and returns the last element</summary> + public T PopEnd () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length == 0) throw new System.InvalidOperationException(); +#endif + var r = this[length-1]; + length--; + return r; + } + + /// <summary>Pops either from the start or from the end of the buffer</summary> + public T Pop (bool fromStart) { + if (fromStart) return PopStart(); + else return PopEnd(); + } + + /// <summary>Return either the first element or the last element</summary> + public readonly T GetBoundaryValue (bool start) { + return start ? GetAbsolute(AbsoluteStartIndex) : GetAbsolute(AbsoluteEndIndex); + } + + /// <summary>Lowers the length of the buffer to the given value, and does nothing if the given value is greater or equal to the current length</summary> + + public void TrimTo (int length) { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length < 0) throw new System.ArgumentOutOfRangeException(); +#endif + this.length = math.min(this.length, length); + } + + /// <summary>Removes toRemove items from the buffer, starting at startIndex, and then inserts the toInsert items at startIndex</summary> + + public void Splice (int startIndex, int toRemove, List<T> toInsert) { + SpliceAbsolute(startIndex + head, toRemove, toInsert); + } + + /// <summary>Like <see cref="Splice"/>, but startIndex is an absolute index</summary> + + public void SpliceAbsolute (int startIndex, int toRemove, List<T> toInsert) { + SpliceUninitializedAbsolute(startIndex, toRemove, toInsert.Count); + unsafe { + for (int i = 0; i < toInsert.Count; i++) data[(startIndex + i) & capacityMask] = toInsert[i]; + } + } + + /// <summary>Like <see cref="Splice"/>, but the newly inserted items are left in an uninitialized state</summary> + public void SpliceUninitialized (int startIndex, int toRemove, int toInsert) { + SpliceUninitializedAbsolute(startIndex + head, toRemove, toInsert); + } + + /// <summary>Like <see cref="SpliceUninitialized"/>, but startIndex is an absolute index</summary> + public void SpliceUninitializedAbsolute (int startIndex, int toRemove, int toInsert) { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (startIndex - head < 0 || startIndex + toRemove - head > length) throw new System.ArgumentOutOfRangeException(); +#endif + var itemsToAdd = toInsert - toRemove; + while (this.length + itemsToAdd > capacityMask + 1) Grow(); + + // move items [startIndex+length .. end] itemsToAdd steps forward in the array + MoveAbsolute(startIndex + toRemove, AbsoluteEndIndex, itemsToAdd); + this.length += itemsToAdd; + } + + void MoveAbsolute (int startIndex, int endIndex, int deltaIndex) { + unsafe { + if (deltaIndex > 0) { + for (int i = endIndex; i >= startIndex; i--) data[(i+deltaIndex) & capacityMask] = data[i & capacityMask]; + } else if (deltaIndex < 0) { + for (int i = startIndex; i <= endIndex; i++) data[(i+deltaIndex) & capacityMask] = data[i & capacityMask]; + } + } + } + + /// <summary>Indexes the buffer, with index 0 being the first element</summary> + public T this[int index] { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + readonly get { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if ((uint)index >= length) throw new System.ArgumentOutOfRangeException(); +#endif + unsafe { + return data[(index+head) & capacityMask]; + } + } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + set { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if ((uint)index >= length) throw new System.ArgumentOutOfRangeException(); +#endif + unsafe { + data[(index+head) & capacityMask] = value; + } + } + } + + /// <summary> + /// Indexes the buffer using absolute indices. + /// When pushing to and popping from the buffer, the absolute indices do not change. + /// So e.g. after doing PushStart(x) on an empty buffer, GetAbsolute(-1) will get the newly pushed element. + /// </summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public readonly T GetAbsolute (int index) { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if ((uint)(index - head) >= length) throw new System.ArgumentOutOfRangeException(); +#endif + unsafe { + return data[index & capacityMask]; + } + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + void Grow () { + unsafe { + // Note: Will always be a power of 2 since capacity is a power of 2 + var capacity = capacityMask + 1; + var newCapacity = math.max(4, capacity*2); + var newData = AllocatorManager.Allocate<T>(this.Allocator, newCapacity); + if (data != null) { + var inOrderItems = capacity - (head & capacityMask); + UnsafeUtility.MemCpy(newData + (head & (newCapacity - 1)), data + (head & capacityMask), inOrderItems * sizeof(T)); + var wraparoundItems = length - inOrderItems; + if (wraparoundItems > 0) { + UnsafeUtility.MemCpy(newData + ((head + inOrderItems) & (newCapacity - 1)), data, wraparoundItems * sizeof(T)); + } + AllocatorManager.Free(Allocator, data); + } + capacityMask = newCapacity - 1; + data = newData; + } + } + + /// <summary>Releases the unmanaged memory held by this container</summary> + public void Dispose () { + capacityMask = -1; + length = 0; + head = 0; + unsafe { + AllocatorManager.Free(Allocator, data); + data = null; + } + } + + public IEnumerator<T> GetEnumerator () { + for (int i = 0; i < length; i++) { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator () { + for (int i = 0; i < length; i++) { + yield return this[i]; + } + } + + public NativeCircularBuffer<T> Clone () { + unsafe { + var newData = AllocatorManager.Allocate<T>(this.Allocator, capacityMask + 1); + UnsafeUtility.MemCpy(newData, data, length * sizeof(T)); + return new NativeCircularBuffer<T> { + data = newData, + head = head, + length = length, + capacityMask = capacityMask, + Allocator = this.Allocator + }; + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/NativeCircularBuffer.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/NativeCircularBuffer.cs.meta new file mode 100644 index 0000000..0e52bc5 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/NativeCircularBuffer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c126a7f5fd26b684984ddef8030409f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/SlabAllocator.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/SlabAllocator.cs new file mode 100644 index 0000000..51c276b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/SlabAllocator.cs @@ -0,0 +1,316 @@ +namespace Pathfinding.Util { + using Unity.Mathematics; + using Unity.Collections; + using Unity.Collections.LowLevel.Unsafe; + + /// <summary> + /// A tiny slab allocator. + /// Allocates spans of type T in power-of-two sized blocks. + /// + /// Note: This allocator has no support for merging adjacent freed blocks. + /// Therefore it is best suited for similarly sized allocations which are relatively small. + /// + /// Can be used in burst jobs. + /// + /// This is faster than allocating NativeArrays using the Temp allocator, and significantly faster + /// than allocating them using the Persistent allocator. + /// </summary> + public struct SlabAllocator<T> where T : unmanaged { + public const int MaxAllocationSizeIndex = 10; + const uint UsedBit = 1u << 31; + const uint AllocatedBit = 1u << 30; + const uint LengthMask = AllocatedBit - 1; + /// <summary>Allocation which is always invalid</summary> + public const int InvalidAllocation = -2; + /// <summary>Allocation representing a zero-length array</summary> + public const int ZeroLengthArray = -1; + + [NativeDisableUnsafePtrRestriction] + unsafe AllocatorData* data; + + struct AllocatorData { + public UnsafeList<byte> mem; + public unsafe fixed int freeHeads[MaxAllocationSizeIndex+1]; + } + + struct Header { + public uint length; + } + + struct NextBlock { + public int next; + } + + public bool IsCreated { + get { + unsafe { + return data != null; + } + } + } + + public int ByteSize { + get { + unsafe { + return data->mem.Length; + } + } + } + + public SlabAllocator(int initialCapacityBytes, AllocatorManager.AllocatorHandle allocator) { + unsafe { + data = AllocatorManager.Allocate<AllocatorData>(allocator); + data->mem = new UnsafeList<byte>(initialCapacityBytes, allocator); + Clear(); + } + } + + /// <summary> + /// Frees all existing allocations. + /// Does not free the underlaying unmanaged memory. Use <see cref="Dispose"/> for that. + /// </summary> + public void Clear () { + CheckDisposed(); + unsafe { + data->mem.Clear(); + for (int i = 0; i < MaxAllocationSizeIndex + 1; i++) { + data->freeHeads[i] = -1; + } + } + } + + + /// <summary> + /// Get the span representing the given allocation. + /// The returned array does not need to be disposed. + /// It is only valid until the next call to <see cref="Allocate"/>, <see cref="Free"/> or <see cref="Dispose"/>. + /// </summary> + public UnsafeSpan<T> GetSpan (int allocatedIndex) { + CheckDisposed(); + unsafe { + if (allocatedIndex == ZeroLengthArray) return new UnsafeSpan<T>(null, 0); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException($"Invalid allocation {allocatedIndex}"); +#endif + var ptr = data->mem.Ptr + allocatedIndex; + var header = (Header*)(ptr - sizeof(Header)); + var length = header->length & LengthMask; +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception($"Invalid allocation {allocatedIndex}"); + if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to get a span for an unallocated index"); +#endif + return new UnsafeSpan<T>(ptr, (int)length); + } + } + + public List GetList (int allocatedIndex) { + return new List(this, allocatedIndex); + } + + public void Realloc (ref int allocatedIndex, int nElements) { + CheckDisposed(); + if (allocatedIndex == ZeroLengthArray) { + allocatedIndex = Allocate(nElements); + return; + } + + unsafe { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException(); +#endif + var ptr = data->mem.Ptr + allocatedIndex; + var header = (Header*)(ptr - sizeof(Header)); + var length = header->length & LengthMask; +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception("Invalid index"); + if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to get a span for an unallocated index"); +#endif + var capacityIndex = ElementsToSizeIndex((int)length); + var newCapacityIndex = ElementsToSizeIndex((int)nElements); + if (capacityIndex == newCapacityIndex) { + header->length = (uint)nElements | AllocatedBit | UsedBit; + } else { + int newAllocation = Allocate(nElements); + var oldSpan = GetSpan(allocatedIndex); + var newSpan = GetSpan(newAllocation); + oldSpan.Slice(0, math.min((int)length, nElements)).CopyTo(newSpan); + Free(allocatedIndex); + allocatedIndex = newAllocation; + } + } + } + + internal static int SizeIndexToElements (int sizeIndex) { + return 1 << sizeIndex; + } + + internal static int ElementsToSizeIndex (int nElements) { + if (nElements < 0) throw new System.Exception("SlabAllocator cannot allocate less than 1 element"); + if (nElements == 0) return 0; + int sizeIndex = CollectionHelper.Log2Ceil(nElements); + if (sizeIndex > MaxAllocationSizeIndex) throw new System.Exception("SlabAllocator cannot allocate more than 2^(MaxAllocationSizeIndex-1) elements"); + return sizeIndex; + } + + /// <summary> + /// Allocates an array big enough to fit the given values and copies them to the new allocation. + /// Returns: An ID for the new allocation. + /// </summary> + public int Allocate (System.Collections.Generic.List<T> values) { + var index = Allocate(values.Count); + var span = GetSpan(index); + for (int i = 0; i < span.Length; i++) span[i] = values[i]; + return index; + } + + /// <summary> + /// Allocates an array big enough to fit the given values and copies them to the new allocation. + /// Returns: An ID for the new allocation. + /// </summary> + public int Allocate (NativeList<T> values) { + var index = Allocate(values.Length); + GetSpan(index).CopyFrom(values.AsArray()); + return index; + } + + /// <summary> + /// Allocates an array of type T with length nElements. + /// Must later be freed using <see cref="Free"/> (or <see cref="Dispose)"/>. + /// + /// Returns: An ID for the new allocation. + /// </summary> + public int Allocate (int nElements) { + CheckDisposed(); + if (nElements == 0) return ZeroLengthArray; + var sizeIndex = ElementsToSizeIndex(nElements); + unsafe { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (sizeIndex < 0 || sizeIndex > MaxAllocationSizeIndex) throw new System.Exception("Invalid size index " + sizeIndex); +#endif + int head = data->freeHeads[sizeIndex]; + if (head != -1) { + var ptr = data->mem.Ptr; + data->freeHeads[sizeIndex] = ((NextBlock*)(ptr + head))->next; + *(Header*)(ptr + head - sizeof(Header)) = new Header { length = (uint)nElements | UsedBit | AllocatedBit }; + return head; + } + + int headerStart = data->mem.Length; + int requiredSize = headerStart + sizeof(Header) + SizeIndexToElements(sizeIndex)*sizeof(T); + if (Unity.Burst.CompilerServices.Hint.Unlikely(requiredSize > data->mem.Capacity)) { + data->mem.SetCapacity(math.max(data->mem.Capacity*2, requiredSize)); + } + + // Set the length field directly because we know we don't have to resize the list, + // and we do not care about zeroing the memory. + data->mem.m_length = requiredSize; + *(Header*)(data->mem.Ptr + headerStart) = new Header { length = (uint)nElements | UsedBit | AllocatedBit }; + return headerStart + sizeof(Header); + } + } + + /// <summary>Frees a single allocation</summary> + public void Free (int allocatedIndex) { + CheckDisposed(); + if (allocatedIndex == ZeroLengthArray) return; + unsafe { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException(); +#endif + var ptr = data->mem.Ptr; + var header = (Header*)(ptr + allocatedIndex - sizeof(Header)); + var length = (int)(header->length & LengthMask); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (length < 0 || length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception("Invalid index"); + if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to free an already freed index"); +#endif + + var sizeIndex = ElementsToSizeIndex(length); + + *(NextBlock*)(ptr + allocatedIndex) = new NextBlock { + next = data->freeHeads[sizeIndex] + }; + data->freeHeads[sizeIndex] = allocatedIndex; + // Mark as not allocated + header->length &= ~(AllocatedBit | UsedBit); + } + } + + public void CopyTo (SlabAllocator<T> other) { + CheckDisposed(); + other.CheckDisposed(); + unsafe { + other.data->mem.CopyFrom(data->mem); + for (int i = 0; i < MaxAllocationSizeIndex + 1; i++) { + other.data->freeHeads[i] = data->freeHeads[i]; + } + } + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + void CheckDisposed () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + unsafe { + if (data == null) throw new System.InvalidOperationException("SlabAllocator is already disposed or not initialized"); + } +#endif + } + + /// <summary>Frees all unmanaged memory associated with this container</summary> + public void Dispose () { + unsafe { + if (data == null) return; + var allocator = data->mem.Allocator; + data->mem.Dispose(); + AllocatorManager.Free(allocator, data); + data = null; + } + } + + public ref struct List { + public UnsafeSpan<T> span; + SlabAllocator<T> allocator; + // TODO: Can be derived from span + public int allocationIndex; + + public List(SlabAllocator<T> allocator, int allocationIndex) { + this.span = allocator.GetSpan(allocationIndex); + this.allocator = allocator; + this.allocationIndex = allocationIndex; + } + + public void Add (T value) { + allocator.Realloc(ref allocationIndex, span.Length + 1); + span = allocator.GetSpan(allocationIndex); + span[span.Length - 1] = value; + } + + public void RemoveAt (int index) { + span.Slice(index + 1).CopyTo(span.Slice(index, span.Length - index - 1)); + allocator.Realloc(ref allocationIndex, span.Length - 1); + span = allocator.GetSpan(allocationIndex); + } + + public void Clear () { + allocator.Realloc(ref allocationIndex, 0); + span = allocator.GetSpan(allocationIndex); + } + + public int Length => span.Length; + + public ref T this[int index] { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get { + return ref span[index]; + } + } + } + } + + public static class SlabListExtensions { + public static void Remove<T>(ref this SlabAllocator<T>.List list, T value) where T : unmanaged, System.IEquatable<T> { + int idx = list.span.IndexOf(value); + if (idx != -1) list.RemoveAt(idx); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/SlabAllocator.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/SlabAllocator.cs.meta new file mode 100644 index 0000000..b317b73 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/SlabAllocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 020cb204e780440f8987b23cd21c02d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/Span.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/Span.cs new file mode 100644 index 0000000..da9976a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/Span.cs @@ -0,0 +1,384 @@ +using Unity.Mathematics; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using System.Runtime.CompilerServices; +using System.Collections.Generic; + +namespace Pathfinding.Util { + /// <summary> + /// Replacement for System.Span which is compatible with earlier versions of C#. + /// + /// Warning: These spans do not in any way guarantee that the memory they refer to is valid. It is up to the user to make sure + /// the memory is not deallocated before usage. It should never be used to refer to managed heap memory without pinning it, since unpinned managed memory can be moved by some runtimes. + /// + /// This has several benefits over e.g. UnsafeList: + /// - It is faster to index into a span than into an UnsafeList, especially from C#. In fact, indexing into an UnsafeSpan is as fast as indexing into a native C# array. + /// - As a comparison, indexing into a NativeArray can easily be 10x slower, and indexing into an UnsafeList is at least a few times slower. + /// - You can create a UnsafeSpan from a C# array by pinning it. + /// - It can be sliced efficiently. + /// - It supports ref returns for the indexing operations. + /// </summary> + public readonly struct UnsafeSpan<T> where T : unmanaged { + [NativeDisableUnsafePtrRestriction] + internal readonly unsafe T* ptr; + internal readonly uint length; + + /// <summary>Number of elements in this span</summary> + public int Length => (int)length; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe UnsafeSpan(void* ptr, int length) { + if (length < 0) throw new System.ArgumentOutOfRangeException(); + if (length > 0 && ptr == null) throw new System.ArgumentNullException(); + this.ptr = (T*)ptr; + this.length = (uint)length; + } + + /// <summary> + /// Creates a new UnsafeSpan from a C# array. + /// The array is pinned to ensure it does not move while the span is in use. + /// + /// You must unpin the pinned memory using UnsafeUtility.ReleaseGCObject when you are done with the span. + /// </summary> + public unsafe UnsafeSpan(T[] data, out ulong gcHandle) { + unsafe { + this.ptr = (T*)UnsafeUtility.PinGCArrayAndGetDataAddress(data, out gcHandle); + } + this.length = (uint)data.Length; + } + + /// <summary> + /// Allocates a new UnsafeSpan with the specified length. + /// The memory is not initialized. + /// + /// You are responsible for freeing the memory using the same allocator when you are done with it. + /// </summary> + public UnsafeSpan(Allocator allocator, int length) { + unsafe { + if (length < 0) throw new System.ArgumentOutOfRangeException(); + if (length > 0) this.ptr = AllocatorManager.Allocate<T>(allocator, length); + else this.ptr = null; + this.length = (uint)length; + } + } + + public ref T this[int index] { + // With aggressive inlining the performance of indexing is essentially the same as indexing into a native C# array + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { + unsafe { + if ((uint)index >= length) throw new System.IndexOutOfRangeException(); + Unity.Burst.CompilerServices.Hint.Assume(ptr != null); + return ref *(ptr + index); + } + } + } + + public ref T this[uint index] { + // With aggressive inlining the performance of indexing is essentially the same as indexing into a native C# array + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { + unsafe { + if (index >= length) throw new System.IndexOutOfRangeException(); + Unity.Burst.CompilerServices.Hint.Assume(ptr != null); + Unity.Burst.CompilerServices.Hint.Assume(ptr + index != null); + return ref *(ptr + index); + } + } + } + + /// <summary> + /// Returns a copy of this span, but with a different data-type. + /// The new data-type must have the same size as the old one. + /// + /// In burst, this should effectively be a no-op, except possibly a branch. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UnsafeSpan<U> Reinterpret<U> () where U : unmanaged { + unsafe { + if (sizeof(T) != sizeof(U)) throw new System.InvalidOperationException("Cannot reinterpret span because the size of the types do not match"); + return new UnsafeSpan<U>(ptr, (int)length); + } + } + + /// <summary> + /// Creates a new span which is a slice of this span. + /// The new span will start at the specified index and have the specified length. + /// </summary> + public UnsafeSpan<T> Slice (int start, int length) { + if (start < 0 || length < 0 || start + length > this.length) throw new System.ArgumentOutOfRangeException(); + unsafe { + return new UnsafeSpan<T>(ptr + start, length); + } + } + + /// <summary> + /// Creates a new span which is a slice of this span. + /// The new span will start at the specified index and continue to the end of this span. + /// </summary> + public UnsafeSpan<T> Slice (int start) { + return Slice(start, (int)this.length - start); + } + + /// <summary>Copy the range [startIndex,startIndex+count) to [toIndex,toIndex+count)</summary> + public void Move (int startIndex, int toIndex, int count) { + unsafe { + if (count < 0) throw new System.ArgumentOutOfRangeException(); + if (startIndex < 0 || startIndex + count > length) throw new System.ArgumentOutOfRangeException(); + if (toIndex < 0 || toIndex + count > length) throw new System.ArgumentOutOfRangeException(); + // If length is zero, the pointers may be null, which is technically undefined behavior (but in practice usually fine) + if (count == 0) return; + UnsafeUtility.MemMove(ptr + toIndex, ptr + startIndex, (long)sizeof(T) * (long)count); + } + } + + /// <summary> + /// Copies the memory of this span to another span. + /// The other span must be large enough to hold the contents of this span. + /// + /// Note: Assumes the other span does not alias this one. + /// </summary> + public void CopyTo (UnsafeSpan<T> other) { + if (other.length < length) throw new System.ArgumentException(); + unsafe { + // If length is zero, the pointers may be null, which is technically undefined behavior (but in practice usually fine) + if (length > 0) UnsafeUtility.MemCpy(other.ptr, ptr, (long)sizeof(T) * (long)length); + } + } + + /// <summary>Appends all elements in this span to the given list</summary> + public void CopyTo (List<T> buffer) { + if (buffer.Capacity < buffer.Count + Length) buffer.Capacity = buffer.Count + Length; + for (int i = 0; i < Length; i++) buffer.Add(this[i]); + } + + /// <summary> + /// Creates a new copy of the span allocated using the given allocator. + /// + /// You are responsible for freeing this memory using the same allocator when you are done with it. + /// </summary> + public UnsafeSpan<T> Clone (Allocator allocator) { + unsafe { + var clone = new UnsafeSpan<T>(allocator, (int)length); + CopyTo(clone); + return clone; + } + } + + /// <summary>Converts the span to a managed array</summary> + public T[] ToArray () { + var arr = new T[length]; + if (length > 0) { + unsafe { + fixed (T* ptr = arr) { + UnsafeUtility.MemCpy(ptr, this.ptr, (long)sizeof(T) * (long)length); + } + } + } + return arr; + } + + /// <summary> + /// Frees the underlaying memory. + /// + /// Warning: The span must have been allocated using the specified allocator. + /// + /// Warning: You must never use this span (or any other span referencing the same memory) again after calling this method. + /// </summary> + public unsafe void Free (Allocator allocator) { + if (length > 0) AllocatorManager.Free<T>(allocator, ptr, (int)length); + } + } + + public static class SpanExtensions { + public static void FillZeros<T>(this UnsafeSpan<T> span) where T : unmanaged { + unsafe { + if (span.length > 0) UnsafeUtility.MemSet(span.ptr, 0, (long)sizeof(T) * (long)span.length); + } + } + + public static void Fill<T>(this UnsafeSpan<T> span, T value) where T : unmanaged { + unsafe { + // This is wayy faster than a C# for loop (easily 10x faster). + // It is also faster than a burst loop (at least as long as the span is reasonably large). + // It also generates a lot less code than a burst for loop. + if (span.length > 0) { + // If this is too big, unity seems to overflow and crash internally + if ((long)sizeof(T) * (long)span.length > (long)int.MaxValue) throw new System.ArgumentException("Span is too large to fill"); + UnsafeUtility.MemCpyReplicate(span.ptr, &value, sizeof(T), (int)span.length); + } + } + } + + /// <summary> + /// Copies the contents of a NativeArray to this span. + /// The span must be large enough to hold the contents of the array. + /// </summary> + public static void CopyFrom<T>(this UnsafeSpan<T> span, NativeArray<T> array) where T : unmanaged { + CopyFrom(span, array.AsUnsafeReadOnlySpan()); + } + + /// <summary> + /// Copies the contents of another span to this span. + /// The span must be large enough to hold the contents of the array. + /// </summary> + public static void CopyFrom<T>(this UnsafeSpan<T> span, UnsafeSpan<T> other) where T : unmanaged { + if (other.Length > span.Length) throw new System.InvalidOperationException(); + if (other.Length == 0) return; + unsafe { + UnsafeUtility.MemCpy(span.ptr, other.ptr, (long)sizeof(T) * (long)other.Length); + } + } + + /// <summary> + /// Copies the contents of an array to this span. + /// The span must be large enough to hold the contents of the array. + /// </summary> + public static void CopyFrom<T>(this UnsafeSpan<T> span, T[] array) where T : unmanaged { + if (array.Length > span.Length) throw new System.InvalidOperationException(); + if (array.Length == 0) return; + unsafe { + var ptr = UnsafeUtility.PinGCArrayAndGetDataAddress(array, out var gcHandle); + UnsafeUtility.MemCpy(span.ptr, ptr, (long)sizeof(T) * (long)array.Length); + UnsafeUtility.ReleaseGCObject(gcHandle); + } + } + + /// <summary> + /// Converts an UnsafeAppendBuffer to a span. + /// The buffer must be a multiple of the element size. + /// + /// The span is a view of the buffer memory, so do not dispose the buffer while the span is in use. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeSpan<T> AsUnsafeSpan<T>(this UnsafeAppendBuffer buffer) where T : unmanaged { + unsafe { + var items = buffer.Length / UnsafeUtility.SizeOf<T>(); + if (items * UnsafeUtility.SizeOf<T>() != buffer.Length) throw new System.ArgumentException("Buffer length is not a multiple of the element size"); + return new UnsafeSpan<T>(buffer.Ptr, items); + } + } + + /// <summary> + /// Converts a NativeList to a span. + /// + /// The span is a view of the list memory, so do not dispose the list while the span is in use. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeSpan<T> AsUnsafeSpan<T>(this NativeList<T> list) where T : unmanaged { + unsafe { + return new UnsafeSpan<T>(list.GetUnsafePtr(), list.Length); + } + } + + /// <summary> + /// Converts a NativeArray to a span. + /// + /// The span is a view of the array memory, so do not dispose the array while the span is in use. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeSpan<T> AsUnsafeSpan<T>(this NativeArray<T> arr) where T : unmanaged { + unsafe { + return new UnsafeSpan<T>(arr.GetUnsafePtr(), arr.Length); + } + } + + /// <summary> + /// Converts a NativeArray to a span without performing any checks. + /// + /// The span is a view of the array memory, so do not dispose the array while the span is in use. + /// This method does not perform any checks to ensure that the array is safe to write to or read from. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeSpan<T> AsUnsafeSpanNoChecks<T>(this NativeArray<T> arr) where T : unmanaged { + unsafe { + return new UnsafeSpan<T>(NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(arr), arr.Length); + } + } + + /// <summary> + /// Converts a NativeArray to a span, assuming it will only be read. + /// + /// The span is a view of the array memory, so do not dispose the array while the span is in use. + /// + /// Warning: No checks are done to ensure that you only read from the array. You are responsible for ensuring that you do not write to the span. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeSpan<T> AsUnsafeReadOnlySpan<T>(this NativeArray<T> arr) where T : unmanaged { + unsafe { + return new UnsafeSpan<T>(arr.GetUnsafeReadOnlyPtr(), arr.Length); + } + } + + /// <summary> + /// Converts an UnsafeList to a span. + /// + /// The span is a view of the list memory, so do not dispose the list while the span is in use. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeSpan<T> AsUnsafeSpan<T>(this UnsafeList<T> arr) where T : unmanaged { + unsafe { + return new UnsafeSpan<T>(arr.Ptr, arr.Length); + } + } + + /// <summary> + /// Converts a NativeSlice to a span. + /// + /// The span is a view of the slice memory, so do not dispose the underlaying memory allocation while the span is in use. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeSpan<T> AsUnsafeSpan<T>(this NativeSlice<T> slice) where T : unmanaged { + unsafe { + return new UnsafeSpan<T>(slice.GetUnsafePtr(), slice.Length); + } + } + + /// <summary>Returns true if the value exists in the span</summary> + public static bool Contains<T>(this UnsafeSpan<T> span, T value) where T : unmanaged, System.IEquatable<T> { + return IndexOf(span, value) != -1; + } + + /// <summary> + /// Returns the index of the first occurrence of a value in the span. + /// If the value is not found, -1 is returned. + /// </summary> + public static int IndexOf<T>(this UnsafeSpan<T> span, T value) where T : unmanaged, System.IEquatable<T> { + unsafe { + return System.MemoryExtensions.IndexOf(new System.ReadOnlySpan<T>(span.ptr, (int)span.length), value); + } + } + + /// <summary>Sorts the span in ascending order</summary> + public static void Sort<T>(this UnsafeSpan<T> span) where T : unmanaged, System.IComparable<T> { + unsafe { + NativeSortExtension.Sort<T>(span.ptr, span.Length); + } + } + + /// <summary>Sorts the span in ascending order</summary> + public static void Sort<T, U>(this UnsafeSpan<T> span, U comp) where T : unmanaged where U : System.Collections.Generic.IComparer<T> { + unsafe { + NativeSortExtension.Sort<T, U>(span.ptr, span.Length, comp); + } + } + +#if !MODULE_COLLECTIONS_2_4_0_OR_NEWER + /// <summary>Shifts elements toward the end of this list, increasing its length</summary> + public static void InsertRange<T>(this NativeList<T> list, int index, int count) where T : unmanaged { + list.ResizeUninitialized(list.Length + count); + list.AsUnsafeSpan().Move(index, index + count, list.Length - (index + count)); + } +#endif + +#if !MODULE_COLLECTIONS_2_1_0_OR_NEWER + /// <summary>Appends value count times to the end of this list</summary> + public static void AddReplicate<T>(this NativeList<T> list, T value, int count) where T : unmanaged { + var origLength = list.Length; + list.ResizeUninitialized(origLength + count); + list.AsUnsafeSpan().Slice(origLength).Fill(value); + } +#endif + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/Span.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/Span.cs.meta new file mode 100644 index 0000000..de2fb18 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Collections/Span.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa5a1e493f0bf8946bc5fe477ef4710b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control.meta new file mode 100644 index 0000000..14d82e8 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 756b69de94c66c508a7195b291178ac7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/MovementUtilities.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/MovementUtilities.cs new file mode 100644 index 0000000..cf08a20 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/MovementUtilities.cs @@ -0,0 +1,208 @@ +using UnityEngine; + +namespace Pathfinding.Util { + public static class MovementUtilities { + public static float FilterRotationDirection (ref Vector2 state, ref Vector2 state2, Vector2 deltaPosition, float threshold, float deltaTime, bool avoidingOtherAgents) { + const float Decay = 0.5f; + + var lastState = state; + + if (!avoidingOtherAgents) { + // When not avoiding other agents, we can be a bit more aggressive with rotating towards the target. + // This is because in that case, the velocity is much less noisy. + state += deltaPosition * 10; + } else { + state += deltaPosition; + } + + // Decay the state slowly (has the most effect if the agent is standing still) + state *= Mathf.Clamp01(1.0f - deltaTime*Decay); + float stateLength = state.magnitude; + + if (stateLength > threshold*2f) { + state = state * (threshold*2f/stateLength); + stateLength = threshold*2f; + } + + // TODO: Figure out what to do with + state2 += (state - lastState) * Decay; + state2 *= Mathf.Clamp01(1.0f - deltaTime*Decay); + + // Prevent rotation if the agent doesn't move much + float speed = stateLength > threshold ? 1.0f : 0.0f; + return speed; + } + + /// <summary> + /// Clamps the velocity to the max speed and optionally the forwards direction. + /// + /// Note that all vectors are 2D vectors, not 3D vectors. + /// + /// Returns: The clamped velocity in world units per second. + /// </summary> + /// <param name="velocity">Desired velocity of the character. In world units per second.</param> + /// <param name="maxSpeed">Max speed of the character. In world units per second.</param> + /// <param name="speedLimitFactor">Value between 0 and 1 which determines how much slower the character should move than normal. + /// Normally 1 but should go to 0 when the character approaches the end of the path.</param> + /// <param name="slowWhenNotFacingTarget">Slow the character down if the desired velocity is not in the same direction as the forward vector.</param> + /// <param name="preventMovingBackwards">Prevent the velocity from being too far away from the forward direction of the character.</param> + /// <param name="forward">Forward direction of the character. Used together with the slowWhenNotFacingTarget parameter.</param> + public static Vector2 ClampVelocity (Vector2 velocity, float maxSpeed, float speedLimitFactor, bool slowWhenNotFacingTarget, bool preventMovingBackwards, Vector2 forward) { + // Max speed to use for this frame + var currentMaxSpeed = maxSpeed * speedLimitFactor; + + // Check if the agent should slow down in case it is not facing the direction it wants to move in + if (slowWhenNotFacingTarget && (forward.x != 0 || forward.y != 0)) { + float currentSpeed; + var normalizedVelocity = VectorMath.Normalize(velocity, out currentSpeed); + float dot = Vector2.Dot(normalizedVelocity, forward); + + // Lower the speed when the character's forward direction is not pointing towards the desired velocity + // 1 when velocity is in the same direction as forward + // 0.2 when they point in the opposite directions + float directionSpeedFactor = Mathf.Clamp(dot+0.707f, 0.2f, 1.0f); + currentMaxSpeed *= directionSpeedFactor; + currentSpeed = Mathf.Min(currentSpeed, currentMaxSpeed); + + if (preventMovingBackwards) { + // Angle between the forwards direction of the character and our desired velocity + float angle = Mathf.Acos(Mathf.Clamp(dot, -1, 1)); + + // Clamp the angle to 20 degrees + // We cannot keep the velocity exactly in the forwards direction of the character + // because we use the rotation to determine in which direction to rotate and if + // the velocity would always be in the forwards direction of the character then + // the character would never rotate. + // Allow larger angles when near the end of the path to prevent oscillations. + angle = Mathf.Min(angle, (20f + 180f*(1 - speedLimitFactor*speedLimitFactor))*Mathf.Deg2Rad); + + float sin = Mathf.Sin(angle); + float cos = Mathf.Cos(angle); + + // Determine if we should rotate clockwise or counter-clockwise to move towards the current velocity + sin *= Mathf.Sign(normalizedVelocity.x*forward.y - normalizedVelocity.y*forward.x); + // Rotate the #forward vector by #angle radians + // The rotation is done using an inlined rotation matrix. + // See https://en.wikipedia.org/wiki/Rotation_matrix + return new Vector2(forward.x*cos + forward.y*sin, forward.y*cos - forward.x*sin) * currentSpeed; + } else { + return normalizedVelocity * currentSpeed; + } + } else { + return Vector2.ClampMagnitude(velocity, currentMaxSpeed); + } + } + + /// <summary>Calculate an acceleration to move deltaPosition units and get there with approximately a velocity of targetVelocity</summary> + public static Vector2 CalculateAccelerationToReachPoint (Vector2 deltaPosition, Vector2 targetVelocity, Vector2 currentVelocity, float forwardsAcceleration, float rotationSpeed, float maxSpeed, Vector2 forwardsVector) { + // Guard against div by zero + if (forwardsAcceleration <= 0) return Vector2.zero; + + float currentSpeed = currentVelocity.magnitude; + + // Convert rotation speed to an acceleration + // See https://en.wikipedia.org/wiki/Centripetal_force + var sidewaysAcceleration = currentSpeed * rotationSpeed * Mathf.Deg2Rad; + + // To avoid weird behaviour when the rotation speed is very low we allow the agent to accelerate sideways without rotating much + // if the rotation speed is very small. Also guards against division by zero. + sidewaysAcceleration = Mathf.Max(sidewaysAcceleration, forwardsAcceleration); + + // Transform coordinates to local space where +X is the forwards direction + // This is essentially equivalent to Transform.InverseTransformDirection. + deltaPosition = VectorMath.ComplexMultiplyConjugate(deltaPosition, forwardsVector); + targetVelocity = VectorMath.ComplexMultiplyConjugate(targetVelocity, forwardsVector); + currentVelocity = VectorMath.ComplexMultiplyConjugate(currentVelocity, forwardsVector); + float ellipseSqrFactorX = 1 / (forwardsAcceleration*forwardsAcceleration); + float ellipseSqrFactorY = 1 / (sidewaysAcceleration*sidewaysAcceleration); + + // If the target velocity is zero we can use a more fancy approach + // and calculate a nicer path. + // In particular, this is the case at the end of the path. + if (targetVelocity == Vector2.zero) { + // Run a binary search over the time to get to the target point. + float mn = 0.01f; + float mx = 10; + while (mx - mn > 0.01f) { + var time = (mx + mn) * 0.5f; + + // Given that we want to move deltaPosition units from out current position, that our current velocity is given + // and that when we reach the target we want our velocity to be zero. Also assume that our acceleration will + // vary linearly during the slowdown. Then we can calculate what our acceleration should be during this frame. + + //{ t = time + //{ deltaPosition = vt + at^2/2 + qt^3/6 + //{ 0 = v + at + qt^2/2 + //{ solve for a + // a = acceleration vector + // q = derivative of the acceleration vector + var a = (6*deltaPosition - 4*time*currentVelocity)/(time*time); + var q = 6*(time*currentVelocity - 2*deltaPosition)/(time*time*time); + + // Make sure the acceleration is not greater than our maximum allowed acceleration. + // If it is we increase the time we want to use to get to the target + // and if it is not, we decrease the time to get there faster. + // Since the acceleration is described by acceleration = a + q*t + // we only need to check at t=0 and t=time. + // Note that the acceleration limit is described by an ellipse, not a circle. + var nextA = a + q*time; + if (a.x*a.x*ellipseSqrFactorX + a.y*a.y*ellipseSqrFactorY > 1.0f || nextA.x*nextA.x*ellipseSqrFactorX + nextA.y*nextA.y*ellipseSqrFactorY > 1.0f) { + mn = time; + } else { + mx = time; + } + } + + var finalAcceleration = (6*deltaPosition - 4*mx*currentVelocity)/(mx*mx); + + // Boosting + { + // The trajectory calculated above has a tendency to use very wide arcs + // and that does unfortunately not look particularly good in some cases. + // Here we amplify the component of the acceleration that is perpendicular + // to our current velocity. This will make the agent turn towards the + // target quicker. + // How much amplification to use. Value is unitless. + const float Boost = 1; + finalAcceleration.y *= 1 + Boost; + + // Clamp the velocity to the maximum acceleration. + // Note that the maximum acceleration constraint is shaped like an ellipse, not like a circle. + float ellipseMagnitude = finalAcceleration.x*finalAcceleration.x*ellipseSqrFactorX + finalAcceleration.y*finalAcceleration.y*ellipseSqrFactorY; + if (ellipseMagnitude > 1.0f) finalAcceleration /= Mathf.Sqrt(ellipseMagnitude); + } + + return VectorMath.ComplexMultiply(finalAcceleration, forwardsVector); + } else { + // Here we try to move towards the next waypoint which has been modified slightly using our + // desired velocity at that point so that the agent will more smoothly round the corner. + + // How much to strive for making sure we reach the target point with the target velocity. Unitless. + const float TargetVelocityWeight = 0.5f; + + // Limit to how much to care about the target velocity. Value is in seconds. + // This prevents the character from moving away from the path too much when the target point is far away + const float TargetVelocityWeightLimit = 1.5f; + float targetSpeed; + var normalizedTargetVelocity = VectorMath.Normalize(targetVelocity, out targetSpeed); + + var distance = deltaPosition.magnitude; + var targetPoint = deltaPosition - normalizedTargetVelocity * System.Math.Min(TargetVelocityWeight * distance * targetSpeed / (currentSpeed + targetSpeed), maxSpeed*TargetVelocityWeightLimit); + + // How quickly the agent will try to reach the velocity that we want it to have. + // We need this to prevent oscillations and jitter which is what happens if + // we let the constant go towards zero. Value is in seconds. + const float TimeToReachDesiredVelocity = 0.1f; + // TODO: Clamp to ellipse using more accurate acceleration (use rotation speed as well) + var finalAcceleration = (targetPoint.normalized*maxSpeed - currentVelocity) * (1f/TimeToReachDesiredVelocity); + + // Clamp the velocity to the maximum acceleration. + // Note that the maximum acceleration constraint is shaped like an ellipse, not like a circle. + float ellipseMagnitude = finalAcceleration.x*finalAcceleration.x*ellipseSqrFactorX + finalAcceleration.y*finalAcceleration.y*ellipseSqrFactorY; + if (ellipseMagnitude > 1.0f) finalAcceleration /= Mathf.Sqrt(ellipseMagnitude); + + return VectorMath.ComplexMultiply(finalAcceleration, forwardsVector); + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/MovementUtilities.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/MovementUtilities.cs.meta new file mode 100644 index 0000000..bb4e60d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/MovementUtilities.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 0a6cffd9895f94907aa43f18b0904587 +timeCreated: 1490097740 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs new file mode 100644 index 0000000..3dd76dd --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs @@ -0,0 +1,1110 @@ +using UnityEngine; +using Unity.Mathematics; +using System.Collections.Generic; +using Unity.Collections; +using UnityEngine.Profiling; + +namespace Pathfinding.PID { + using Pathfinding.Drawing; + using Pathfinding.Util; + using Palette = Pathfinding.Drawing.Palette.Colorbrewer.Set1; + using Unity.Jobs; + using Unity.Profiling; + using UnityEngine.Assertions; + using Unity.Burst; + using Unity.Collections.LowLevel.Unsafe; + using Pathfinding.RVO; + + /// <summary>Core control loop for the <see cref="FollowerEntity"/> movement script</summary> + [System.Serializable] + [BurstCompile] + public struct PIDMovement { + public struct PersistentState { + public float maxDesiredWallDistance; + } + + /// <summary> + /// Desired rotation speed in degrees per second. + /// + /// If the agent is in an open area and gets a new destination directly behind itself, it will start to rotate around with exactly this rotation speed. + /// + /// The agent will slow down its rotation speed as it approaches its desired facing direction. + /// So for example, when it is only 90 degrees away from its desired facing direction, it will only rotate with about half this speed. + /// + /// See: <see cref="maxRotationSpeed"/> + /// </summary> + public float rotationSpeed; + + /// <summary> + /// Desired speed of the agent in meters per second. + /// + /// This will be multiplied by the agent's scale to get the actual speed. + /// </summary> + public float speed; + + /// <summary> + /// Maximum rotation speed in degrees per second. + /// + /// If the agent would have to rotate faster than this, it will instead slow down to get more time to rotate. + /// + /// The agent may want to rotate faster than <see cref="rotationSpeed"/> if there's not enough space, so that it has to move in a more narrow arc. + /// It may also want to rotate faster if it is very close to its destination and it wants to make sure it ends up on the right spot without any circling. + /// + /// It is recommended to keep this at a value slightly greater than <see cref="rotationSpeed"/>. + /// + /// See: <see cref="rotationSpeed"/> + /// </summary> + public float maxRotationSpeed; + + /// <summary> + /// Maximum rotation speed in degrees per second while rotating on the spot. + /// + /// Only used if <see cref="allowRotatingOnSpot"/> is enabled. + /// </summary> + public float maxOnSpotRotationSpeed; + + /// <summary> + /// Time for the agent to slow down to a complete stop when it approaches the destination point, in seconds. + /// + /// One can calculate the deceleration like: <see cref="speed"/>/<see cref="slowdownTime"/> (with units m/s^2). + /// </summary> + public float slowdownTime; + + /// <summary> + /// Time for the agent to slow down to a complete stop when it decides to change direction by turning on the spot + /// + /// If set to zero, the agent will instantly stop and start to turn around. + /// + /// Only used if <see cref="allowRotatingOnSpot"/> is enabled. + /// </summary> + public float slowdownTimeWhenTurningOnSpot; + + /// <summary> + /// How big of a distance to try to keep from obstacles. + /// + /// Typically around 1 or 2 times the agent radius is a good value for this. + /// + /// Try to avoid making it so large that there might not be enough space for the agent to keep this amount of distance from obstacles. + /// It may start to move less optimally if it is not possible to keep this distance. + /// + /// This works well in open spaces, but if your game consists of a lot of tight corridors, a low, or zero value may be better. + /// + /// This will be multiplied by the agent's scale to get the actual distance. + /// </summary> + public float desiredWallDistance; + + /// <summary> + /// How wide of a turn to make when approaching a destination for which a desired facing direction has been set. + /// + /// The following video shows three agents, one with no facing direction set, and then two agents with varying values of the lead in radius. + /// [Open online documentation to see videos] + /// + /// Setting this to zero will make the agent move directly to the end of the path and rotate on the spot to face the desired facing direction, once it is there. + /// + /// When approaching a destination for which no desired facing direction has been set, this field has no effect. + /// + /// Warning: Setting this to a too small (but non-zero) value may look bad if the agent cannot rotate fast enough to stay on the arc. + /// + /// This will be multiplied by the agent's scale to get the actual radius. + /// </summary> + public float leadInRadiusWhenApproachingDestination; + + /// <summary>If rotation on the spot is allowed or not</summary> + public bool allowRotatingOnSpot { + get => allowRotatingOnSpotBacking != 0; + set => allowRotatingOnSpotBacking = (byte)(value ? 1 : 0); + } + + /// <summary> + /// If rotation on the spot is allowed or not. + /// 1 for allowed, 0 for not allowed. + /// + /// That we have to use a byte instead of a boolean is due to a Burst limitation. + /// </summary> + [SerializeField] + byte allowRotatingOnSpotBacking; + + public const float DESTINATION_CLEARANCE_FACTOR = 4f; + + private static readonly ProfilerMarker MarkerSidewaysAvoidance = new ProfilerMarker("SidewaysAvoidance"); + private static readonly ProfilerMarker MarkerPID = new ProfilerMarker("PID"); + private static readonly ProfilerMarker MarkerOptimizeDirection = new ProfilerMarker("OptimizeDirection"); + private static readonly ProfilerMarker MarkerSmallestDistance = new ProfilerMarker("ClosestDistance"); + private static readonly ProfilerMarker MarkerConvertObstacles = new ProfilerMarker("ConvertObstacles"); + + [System.Flags] + public enum DebugFlags { + Nothing = 0, + Position = 1 << 0, + Tangent = 1 << 1, + SidewaysClearance = 1 << 2, + ForwardClearance = 1 << 3, + Obstacles = 1 << 4, + Funnel = 1 << 5, + Path = 1 << 6, + ApproachWithOrientation = 1 << 7, + Rotation = 1 << 8, + } + + public void ScaleByAgentScale (float agentScale) { + speed *= agentScale; + leadInRadiusWhenApproachingDestination *= agentScale; + desiredWallDistance *= agentScale; + } + + public float Speed (float remainingDistance) { + if (speed <= 0) return 0; + if (this.slowdownTime <= 0) return remainingDistance <= 0.0001f ? 0 : speed; + + // This is what you get if you apply a constant deceleration per unit of time of this.speed/this.slowdownTime + float slowdownFactor = Mathf.Min(1.0f, Mathf.Sqrt(2 * remainingDistance / (speed * this.slowdownTime))); + + var res = speed * slowdownFactor; + Assert.IsTrue(math.isfinite(res)); + return res; + } + + /// <summary> + /// Accelerates as quickly as possible. + /// + /// This follows the same curve as the <see cref="Speed"/> function, as a function of the remaining distance. + /// + /// Returns: The speed the agent should have after accelerating for dt seconds. Assuming dt is small. + /// </summary> + /// <param name="speed">The current speed of the agent.</param> + /// <param name="timeToReachMaxSpeed">The time it takes for the agent to reach the maximum speed, starting from a standstill.</param> + /// <param name="dt">The time to accelerate for. Can be negative to decelerate instead.</param> + public float Accelerate (float speed, float timeToReachMaxSpeed, float dt) { + // This can be derived by assuming a constant deceleration per unit of time: + // x''(t) = A + // Integrating twice gives us + // x'(t) = A * t + // x(t) = A * t^2 / 2 + // + // Ensuring it yields the same output as the Speed function gives us the equation: + // x'(t) = S * sqrt(2 * x(t) / (S * T)) + // A * t = S * sqrt(2 * (A * t^2 / 2) / (S * T)) + // + // Which yields the acceleration when solved: + // A = S / T + if (timeToReachMaxSpeed > 0.001f) { + var a = this.speed / timeToReachMaxSpeed; + return math.clamp(speed + dt * a, 0, this.speed); + } else { + return dt > 0 ? this.speed : 0; + } + } + + public float CurveFollowingStrength (float signedDistToClearArea, float radiusToWall, float remainingDistance) { + var speed = math.max(0.00001f, this.speed); + var followingStrength = AnglePIDController.RotationSpeedToFollowingStrength(speed, math.radians(this.rotationSpeed)); + var modifiedAlpha = math.max(followingStrength, 40.0f * math.pow(math.abs(signedDistToClearArea) / math.max(0.0001f, radiusToWall), 1)); + var remainingTime = remainingDistance / speed; + + // Just before reaching the end of the path, the agent should try to follow the path very closely to avoid overshooting, + // and potentially spinning in place. + const float HIGH_EFFORT_TIME = 0.2f; + modifiedAlpha = math.max(modifiedAlpha, math.min(80.0f, math.pow(1.0f / math.max(0, remainingTime - HIGH_EFFORT_TIME), 3))); + + Assert.IsTrue(math.isfinite(modifiedAlpha)); + return modifiedAlpha; + } + + static bool ClipLineByHalfPlaneX (ref float2 a, ref float2 b, float x, float side) { + var wrongSideA = (a.x - x)*side < 0; + var wrongSideB = (b.x - x)*side < 0; + if (wrongSideA && wrongSideB) return false; + if (wrongSideA != wrongSideB) { + var t = math.unlerp(a.x, b.x, x); + var intersection = math.lerp(a, b, t); + if (wrongSideA) a = intersection; + else b = intersection; + } + return true; + } + + static void ClipLineByHalfPlaneYt (float2 a, float2 b, float y, float side, ref float mnT, ref float mxT) { + var wrongSideA = (a.y - y)*side < 0; + var wrongSideB = (b.y - y)*side < 0; + if (wrongSideA && wrongSideB) { + mnT = 1; + mxT = 0; + } else if (wrongSideA != wrongSideB) { + var t = math.unlerp(a.y, b.y, y); + if (wrongSideA) mnT = math.max(mnT, t); + else mxT = math.min(mxT, t); + } + } + + /// <summary> + /// Returns either the most clockwise, or most counter-clockwise direction of the three given directions. + /// The directions are compared pairwise, not using any global reference angle. + /// </summary> + static float2 MaxAngle (float2 a, float2 b, float2 c, bool clockwise) { + a = math.select(a, b, VectorMath.Determinant(a, b) < 0 == clockwise); + a = math.select(a, c, VectorMath.Determinant(a, c) < 0 == clockwise); + return a; + } + + /// <summary> + /// Returns either the most clockwise, or most counter-clockwise direction of the two given directions. + /// The directions are compared pairwise, not using any global reference angle. + /// </summary> + static float2 MaxAngle (float2 a, float2 b, bool clockwise) { + return math.select(a, b, VectorMath.Determinant(a, b) < 0 == clockwise); + } + + const float ALLOWED_OVERLAP_FACTOR = 0.1f; + const float STEP_MULTIPLIER = 1.0f; + const float MAX_FRACTION_OF_REMAINING_DISTANCE = 0.9f; + const int OPTIMIZATION_ITERATIONS = 8; + + static void DrawChisel (float2 start, float2 direction, float pointiness, float length, float width, CommandBuilder draw, Color col) { + draw.PushColor(col); + var cornerL = start + (direction * pointiness + new float2(-direction.y, direction.x)) * width; + var cornerR = start + (direction * pointiness - new float2(-direction.y, direction.x)) * width; + draw.xz.Line(start, cornerL, col); + draw.xz.Line(start, cornerR, col); + var remainingLength = length - pointiness * width; + if (remainingLength > 0) { + draw.xz.Ray(cornerL, direction * remainingLength, col); + draw.xz.Ray(cornerR, direction * remainingLength, col); + } + draw.PopColor(); + } + + static void SplitSegment (float2 e1, float2 e2, float desiredRadius, float length, float pointiness, ref EdgeBuffers buffers) { + // Check if it is completely outside the range we concern ourselves with. + // When the direction is rotated, we may end up caring about segments further to the side than #desiredRadius, but with + // a safety margin of 2, we should catch all potential segments that we care about. + float radiusWithMargin = desiredRadius * 2f; + if ((e1.y < -radiusWithMargin && e2.y < -radiusWithMargin) || (e1.y > radiusWithMargin && e2.y > radiusWithMargin)) return; + + // Remove the part of the segment that is behind the agent + if (!ClipLineByHalfPlaneX(ref e1, ref e2, 0, 1)) return; + + // We don't care about any segments further away than #length + if (!VectorMath.SegmentCircleIntersectionFactors(e1, e2, length*length, out var t1, out var t2)) { + // Completely outside the circle + return; + } + + // Remove the parts of segments that are really close to the agent. + // Otherwise it can try to aggressively avoid segments that are super close, but are only obstacles due to minimal floating point errors. + var thresholdRadius = desiredRadius*0.01f; + if (VectorMath.SegmentCircleIntersectionFactors(e1, e2, thresholdRadius*thresholdRadius, out var tInner1, out var tInner2) && tInner1 < t2 && tInner2 > t1) { + // Remove the intersection with the inner circle. + // This may split the segment into 0, 1 or 2 parts. + if (tInner1 > t1 && tInner1 < t2) SplitSegment2(math.lerp(e1, e2, t1), math.lerp(e1, e2, tInner1), desiredRadius, pointiness, ref buffers); + if (tInner2 > t1 && tInner2 < t2) SplitSegment2(math.lerp(e1, e2, tInner2), math.lerp(e1, e2, t2), desiredRadius, pointiness, ref buffers); + } else { + // No intersection with the inner circle. This is the common case. + SplitSegment2(math.lerp(e1, e2, t1), math.lerp(e1, e2, t2), desiredRadius, pointiness, ref buffers); + } + } + + static void SplitSegment2 (float2 e1, float2 e2, float desiredRadius, float pointiness, ref EdgeBuffers buffers) { + // The shape that we use for avoidance looks like this: + // __________ + // / + // / + // \ + // \ __________ + // + // With the agent at the pointy end of the shape. + // Here we check if the segment overlaps the triangular part of the shape, as defined by a circle with the same radius + // as the sides of the triangle. + if (VectorMath.SegmentCircleIntersectionFactors(e1, e2, (pointiness*pointiness + 1)*desiredRadius*desiredRadius, out var t1, out var t2)) { + // Split the segment at the intersection with the circle + // This may split the segment into 0, 1, 2 or 3 parts. + if (t1 > 0.0f && t2 < 1.0f) { + SplitSegment3(e1, math.lerp(e1, e2, t1), desiredRadius, false, ref buffers); + SplitSegment3(math.lerp(e1, e2, t1), math.lerp(e1, e2, t2), desiredRadius, true, ref buffers); + SplitSegment3(math.lerp(e1, e2, t2), e2, desiredRadius, false, ref buffers); + } else if (t1 > 0.0f) { + SplitSegment3(e1, math.lerp(e1, e2, t1), desiredRadius, false, ref buffers); + SplitSegment3(math.lerp(e1, e2, t1), e2, desiredRadius, true, ref buffers); + } else if (t2 < 1.0f) { + SplitSegment3(e1, math.lerp(e1, e2, t2), desiredRadius, true, ref buffers); + SplitSegment3(math.lerp(e1, e2, t2), e2, desiredRadius, false, ref buffers); + } else { + // Whole segment + SplitSegment3(e1, e2, desiredRadius, true, ref buffers); + } + } else { + // Outside the circle + SplitSegment3(e1, e2, desiredRadius, false, ref buffers); + } + } + + static void SplitSegment3 (float2 e1, float2 e2, float desiredRadius, bool inTriangularRegion, ref EdgeBuffers buffers) { + // Check the orientation of the segment, and bias it so that the agent is + // more like to try to pass on the "correct" side of the segment. + // All obstacle edges that we get from the navmesh are oriented. + // However, we only bias the segment when we calculate which side the segment is on, + // and when calculating the intersection factor. After that, we return to using + // the original segment. + // + // If we don't do this, then the agent can get stuck at the border of the navmesh. + // Even if the agent is clamped to the navmesh, it may end up a tiiny bit outside it, + // and then it would try to avoid the border of the navmesh by moving even further outside it. + var r1 = e1; + var r2 = e2; + if (r2.x < r1.x) { + r1.y -= 0.01f; + r2.y -= 0.01f; + } else { + r1.y += 0.01f; + r2.y += 0.01f; + } + + var e1Left = r1.y > 0; + + // Ensure e1 is to the left of the midpoint line + if (!e1Left) { + Memory.Swap(ref e1, ref e2); + Memory.Swap(ref r1, ref r2); + } + + // Intersection of the line e1 -> e2 with the line y=0 + var tIntersection = math.unlerp(r1.y, r2.y, 0f); + + var anyIntersection = math.isfinite(tIntersection); + if (tIntersection <= 0.0f || tIntersection >= 1.0f || !anyIntersection) { + // No intersection + SplitSegment4(e1, e2, inTriangularRegion, e1Left, ref buffers); + } else { + // Intersection. Split the segment into two parts, one for the left side, and one for the right side. + var intersection = e1 + tIntersection * (e2 - e1); + var l1 = math.lengthsq(e1 - intersection); + var l2 = math.lengthsq(e2 - intersection); + var allowedLineOverlap = desiredRadius * ALLOWED_OVERLAP_FACTOR; + float allowedLineOverlapSq = allowedLineOverlap*allowedLineOverlap; + + // Check both the left and right subsegments. Ignore them if they are really short. + if (l1 > allowedLineOverlapSq || l1 >= l2) SplitSegment4(e1, intersection, inTriangularRegion, true, ref buffers); + if (l2 > allowedLineOverlapSq || l2 >= l1) SplitSegment4(intersection, e2, inTriangularRegion, false, ref buffers); + } + } + + static void SplitSegment4 (float2 e1, float2 e2, bool inTriangularRegion, bool left, ref EdgeBuffers buffers) { + // Ignore tiiiny edges + // Not quite sure when they get generated, but they do exist. + // Including these can cause issues if end up almost, but not quite, on the midpoint line, + // near the end. + // Ideally we'd have some better code for tolerating cases when there's only a tiny obstacle on the left/right side. + if (math.all(math.abs(e1 - e2) < 0.01f)) return; + + ref var buffer = ref buffers.triangleRegionEdgesL; + if (inTriangularRegion) { + if (left) {} // NOOP + else buffer = ref buffers.triangleRegionEdgesR; + } else { + if (left) buffer = ref buffers.straightRegionEdgesL; + else buffer = ref buffers.straightRegionEdgesR; + } + + if (buffer.Length + 2 > buffer.Capacity) return; + buffer.AddNoResize(e1); + buffer.AddNoResize(e2); + } + + struct EdgeBuffers { + public FixedList512Bytes<float2> triangleRegionEdgesL; + public FixedList512Bytes<float2> triangleRegionEdgesR; + public FixedList512Bytes<float2> straightRegionEdgesL; + public FixedList512Bytes<float2> straightRegionEdgesR; + } + + /// <summary> + /// Finds a direction to move in that is as close as possible to the desired direction while being clear of obstacles, if possible. + /// This keeps the agent from moving too close to walls. + /// </summary> + /// <param name="start">Current position of the agent.</param> + /// <param name="end">Point the agent is moving towards.</param> + /// <param name="desiredRadius">The distance the agent should try to keep from obstacles.</param> + /// <param name="remainingDistance">Remaining distance in the path.</param> + /// <param name="pointiness">Essentially controls how much the agent will cut corners. A higher value will lead to a smoother path, + /// but it will also lead to the agent not staying as far away from corners as the desired wall distance parameter would suggest. + /// It is a unitless quantity.</param> + /// <param name="edges">Edges of obstacles. Each edge is represented by two points.</param> + /// <param name="draw">CommandBuilder to use for drawing debug information.</param> + /// <param name="debugFlags">Flags to control what debug information to draw.</param> + public static float2 OptimizeDirection (float2 start, float2 end, float desiredRadius, float remainingDistance, float pointiness, NativeArray<float2> edges, CommandBuilder draw, DebugFlags debugFlags) { + var length = math.length(end - start); + var direction0 = math.normalizesafe(end - start); + length *= 0.999f; + length = math.min(MAX_FRACTION_OF_REMAINING_DISTANCE * remainingDistance, length); + if (desiredRadius <= 0.0001f) return direction0; + + var lengthOrig = length; + var lengthInvOrig = 1 / lengthOrig; + + // Pre-process all edges by splitting them up and grouping them by zone. + // We have 4 zones that we care about: + // 1. Within the triangular region near the agent, on the left side of the line from #start to #end + // 2. Within the triangular region near the agent, on the right side + // 3. Outside the triangular region, on the left side + // 4. Outside the triangular region, on the right side + // We assume that about 32 edges for each zone is enough. If we find more edges, the remainder will be discarded. + // Usually there are only a few edges, so this is not a problem in practice. + var buffers = new EdgeBuffers(); + for (int i = 0; i < edges.Length; i += 2) { + // Rotate the edge so that the x-axis corresponds to #direction0 + var e1 = VectorMath.ComplexMultiplyConjugate(edges[i] - start, direction0); + var e2 = VectorMath.ComplexMultiplyConjugate(edges[i+1] - start, direction0); + SplitSegment(e1, e2, desiredRadius, length, pointiness, ref buffers); + } + + // if ((debugFlags & DebugFlags.ForwardClearance) != 0) { + // for (int i = 0; i < buffers.straightRegionEdgesL.Length; i += 2) { + // draw.xz.Line(start + VectorMath.ComplexMultiply(buffers.straightRegionEdgesL[i], direction0), start + VectorMath.ComplexMultiply(buffers.straightRegionEdgesL[i+1], direction0), Palette.Orange); + // } + // for (int i = 0; i < buffers.straightRegionEdgesR.Length; i += 2) { + // draw.xz.Line(start + VectorMath.ComplexMultiply(buffers.straightRegionEdgesR[i], direction0), start + VectorMath.ComplexMultiply(buffers.straightRegionEdgesR[i+1], direction0), Palette.Red); + // } + // for (int i = 0; i < buffers.triangleRegionEdgesL.Length; i += 2) { + // draw.xz.Line(start + VectorMath.ComplexMultiply(buffers.triangleRegionEdgesL[i], direction0), start + VectorMath.ComplexMultiply(buffers.triangleRegionEdgesL[i+1], direction0), Palette.Pink); + // } + // for (int i = 0; i < buffers.triangleRegionEdgesR.Length; i += 2) { + // draw.xz.Line(start + VectorMath.ComplexMultiply(buffers.triangleRegionEdgesR[i], direction0), start + VectorMath.ComplexMultiply(buffers.triangleRegionEdgesR[i+1], direction0), Palette.Purple); + // } + // } + + // Complex number representing how much to rotate the original direction by. + // The number (1,0) indicates no rotation. + var direction = new float2(1, 0); + + // The optimization usually converges very quickly. Error is approximately O(0.5^n) + for (int it = 0; it < OPTIMIZATION_ITERATIONS; it++) { + if ((debugFlags & DebugFlags.ForwardClearance) != 0) { + var col = Palette.Blue; + col.a = 0.5f; + var d = VectorMath.ComplexMultiply(direction, direction0); + DrawChisel(start, d, pointiness, length, desiredRadius, draw, col); + draw.xz.Ray(start, d * length, Palette.Purple); + draw.xz.Circle(start, remainingDistance, col); + } + + var leftReference = new float2(0, desiredRadius); + var rightReference = new float2(0, -desiredRadius); + var leftObstacleDir = new float2(length, 0); + var rightObstacleDir = new float2(length, 0); + + // Iterate through all edges and calculate how much we need to rotate the direction to avoid them. + // We store all directions as complex numbers. + for (int i = 0; i < buffers.straightRegionEdgesL.Length; i += 2) { + // Rotate the edge so that the x-axis corresponds to #direction + var e1 = VectorMath.ComplexMultiplyConjugate(buffers.straightRegionEdgesL[i], direction); + var e2 = VectorMath.ComplexMultiplyConjugate(buffers.straightRegionEdgesL[i+1], direction); + leftObstacleDir = MaxAngle(leftObstacleDir, e1 - leftReference, e2 - leftReference, true); + } + + for (int i = 0; i < buffers.straightRegionEdgesR.Length; i += 2) { + var e1 = VectorMath.ComplexMultiplyConjugate(buffers.straightRegionEdgesR[i], direction); + var e2 = VectorMath.ComplexMultiplyConjugate(buffers.straightRegionEdgesR[i+1], direction); + rightObstacleDir = MaxAngle(rightObstacleDir, e1 - rightReference, e2 - rightReference, false); + } + + var referenceDiagonalL = math.normalizesafe(VectorMath.ComplexMultiply(new float2(pointiness*desiredRadius, desiredRadius), direction)); + var referenceDiagonalR = math.normalizesafe(VectorMath.ComplexMultiply(new float2(pointiness*desiredRadius, -desiredRadius), direction)); + for (int i = 0; i < buffers.triangleRegionEdgesL.Length; i += 2) { + // Rotate the edge so that the x-axis corresponds to #referenceDiagonalL + var offset1 = VectorMath.ComplexMultiplyConjugate(buffers.triangleRegionEdgesL[i], referenceDiagonalL); + var offset2 = VectorMath.ComplexMultiplyConjugate(buffers.triangleRegionEdgesL[i+1], referenceDiagonalL); + var offset = offset2.y < offset1.y ? offset2 : offset1; + if (offset.y < 0) leftObstacleDir = MaxAngle(leftObstacleDir, offset, true); + } + + for (int i = 0; i < buffers.triangleRegionEdgesR.Length; i += 2) { + var offset1 = VectorMath.ComplexMultiplyConjugate(buffers.triangleRegionEdgesR[i], referenceDiagonalR); + var offset2 = VectorMath.ComplexMultiplyConjugate(buffers.triangleRegionEdgesR[i+1], referenceDiagonalR); + var offset = offset2.y > offset1.y ? offset2 : offset1; + if (offset.y > 0) rightObstacleDir = MaxAngle(rightObstacleDir, offset, false); + } + + // Do some kind of weighted average of the two directions. + // Here we map the length of the obstacle directions as 0=>0 and L=>infinity (but we clamp it to a finite but large value). + // Basically we want to give more weight to obstacles closer to the agent. + var leftInverseWeight = 1 / math.max(0.000001f, lengthOrig - leftObstacleDir.x*leftObstacleDir.x) - lengthInvOrig; + var rightInverseWeight = 1 / math.max(0.000001f, lengthOrig - rightObstacleDir.x*rightObstacleDir.x) - lengthInvOrig; + var rTot = math.normalizesafe(leftObstacleDir * rightInverseWeight + rightObstacleDir * leftInverseWeight); + + // Alternative averaging which only takes the sum of the angles + // var rTot2 = math.normalizesafe(VectorMath.ComplexMultiply(leftObstacleDir, rightObstacleDir)); + + // Approximately multiplying the angle by STEP_MULTIPLIER + var rStep = math.lerp(new float2(1, 0), rTot, STEP_MULTIPLIER); + direction = math.normalizesafe(VectorMath.ComplexMultiply(direction, rStep)); + if (leftObstacleDir.y == 0 && rightObstacleDir.y == 0) { + // Apparently there were NO obstacles. + // We can afford to increase our length check a little bit. + // This is important in case we encounter a corner which is on a very pointy obstacle. + // _______ + // _ _ _ _ / + // A _ _ _ _ <______ + // + // Where A is the agent, trying to move towards the corner marked with a '<'. + // In that case, we will find no edges to avoid and we will end up moving directly towards the corner instead + // of staying slightly away from the walls. Unless we increase the length check a little bit, that is. + // + // However, we don't want to increase the length more than the remaining distance to the target minus a small margin, + // as that can cause weird movement when approaching a target near a wall. It would try to unnecessarily avoid the wall + // causing ocillating movement. + length = math.min(remainingDistance * MAX_FRACTION_OF_REMAINING_DISTANCE, math.min(length * 1.1f, lengthOrig * 1.2f)); + } else { + // Decrease the length a bit, to bias the optimization towards closer obstacles + length = math.min(length, math.max(desiredRadius * 2.0f, math.min(leftObstacleDir.x, rightObstacleDir.x) * 2.0f)); + } + } + + direction = VectorMath.ComplexMultiply(direction, direction0); + + if ((debugFlags & DebugFlags.ForwardClearance) != 0) { + DrawChisel(start, direction, pointiness, length, desiredRadius, draw, Color.black); + } + + Assert.IsTrue(!math.any(math.isnan(direction))); + return direction; + } + + /// <summary> + /// Calculates the closest point on any point of an edge that is inside a wedge. + /// + /// Returns: The distance to the closest point on any edge that is inside the wedge. + /// </summary> + /// <param name="point">The origin point of the wedge (the pointy end).</param> + /// <param name="dir1">The first direction of the wedge.</param> + /// <param name="dir2">The second direction of the wedge.</param> + /// <param name="shrinkAmount">The wedge is shrunk by this amount. In the same units as the input points.</param> + /// <param name="edges">The edges to check for intersection with.</param> + public static float SmallestDistanceWithinWedge (float2 point, float2 dir1, float2 dir2, float shrinkAmount, NativeArray<float2> edges) { + dir1 = math.normalizesafe(dir1); + dir2 = math.normalizesafe(dir2); + + // Early out in case the wedge is very narrow. + // This is primarily a performance optimization. + // If the agent is almost facing the correct direction, then it shouldn't be heading towards an obstacle. + const float MIN_ANGLE_COS = 0.999f; + if (math.dot(dir1, dir2) > MIN_ANGLE_COS) return float.PositiveInfinity; + + var side = math.sign(VectorMath.Determinant(dir1, dir2)); + shrinkAmount *= side; + + var closestDistanceSq = float.PositiveInfinity; + for (int i = 0; i < edges.Length; i += 2) { + var e1 = edges[i] - point; + var e2 = edges[i+1] - point; + + // Clip the line by the two half planes that the wedge consists of + var e1a = VectorMath.ComplexMultiplyConjugate(e1, dir1); + var e2a = VectorMath.ComplexMultiplyConjugate(e2, dir1); + var e1b = VectorMath.ComplexMultiplyConjugate(e1, dir2); + var e2b = VectorMath.ComplexMultiplyConjugate(e2, dir2); + var mnT = 0f; + var mxT = 1f; + + ClipLineByHalfPlaneYt(e1a, e2a, shrinkAmount, side, ref mnT, ref mxT); + if (mnT > mxT) continue; + ClipLineByHalfPlaneYt(e1b, e2b, -shrinkAmount, -side, ref mnT, ref mxT); + if (mnT > mxT) continue; + + // Find the distance to the closest point on the clipped line segment + var lengthsq = math.lengthsq(e2 - e1); + var t = math.clamp(math.dot(e1, e1 - e2) * math.rcp(lengthsq), mnT, mxT); + var d = math.lengthsq(math.lerp(e1, e2, t)); + closestDistanceSq = math.select(closestDistanceSq, math.min(closestDistanceSq, d), lengthsq > math.FLT_MIN_NORMAL); + } + Assert.IsTrue(!float.IsNaN(closestDistanceSq)); + return math.sqrt(closestDistanceSq); + } + + public static float2 Linecast (float2 a, float2 b, NativeArray<float2> edges) { + var k = 1f; + for (int i = 0; i < edges.Length; i += 2) { + var e1 = edges[i]; + var e2 = edges[i+1]; + VectorMath.LineLineIntersectionFactors(a, b - a, e1, e2 - e1, out var t1, out var t2); + if (t2 >= 0 && t2 <= 1 && t1 > 0) { + k = math.min(k, t1); + } + } + return a + (b - a) * k; + } + + public struct ControlParams { + public Vector3 p; + public float speed; + public float rotation; + public float maxDesiredWallDistance; + public float3 endOfPath; + public float3 facingDirectionAtEndOfPath; + public NativeArray<float2> edges; + public float3 nextCorner; + public float agentRadius; + public float remainingDistance; + public float3 closestOnNavmesh; + public DebugFlags debugFlags; + public NativeMovementPlane movementPlane; + } + + /// <summary> + /// Finds the bounding box in which this controller is interested in navmesh edges. + /// + /// The edges should be assigned to <see cref="ControlParams.edges"/>. + /// The bounding box is relative to the given movement plane. + /// </summary> + public static Bounds InterestingEdgeBounds (ref PIDMovement settings, float3 position, float3 nextCorner, float height, NativeMovementPlane plane) { + // Convert the position and next corner to local space, relative to the movement plane + var localPos = math.mul(math.conjugate(plane.rotation), position); + var localNextCorner = math.mul(math.conjugate(plane.rotation), nextCorner); + // Default bounds which extend from 1/2*height below the agent to the agent's head (assuming its pivot is at the agent's feet). + var localBounds = new Bounds(localPos + new float3(0, height * 0.25f, 0), new Vector3(0, 1.5f*height, 0)); + // Don't allow the next corner to push the bounding box up or down too much, since that can let us include obstacle edges + // that are e.g. on a floor below or a floor above the agent. Especially if the agent is currently moving on a sloped surface. + localNextCorner.y = localPos.y; + + localBounds.Encapsulate(localNextCorner); + // If an agent needs to make a full 180 degree turn, then we need a diameter instead of a radius. + // However since the agent gets a lower rotation speed the closer it gets to the desired rotation, + // this factor of two gets sort of compensated for already. + if (settings.rotationSpeed > 0) { + var approximateTurningDiameter = settings.speed / math.radians(settings.rotationSpeed); //2.0f * AnglePIDController.ApproximateTurningRadius(settings.followingStrength); + // + localBounds.Expand(new Vector3(1, 0, 1) * math.max(approximateTurningDiameter, settings.desiredWallDistance * OPTIMIZATION_ITERATIONS * STEP_MULTIPLIER)); + } + return localBounds; + } + + static float2 OffsetCornerForApproach (float2 position2D, float2 endOfPath2D, float2 facingDir2D, ref PIDMovement settings, float2 nextCorner2D, ref float gammaAngle, ref float gammaAngleWeight, DebugFlags debugFlags, ref CommandBuilder draw, NativeArray<float2> edges) { + var d1 = endOfPath2D - position2D; + + // Cosine of the approach angle that is considered too steep to try to turn in an arc + const float STEEP_ANGLE_THRESHOLD_COS = -0.2f; + + if (math.dot(math.normalizesafe(d1), facingDir2D) < STEEP_ANGLE_THRESHOLD_COS) { + // Too steep + return nextCorner2D; + } + + // Line orthogonal to d1 + var n1 = new float2(-d1.y, d1.x); + + // Line orthogonal to facingDir2D + var n2 = new float2(-facingDir2D.y, facingDir2D.x); + var mid = (position2D + endOfPath2D) * 0.5f; + + // Find the center of the circle which touches both the points endOfPath2D and position2D, and has a tangent parallel to facingDir2D at endOfPath2D. + var circleCenter = (float2)VectorMath.LineIntersectionPoint(mid, mid + n1, endOfPath2D, endOfPath2D + n2, out bool intersects); + + if (!intersects) return nextCorner2D; + + // Do not try to approach the destination with a large arc if there might be an obstacle in the way + // Check within a wedge and offset it sliightly backwards to take care of the case when the end of the path + // is right at the end of the navmesh. This is a common case when for example ordering an agent to interact + // with some prop. + // + // Agent + // | | + // <-x----/ + // | + // + var distToObstacle = SmallestDistanceWithinWedge(endOfPath2D - 0.01f * facingDir2D, n2 - 0.1f * facingDir2D, -n2 - 0.1f * facingDir2D, 0.001f, edges); + var maxRadius = settings.leadInRadiusWhenApproachingDestination; + maxRadius = math.min(maxRadius, distToObstacle * 0.9f); + var circleRadius = math.length(circleCenter - endOfPath2D); + + // Calculate the intersection point of the two tangents of the circle, one at endOfPath2D and one at position2D. + // Offset is the distance from endOfPath2D to the intersection point + var dot = math.abs(math.dot(math.normalizesafe(d1), n2)); + var offset = 1.0f / math.sqrt(1 - dot*dot) * math.length(d1) * 0.5f; + + // Tweak the offset slightly to account for the maximum radius. + // Limit the radius using a smooth thresholding function. + offset /= math.min(maxRadius, circleRadius); + offset = math.tanh(offset); + offset *= math.min(maxRadius, circleRadius); + + // Offset the next corner backwards along the facing direction, + // so that the agent will approach the destination along a curve. + var newNextCorner2D = nextCorner2D - facingDir2D * offset; + + if ((debugFlags & DebugFlags.ApproachWithOrientation) != 0) { + draw.xz.Circle(circleCenter, circleRadius, Color.blue); + draw.xz.Arrow(position2D, newNextCorner2D, Palette.Orange); + } + + // If the new corner is not visible from the agent's current position, + // then return the original corner, as we do not want to try to walk into a wall. + if (math.lengthsq(Linecast(position2D, newNextCorner2D, edges) - newNextCorner2D) > 0.01f) { + return nextCorner2D; + } else { + return newNextCorner2D; + } + } + + public static AnglePIDControlOutput2D Control (ref PIDMovement settings, float dt, ref ControlParams controlParams, ref CommandBuilder draw, out float maxDesiredWallDistance) { + if (dt <= 0) { + // If the game is paused, then do not move or rotate. + maxDesiredWallDistance = controlParams.maxDesiredWallDistance; + return new AnglePIDControlOutput2D { + rotationDelta = 0, + positionDelta = float2.zero, + }; + } + var movementPlane = controlParams.movementPlane; + var position2D = movementPlane.ToPlane(controlParams.p, out float positionElevation); + + // If we are drawing any debug information, push a matrix so that we can draw in local space. + // If not, skip pushing the matrix to improve performance. + if (controlParams.debugFlags != 0) draw.PushMatrix(math.mul(new float4x4(movementPlane.rotation, float3.zero), float4x4.Translate(new float3(0, positionElevation, 0)))); + + if ((controlParams.debugFlags & DebugFlags.Position) != 0) { + draw.xz.Cross(controlParams.closestOnNavmesh, 0.05f, Color.red); + } + + var edges = controlParams.edges; + if ((controlParams.debugFlags & DebugFlags.Obstacles) != 0) { + draw.PushLineWidth(2); + draw.PushColor(Color.red); + for (int i = 0; i < edges.Length; i += 2) { + draw.xz.Line(edges[i], edges[i+1]); + } + draw.PopColor(); + draw.PopLineWidth(); + } + + var nextCorner2D = movementPlane.ToPlane(controlParams.nextCorner); + float gamma = 0; + float gammaAngle = 0; + float gammaAngleWeight = 0; + // +Y is our forward direction, so add 90 degrees so that rotation2D = curveAngle means we are following the curve. + // Mathematically it makes much more sense if rotations are relative to the +X axis. So we use this convention internally. + var rotation2D = controlParams.rotation + Mathf.PI / 2; + var facingDir2D = math.normalizesafe(movementPlane.ToPlane(controlParams.facingDirectionAtEndOfPath)); + bool isVeryCloseToEndOfPath = controlParams.remainingDistance < controlParams.agentRadius*0.1f; + + if (!isVeryCloseToEndOfPath && settings.leadInRadiusWhenApproachingDestination > 0 && math.any(facingDir2D != 0)) { + var endOfPath2D = movementPlane.ToPlane(controlParams.endOfPath); + bool isAtLastCorner = math.lengthsq(endOfPath2D - nextCorner2D) <= 0.1f; + if (isAtLastCorner) { + var c1 = OffsetCornerForApproach( + position2D, + endOfPath2D, + facingDir2D, + ref settings, + nextCorner2D, + ref gammaAngle, + ref gammaAngleWeight, + controlParams.debugFlags, + ref draw, + edges + ); + nextCorner2D = c1; + + var simDx = settings.speed * 0.1f; + if (simDx > 0.001f) { + math.sincos(rotation2D, out var sin, out var cos); + var forward = new float2(cos, sin); + var c2 = OffsetCornerForApproach( + position2D + forward * simDx, + endOfPath2D, + facingDir2D, + ref settings, + nextCorner2D, + ref gammaAngle, + ref gammaAngleWeight, + DebugFlags.Nothing, + ref draw, + edges + ); + + // Calculate the number of radians between c1 and c2 from the agent's perspective. + // This is the amount that the agent must rotate to stay on the desired curve. + var s = VectorMath.Determinant(math.normalizesafe(c1 - position2D), math.normalizesafe(c2 - position2D)); + gamma = math.asin(s)/simDx; + } + } + } + + var desiredForwardClearanceRadius = settings.desiredWallDistance; + desiredForwardClearanceRadius = math.max(0, math.min(desiredForwardClearanceRadius, (controlParams.remainingDistance - desiredForwardClearanceRadius) / DESTINATION_CLEARANCE_FACTOR)); + MarkerOptimizeDirection.Begin(); + + // In case the next corner is not visible from the agent's current position, then instead move towards the first intersection with an obstacle. + // This is important in some cases even when one would think that the next corner should be visible. + // This is because when unwrapping and flattening the funnel, the next corner may end up being move slightly due to various projections. + // This may cause it to end up inside a wall. If we didn't use a linecast here, the OptimizeDirection function + // would likely just give up and the agent would not stay away from the wall as it should. + nextCorner2D = Linecast(position2D, nextCorner2D, edges); + + const float Pointiness = 2f; + var estimatedForward = OptimizeDirection(position2D, nextCorner2D, desiredForwardClearanceRadius, controlParams.remainingDistance, Pointiness, edges, draw, controlParams.debugFlags); + MarkerOptimizeDirection.End(); + + // Increase the maxDesiredWallDistance over time, to slowly push the agent away from walls. + maxDesiredWallDistance = controlParams.maxDesiredWallDistance + settings.speed * 0.1f * dt; + var desiredPositionClearance = maxDesiredWallDistance; + var signedDist = 0f; + var signedDistToClearArea = 0f; + maxDesiredWallDistance = math.min(maxDesiredWallDistance, desiredPositionClearance); + + if ((controlParams.debugFlags & DebugFlags.Tangent) != 0) { + draw.Arrow(controlParams.p, controlParams.p + new Vector3(estimatedForward.x, 0, estimatedForward.y), Palette.Orange); + } + + AnglePIDControlOutput2D output; + if (isVeryCloseToEndOfPath) { + // When we are really close to the endpoint, move directly towards the end and do not rotate (unless a facing direction has been set). + + // Accelerate, but only up to the very low speed we use when we are very close to the endpoint. + // We must be able to accelerate here, as otherwise we may never reach the endpoint if we started + // very close to the endpoint with zero speed. + var speed = math.min(settings.Speed(controlParams.remainingDistance), settings.Accelerate(controlParams.speed, settings.slowdownTime, dt)); + + // TODO: Maybe add a settling mechanic. Once we are really close, lock the destination and do not change it until it gets a certain minimum distance away from the agent + // This would avoid the agent drifting without rotating to follow a destination that moves slowly. + var dirToEnd = nextCorner2D - position2D; + var distToEnd = math.length(dirToEnd); + if (math.any(facingDir2D != 0)) { + var desiredAngle = math.atan2(facingDir2D.y, facingDir2D.x); + var maxRotationDelta = dt * math.radians(settings.maxRotationSpeed); + output = new AnglePIDControlOutput2D { + rotationDelta = math.clamp(AstarMath.DeltaAngle(rotation2D, desiredAngle), -maxRotationDelta, maxRotationDelta), + // Convert back to a rotation convention where +Y is forward + targetRotation = desiredAngle - Mathf.PI / 2, + positionDelta = distToEnd > math.FLT_MIN_NORMAL ? dirToEnd * (dt * speed / distToEnd) : dirToEnd, + }; + } else { + output = new AnglePIDControlOutput2D { + rotationDelta = 0, + // Convert back to a rotation convention where +Y is forward + targetRotation = rotation2D - Mathf.PI / 2, + positionDelta = distToEnd > math.FLT_MIN_NORMAL ? dirToEnd * (dt * speed / distToEnd) : dirToEnd, + }; + } + } else { + var modifiedFollowingStrength = settings.CurveFollowingStrength(signedDistToClearArea, desiredPositionClearance, controlParams.remainingDistance); + var curveAngle = math.atan2(estimatedForward.y, estimatedForward.x); + + var minimumRotationSpeed = 0f; + // If we are not perfectly facing our desired direction, we need to rotate to face it. + // We try to ensure we will not hit any obstacles by checking for nearby obstacles + // in the direction we are moving. If there are any obstacles, we can calculate + // the approximate rotation speed we need to have to avoid them. + // + // If we are very close to our desired facing direction, we skip this check + // to improve performance. + if (math.abs(AstarMath.DeltaAngle(curveAngle, rotation2D)) > math.PI*0.001f) { + math.sincos(rotation2D, out var sin, out var cos); + var forward = new float2(cos, sin); + var closestWithinWedge = SmallestDistanceWithinWedge(position2D, estimatedForward, forward, controlParams.agentRadius*0.1f, edges); + + if ((controlParams.debugFlags & DebugFlags.ForwardClearance) != 0 && float.IsFinite(closestWithinWedge)) { + draw.xz.Arc(position2D, position2D + forward * closestWithinWedge, position2D + estimatedForward, Palette.Purple); + } + + if (closestWithinWedge > 0.001f && closestWithinWedge*1.01f < controlParams.remainingDistance) { + const float SAFETY_FACTOR = 2.0f; + minimumRotationSpeed = math.rcp(closestWithinWedge) * SAFETY_FACTOR; + } + } + + MarkerPID.Begin(); + output = AnglePIDController.Control( + ref settings, + modifiedFollowingStrength, + rotation2D, + curveAngle + AstarMath.DeltaAngle(curveAngle, gammaAngle) * gammaAngleWeight, + gamma, + signedDist, + controlParams.speed, + controlParams.remainingDistance, + minimumRotationSpeed, + controlParams.speed < settings.speed*0.1f, + dt + ); + // Convert back to a rotation convention where +Y is forward + output.targetRotation -= Mathf.PI / 2; + MarkerPID.End(); + } + if (controlParams.debugFlags != 0) draw.PopMatrix(); + return output; + } + } + + /// <summary> + /// Implements a PID controller for the angular velocity of an agent following a curve. + /// + /// The PID controller is formulated for small angles (see https://en.wikipedia.org/wiki/Small-angle_approximation), but extends well to large angles. + /// For small angles, if y(t) is the curve/agent position, then y'(t) is the angle and y''(t) is the angular velocity. + /// This controller outputs an angular velocity, meaning it controls y''(t). + /// + /// See https://en.wikipedia.org/wiki/PID_controller + /// </summary> + public static class AnglePIDController { + const float DampingRatio = 1.0f; + + /// <summary> + /// An approximate turning radius the agent will have in an open space. + /// + /// This is based on the PID controller in the <see cref="Control"/> method. + /// </summary> + public static float ApproximateTurningRadius (float followingStrength) { + // With dampingRatio = 1, this will result in critical damping + var alpha = followingStrength; + var beta = 2 * math.sqrt(math.abs(alpha)) * DampingRatio; + + // Some sort of mean value + // If a character turns around, the angleToCurveError will go from math.PI to 0. + const float angleToCurveError = math.PI * 0.5f; + + return 1.0f/(beta * angleToCurveError); + } + + /// <summary> + /// Given a speed and a rotation speed, what is the approximate corresponding following strength. + /// + /// This is based on the PID controller in the <see cref="Control"/> method. + /// </summary> + public static float RotationSpeedToFollowingStrength (float speed, float maxRotationSpeed) { + // Using the following identity: + // turningRadius = speed/rotationSpeed + // and using the implementation for ApproximateTurningRadius, we can solve for the rotation speed + // and we get the expression below. + + // Note that we use a different angleToCurveError here compared to in ApproximateTurningRadius. + // This is because here we use the maximum angleToCurveError that could happen, while in ApproximateTurningRadius + // we use an average value. This is reasonable because the input to this method is + // the maximum rotation speed, not the average rotation speed. + const float angleToCurveError = math.PI; + var k = maxRotationSpeed / (2.0f * angleToCurveError * speed * DampingRatio); + var alpha = k * k; + return alpha; + } + + public static float FollowingStrengthToRotationSpeed (float followingStrength) { + return 1.0f / (ApproximateTurningRadius(followingStrength) * 0.5f); + } + + /// <summary> + /// How much to rotate and move in order to smoothly follow a given curve. + /// + /// If the maximum rotation speed (settings.maxRotationSpeed) would be exceeded, the agent will slow down to avoid exceeding it (up to a point). + /// + /// Returns: A control value that can be used to move the agent. + /// </summary> + /// <param name="settings">Various movement settings</param> + /// <param name="followingStrength">The integral term of the PID controller. The higher this value is, the quicker the agent will try to align with the curve.</param> + /// <param name="angle">The current direction of the agent, in radians.</param> + /// <param name="curveAngle">The angle of the curve tangent at the nearest point, in radians.</param> + /// <param name="curveCurvature">The curvature of the curve at the nearest point. Positive values means the curve is turning to the left, negative values means the curve is turning to the right.</param> + /// <param name="curveDistanceSigned">The signed distance from the agent to the curve. Positive values means the agent is to the right of the curve, negative values means the agent is to the left of the curve.</param> + /// <param name="speed">How quickly the agent should move. In meters/second.</param> + /// <param name="remainingDistance">The remaining distance to where the agent should stop. In meters.</param> + /// <param name="minRotationSpeed">The minimum rotation speed of the agent. In radians/second. Unless the agent does not desire to rotate at all, it will rotate at least this fast.</param> + /// <param name="isStationary">Should be true if the agent is currently standing still (or close to it). This allows it to rotate in place.</param> + /// <param name="dt">How long the current time-step is. In seconds.</param> + public static AnglePIDControlOutput2D Control (ref PIDMovement settings, float followingStrength, float angle, float curveAngle, float curveCurvature, float curveDistanceSigned, float speed, float remainingDistance, float minRotationSpeed, bool isStationary, float dt) { + Assert.IsTrue(math.isfinite(angle)); + Assert.IsTrue(math.isfinite(curveAngle)); + Assert.IsTrue(math.isfinite(curveDistanceSigned)); + Assert.IsTrue(math.isfinite(curveCurvature)); + Assert.IsTrue(minRotationSpeed >= 0); + + // With dampingRatio = 1, this will result in critical damping + var alpha = followingStrength; + var beta = 2 * math.sqrt(math.abs(alpha)) * DampingRatio; + var gamma = 1.0f; + var angleToCurveError = AstarMath.DeltaAngle(angle, curveAngle); + var angleTowardsCurve = curveAngle + math.sign(curveDistanceSigned) * math.PI * 0.5f; + var deltaAngleTowardsCurve = AstarMath.DeltaAngle(angle, angleTowardsCurve); // TODO: Divide by PI/2? + + // Desired primary rotation in radians per meter + var alphaAngle = alpha * math.abs(curveDistanceSigned) * deltaAngleTowardsCurve; + // Desired primary rotation during this timestep + var alphaAngleDelta = alphaAngle * speed * dt; + + // Desired secondary rotation in radians per meter + var betaAngle = beta * angleToCurveError; + + // Assuming that an agent is stationary, the rotation of the agent will reach a steady state after a short while (alphaAngle + betaAngle = 0). + // This is the remaining angle we have left until we reach that steady state. + var denominator = beta + alpha * math.abs(curveDistanceSigned); + var remainingAngle = denominator > math.FLT_MIN_NORMAL ? (betaAngle + alphaAngle)/denominator : 0; + Assert.IsTrue(math.isfinite(remainingAngle)); + float.IsFinite(remainingAngle); + + // If the agent has to rotate *a lot* then stop moving and rotate in-place. + // Once we are rotating in place, we should continue doing that until we are almost facing the desired direction. + isStationary = settings.allowRotatingOnSpot && (math.abs(remainingAngle) > math.PI*0.6666f || (isStationary && math.abs(remainingAngle) > 0.1f)); + if (isStationary) { + var newSpeed = settings.Accelerate(speed, settings.slowdownTimeWhenTurningOnSpot, -dt); + var maxOnSpotRotationSpeed = math.radians(settings.maxOnSpotRotationSpeed); + var canRotateInOneStep = maxOnSpotRotationSpeed*dt > math.abs(remainingAngle); + if (newSpeed > 0 && !canRotateInOneStep) { + // Slow down as quickly as possible + return AnglePIDControlOutput2D.WithMovementAtEnd( + currentRotation: angle, + targetRotation: angle, + rotationDelta: 0, + moveDistance: newSpeed * dt + ); + } else { + // If we are rotating in place, rotate with the maximum rotation speed + return AnglePIDControlOutput2D.WithMovementAtEnd( + currentRotation: angle, + targetRotation: angle + remainingAngle, + rotationDelta: math.clamp(remainingAngle, -maxOnSpotRotationSpeed*dt, maxOnSpotRotationSpeed*dt), + // Check if we can rotate in place in one time-step. If so, skip standing still for this time-step. + moveDistance: canRotateInOneStep ? speed * dt : 0.0f + ); + } + } + + speed = math.min(settings.Speed(remainingDistance), settings.Accelerate(speed, settings.slowdownTime, dt)); + + if (math.abs(angleToCurveError) > math.PI*0.5f) { + // Ensures that if the agent is moving in the completely wrong direction, it will not continue doing that + // because the alpha term tells it to move left, and the beta term tells it to move right, cancelling each other out. + alphaAngleDelta = 0; + } + + if (math.abs(betaAngle) > 0.0001f) { + betaAngle = math.max(math.abs(betaAngle), minRotationSpeed) * math.sign(betaAngle); + } + + var betaAngleDelta = betaAngle * speed * dt; + // The weights are "how much we want to rotate this timestep, divided by the maximum amount of rotation that is allowed" + // This is used to avoid overshooting when following strengths are very high or the fps is low. + var alphaWeight = math.abs(alphaAngleDelta / deltaAngleTowardsCurve); + var betaWeight = math.abs(betaAngleDelta / angleToCurveError); + var gammaWeight = 1.0f; + var directionComponentInCurveDirection = math.max(0, math.cos(angleToCurveError)); + var speedMultiplier = 1.0f; + var moveDistance = speed * speedMultiplier * dt; + var curvatureIntegral = curveCurvature * moveDistance; + var gammaAngleDelta = gamma * curvatureIntegral * directionComponentInCurveDirection; + // Don't allow individual contributions to contribute more than their limit (e.g. overshooting their rotation target). + // But still keep the relative contribution proportions the same. + var overflowWeight = math.max(1f, math.max(alphaWeight, math.max(betaWeight, gammaWeight))); + var angleDelta = (gammaAngleDelta + betaAngleDelta + alphaAngleDelta) / overflowWeight; + + // If we would have rotated too quickly, slow down the agent + var maxRotationSpeed = math.radians(settings.maxRotationSpeed); + var rotationMultiplier = math.max(0.1f, math.min(1.0f, maxRotationSpeed*dt / math.abs(angleDelta))); + + Assert.IsTrue(math.isfinite(angle)); + Assert.IsTrue(math.isfinite(rotationMultiplier)); + Assert.IsTrue(math.isfinite(angleDelta)); + Assert.IsTrue(math.isfinite(moveDistance)); + + return new AnglePIDControlOutput2D( + currentRotation: angle, + targetRotation: angle + remainingAngle, + rotationDelta: angleDelta * rotationMultiplier, + moveDistance: moveDistance * rotationMultiplier + ); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs.meta new file mode 100644 index 0000000..9080220 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d19693db79b5eedab18d1362b58bca1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs new file mode 100644 index 0000000..3695b37 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs @@ -0,0 +1,48 @@ +using UnityEngine; +using Unity.Mathematics; +using UnityEngine.Assertions; +using Pathfinding.Util; + +namespace Pathfinding.PID { + public struct AnglePIDControlOutput2D { + /// <summary>How much to rotate in a single time-step. In radians.</summary> + public float rotationDelta; + public float targetRotation; + /// <summary>How much to move in a single time-step. In world units.</summary> + public float2 positionDelta; + + public AnglePIDControlOutput2D(float currentRotation, float targetRotation, float rotationDelta, float moveDistance) { + var midpointRotation = currentRotation + rotationDelta * 0.5f; + math.sincos(midpointRotation, out float s, out float c); + this.rotationDelta = rotationDelta; + this.positionDelta = new float2(c, s) * moveDistance; + this.targetRotation = targetRotation; + } + + public static AnglePIDControlOutput2D WithMovementAtEnd (float currentRotation, float targetRotation, float rotationDelta, float moveDistance) { + var finalRotation = currentRotation + rotationDelta; + math.sincos(finalRotation, out float s, out float c); + return new AnglePIDControlOutput2D { + rotationDelta = rotationDelta, + targetRotation = targetRotation, + positionDelta = new float2(c, s) * moveDistance, + }; + } + } + + public struct AnglePIDControlOutput { + /// <summary>How much to rotate in a single time-step</summary> + public quaternion rotationDelta; + /// <summary>How much to move in a single time-step. In world units.</summary> + public float3 positionDelta; + public float maxDesiredWallDistance; + + public AnglePIDControlOutput(NativeMovementPlane movementPlane, AnglePIDControlOutput2D control2D) { + this.rotationDelta = movementPlane.ToWorldRotationDelta(-control2D.rotationDelta); + this.positionDelta = movementPlane.ToWorld(control2D.positionDelta, 0); + this.maxDesiredWallDistance = 0; + Assert.IsTrue(math.all(math.isfinite(rotationDelta.value))); + Assert.IsTrue(math.all(math.isfinite(positionDelta))); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs.meta new file mode 100644 index 0000000..5641b86 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3e01131736f2ac179398e1f408f94eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS.meta new file mode 100644 index 0000000..5dfeeec --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4551b5f18d9b68d428cf336b3ab3c38b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components.meta new file mode 100644 index 0000000..662ec62 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c496a3fafab9fab4a8c139fc7295f219 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentCylinderShape.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentCylinderShape.cs new file mode 100644 index 0000000..9909d0f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentCylinderShape.cs @@ -0,0 +1,18 @@ +#if MODULE_ENTITIES +using Unity.Entities; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.ECS.RVO; + + /// <summary>An agent's shape represented as a cylinder</summary> + [System.Serializable] + public struct AgentCylinderShape : IComponentData { + /// <summary>Radius of the agent in world units</summary> + public float radius; + + /// <summary>Height of the agent in world units</summary> + public float height; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentCylinderShape.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentCylinderShape.cs.meta new file mode 100644 index 0000000..aa81963 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentCylinderShape.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9dd6a4018eb50a48b69d83cb69a09b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlane.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlane.cs new file mode 100644 index 0000000..3321294 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlane.cs @@ -0,0 +1,30 @@ +#if MODULE_ENTITIES +using Unity.Entities; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.Util; + using Unity.Mathematics; + + /// <summary>Holds an agent's movement plane</summary> + [System.Serializable] + public struct AgentMovementPlane : IComponentData { + /// <summary> + /// The movement plane for the agent. + /// + /// The movement plane determines what the "up" direction of the agent is. + /// For most typical 3D games, this will be aligned with the Y axis, but there are + /// games in which the agent needs to navigate on walls, or on spherical worlds. + /// For those games this movement plane will track the plane in which the agent is currently moving. + /// + /// See: spherical (view in online documentation for working links) + /// </summary> + public NativeMovementPlane value; + + /// <summary>Create a movement plane aligned with the XZ plane of the specified rotation</summary> + public AgentMovementPlane (quaternion rotation) { + value = new NativeMovementPlane(rotation); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlane.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlane.cs.meta new file mode 100644 index 0000000..9e6ebba --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlane.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7137ab6e696b37428b1bec8e09e78ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlaneSource.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlaneSource.cs new file mode 100644 index 0000000..1bcfbb4 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlaneSource.cs @@ -0,0 +1,15 @@ +#if MODULE_ENTITIES +using Unity.Entities; + +namespace Pathfinding.ECS { + /// <summary> + /// The movement plane source for an agent. + /// + /// See: <see cref="MovementPlaneSource"/> + /// </summary> + [System.Serializable] + public struct AgentMovementPlaneSource : ISharedComponentData { + public MovementPlaneSource value; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlaneSource.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlaneSource.cs.meta new file mode 100644 index 0000000..e3a1b88 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentMovementPlaneSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a2198aa10fd82b94db223e3dbec9352b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentOffMeshLinkTraversal.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentOffMeshLinkTraversal.cs new file mode 100644 index 0000000..0b79f59 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentOffMeshLinkTraversal.cs @@ -0,0 +1,388 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.Util; + using Unity.Collections.LowLevel.Unsafe; + using UnityEngine; + + /// <summary> + /// Holds unmanaged information about an off-mesh link that the agent is currently traversing. + /// This component is added to the agent when it starts traversing an off-mesh link. + /// It is removed when the agent has finished traversing the link. + /// + /// See: <see cref="ManagedAgentOffMeshLinkTraversal"/> + /// </summary> + public struct AgentOffMeshLinkTraversal : IComponentData { + /// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeStart}</summary> + public float3 relativeStart; + + /// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeEnd}</summary> + public float3 relativeEnd; + + /// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeStart}. Deprecated: Use relativeStart instead</summary> + [System.Obsolete("Use relativeStart instead")] + public float3 firstPosition => relativeStart; + + /// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeEnd}. Deprecated: Use relativeEnd instead</summary> + [System.Obsolete("Use relativeEnd instead")] + public float3 secondPosition => relativeEnd; + + /// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.isReverse}</summary> + public bool isReverse; + + public AgentOffMeshLinkTraversal (OffMeshLinks.OffMeshLinkTracer linkInfo) { + relativeStart = linkInfo.relativeStart; + relativeEnd = linkInfo.relativeEnd; + isReverse = linkInfo.isReverse; + } + } + + /// <summary> + /// Holds managed information about an off-mesh link that the agent is currently traversing. + /// This component is added to the agent when it starts traversing an off-mesh link. + /// It is removed when the agent has finished traversing the link. + /// + /// See: <see cref="AgentOffMeshLinkTraversal"/> + /// </summary> + public class ManagedAgentOffMeshLinkTraversal : IComponentData, System.ICloneable, ICleanupComponentData { + /// <summary>Internal context used to pass component data to the coroutine</summary> + public AgentOffMeshLinkTraversalContext context; + + /// <summary>Coroutine which is used to traverse the link</summary> + public System.Collections.IEnumerator coroutine; + public IOffMeshLinkHandler handler; + public IOffMeshLinkStateMachine stateMachine; + + public ManagedAgentOffMeshLinkTraversal() {} + + public ManagedAgentOffMeshLinkTraversal (AgentOffMeshLinkTraversalContext context, IOffMeshLinkHandler handler) { + this.context = context; + this.handler = handler; + this.coroutine = null; + this.stateMachine = null; + } + + public object Clone () { + // This will set coroutine and stateMachine to null. + // This is correct, as the coroutine cannot be cloned, and the state machine may be unique for a specific agent + return new ManagedAgentOffMeshLinkTraversal((AgentOffMeshLinkTraversalContext)context.Clone(), handler); + } + } + + public struct MovementTarget { + internal bool isReached; + public bool reached => isReached; + + public MovementTarget (bool isReached) { + this.isReached = isReached; + } + } + + /// <summary> + /// Context with helpers for traversing an off-mesh link. + /// + /// This will be passed to the code that is responsible for traversing the off-mesh link. + /// + /// Warning: This context should never be accessed outside of an implementation of the <see cref="IOffMeshLinkStateMachine"/> interface. + /// </summary> + public class AgentOffMeshLinkTraversalContext : System.ICloneable { + internal unsafe AgentOffMeshLinkTraversal* linkInfoPtr; + internal unsafe MovementControl* movementControlPtr; + internal unsafe MovementSettings* movementSettingsPtr; + internal unsafe LocalTransform* transformPtr; + internal unsafe AgentMovementPlane* movementPlanePtr; + + /// <summary>The entity that is traversing the off-mesh link</summary> + public Entity entity; + + /// <summary>Some internal state of the agent</summary> + [Unity.Properties.DontCreateProperty] + public ManagedState managedState; + + /// <summary> + /// The off-mesh link that is being traversed. + /// + /// See: <see cref="link"/> + /// </summary> + [Unity.Properties.DontCreateProperty] + internal OffMeshLinks.OffMeshLinkConcrete concreteLink; + + protected bool disabledRVO; + protected float backupRotationSmoothing = float.NaN; + + /// <summary> + /// Delta time since the last link simulation. + /// + /// During high time scales, the simulation may run multiple substeps per frame. + /// + /// This is not the same as Time.deltaTime. Inside the link coroutine, you should always use this field instead of Time.deltaTime. + /// </summary> + public float deltaTime; + + protected GameObject gameObjectCache; + + /// <summary> + /// GameObject associated with the agent. + /// + /// In most cases, an agent is associated with an agent, but this is not always the case. + /// For example, if you have created an entity without using the <see cref="FollowerEntity"/> component, this property may return null. + /// + /// Note: When directly modifying the agent's transform during a link traversal, you should use the <see cref="transform"/> property instead of modifying the GameObject's transform. + /// </summary> + public virtual GameObject gameObject { + get { + if (gameObjectCache == null) { + var follower = BatchedEvents.Find<FollowerEntity, Entity>(entity, (follower, entity) => follower.entity == entity); + if (follower != null) gameObjectCache = follower.gameObject; + } + return gameObjectCache; + } + } + + /// <summary>ECS LocalTransform component attached to the agent</summary> + public ref LocalTransform transform { + get { + unsafe { + return ref *transformPtr; + } + } + } + + /// <summary>The movement settings for the agent</summary> + public ref MovementSettings movementSettings { + get { + unsafe { + return ref *movementSettingsPtr; + } + } + } + + /// <summary> + /// How the agent should move. + /// + /// The agent will move according to this data, every frame. + /// </summary> + public ref MovementControl movementControl { + get { + unsafe { + return ref *movementControlPtr; + } + } + } + + /// <summary>Information about the off-mesh link that the agent is traversing</summary> + public OffMeshLinks.OffMeshLinkTracer link { + get { + unsafe { + return new OffMeshLinks.OffMeshLinkTracer(concreteLink, linkInfoPtr->relativeStart, linkInfoPtr->relativeEnd, linkInfoPtr->isReverse); + } + } + } + + /// <summary> + /// Information about the off-mesh link that the agent is traversing. + /// + /// Deprecated: Use the <see cref="link"/> property instead + /// </summary> + [System.Obsolete("Use the link property instead")] + public AgentOffMeshLinkTraversal linkInfo { + get { + unsafe { + return *linkInfoPtr; + } + } + } + + /// <summary> + /// The plane in which the agent is moving. + /// + /// In a 3D game, this will typically be the XZ plane, but in a 2D game + /// it will typically be the XY plane. Games on spherical planets could have planes that are aligned with the surface of the planet. + /// </summary> + public ref NativeMovementPlane movementPlane { + get { + unsafe { + return ref movementPlanePtr->value; + } + } + } + + public AgentOffMeshLinkTraversalContext (OffMeshLinks.OffMeshLinkConcrete link) { + this.concreteLink = link; + } + + /// <summary> + /// Internal method to set the data of the context. + /// + /// This is used by the job system to set the data of the context. + /// You should almost never need to use this. + /// </summary> + public virtual unsafe void SetInternalData (Entity entity, ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref MovementControl movementControl, ref MovementSettings movementSettings, ref AgentOffMeshLinkTraversal linkInfo, ManagedState state, float deltaTime) { + this.linkInfoPtr = (AgentOffMeshLinkTraversal*)UnsafeUtility.AddressOf(ref linkInfo); + this.movementControlPtr = (MovementControl*)UnsafeUtility.AddressOf(ref movementControl); + this.movementSettingsPtr = (MovementSettings*)UnsafeUtility.AddressOf(ref movementSettings); + this.transformPtr = (LocalTransform*)UnsafeUtility.AddressOf(ref transform); + this.movementPlanePtr = (AgentMovementPlane*)UnsafeUtility.AddressOf(ref movementPlane); + this.managedState = state; + this.deltaTime = deltaTime; + this.entity = entity; + } + + /// <summary> + /// Disables local avoidance for the agent. + /// + /// Agents that traverse links are already marked as 'unstoppable' by the local avoidance system, + /// but calling this method will make other agents ignore them completely while traversing the link. + /// </summary> + public void DisableLocalAvoidance () { + if (managedState.enableLocalAvoidance) { + disabledRVO = true; + managedState.enableLocalAvoidance = false; + } + } + + /// <summary> + /// Disables rotation smoothing for the agent. + /// + /// This disables the effect of <see cref="MovementSettings.rotationSmoothing"/> while the agent is traversing the link. + /// Having rotation smoothing enabled can make the agent rotate towards its target rotation more slowly, + /// which is sometimes not desirable. + /// + /// Rotation smoothing will automatically be restored when the agent finishes traversing the link (if it was enabled before). + /// + /// The <see cref="MoveTowards"/> method automatically disables rotation smoothing when called. + /// </summary> + public void DisableRotationSmoothing () { + if (float.IsNaN(backupRotationSmoothing) && movementSettings.rotationSmoothing > 0) { + backupRotationSmoothing = movementSettings.rotationSmoothing; + movementSettings.rotationSmoothing = 0; + } + } + + /// <summary> + /// Restores the agent's settings to what it was before the link traversal started. + /// + /// This undos the changes made by <see cref="DisableLocalAvoidance"/> and <see cref="DisableRotationSmoothing"/>. + /// + /// This method is automatically called when the agent finishes traversing the link. + /// </summary> + public virtual void Restore () { + if (disabledRVO) { + managedState.enableLocalAvoidance = true; + disabledRVO = false; + } + if (!float.IsNaN(backupRotationSmoothing)) { + movementSettings.rotationSmoothing = backupRotationSmoothing; + backupRotationSmoothing = float.NaN; + } + } + + /// <summary>Teleports the agent to the given position</summary> + public virtual void Teleport (float3 position) { + transform.Position = position; + } + + /// <summary> + /// Thrown when the off-mesh link traversal should be aborted. + /// + /// See: <see cref="AgentOffMeshLinkTraversal.Abort"/> + /// </summary> + public class AbortOffMeshLinkTraversal : System.Exception {} + + /// <summary> + /// Aborts traversing the off-mesh link. + /// + /// This will immediately stop your off-mesh link traversal coroutine. + /// + /// This is useful if your agent was traversing an off-mesh link, but you have detected that it cannot continue. + /// Maybe the ladder it was climbing was destroyed, or the bridge it was walking on collapsed. + /// + /// Note: If you instead want to immediately make the agent move to the end of the link, you can call <see cref="Teleport"/>, and then use 'yield break;' from your coroutine. + /// </summary> + /// <param name="teleportToStart">If true, the agent will be teleported back to the start of the link (from the perspective of the agent). Its rotation will remain unchanged.</param> + public virtual void Abort (bool teleportToStart = true) { + if (teleportToStart) Teleport(link.relativeStart); + // Cancel the current path, as otherwise the agent will instantly try to traverse the off-mesh link again. + managedState.pathTracer.SetFromSingleNode(managedState.pathTracer.startNode, transform.Position, movementPlane); + throw new AbortOffMeshLinkTraversal(); + } + + /// <summary> + /// Move towards a point while ignoring the navmesh. + /// This method should be called repeatedly until the returned <see cref="MovementTarget.reached"/> property is true. + /// + /// Returns: A <see cref="MovementTarget"/> struct which can be used to check if the target has been reached. + /// + /// Note: This method completely ignores the navmesh. It also overrides local avoidance, if enabled (other agents will still avoid it, but this agent will not avoid other agents). + /// + /// TODO: The gravity property is not yet implemented. Gravity is always applied. + /// </summary> + /// <param name="position">The position to move towards.</param> + /// <param name="rotation">The rotation to rotate towards.</param> + /// <param name="gravity">If true, gravity will be applied to the agent.</param> + /// <param name="slowdown">If true, the agent will slow down as it approaches the target.</param> + public virtual MovementTarget MoveTowards (float3 position, quaternion rotation, bool gravity, bool slowdown) { + // If rotation smoothing was enabled, it could cause a very slow convergence to the target rotation. + // Therefore, we disable it here. + // The agent will try to remove its remaining rotation smoothing offset as quickly as possible. + // After the off-mesh link is traversed, the rotation smoothing will be automatically restored. + DisableRotationSmoothing(); + + var dirInPlane = movementPlane.ToPlane(position - transform.Position); + var remainingDistance = math.length(dirInPlane); + var maxSpeed = movementSettings.follower.Speed(slowdown ? remainingDistance : float.PositiveInfinity); + var speed = movementSettings.follower.Accelerate(movementControl.speed, movementSettings.follower.slowdownTime, deltaTime); + speed = math.min(speed, maxSpeed); + + var targetRot = movementPlane.ToPlane(rotation); + var currentRot = movementPlane.ToPlane(transform.Rotation); + var remainingRot = Mathf.Abs(AstarMath.DeltaAngle(currentRot, targetRot)); + movementControl = new MovementControl { + targetPoint = position, + endOfPath = position, + speed = speed, + maxSpeed = speed * 1.1f, + hierarchicalNodeIndex = -1, + overrideLocalAvoidance = true, + targetRotation = targetRot, + targetRotationHint = targetRot, + targetRotationOffset = 0, + rotationSpeed = math.radians(movementSettings.follower.rotationSpeed), + }; + + return new MovementTarget { + isReached = remainingDistance <= (slowdown ? 0.01f : speed * (1/30f)) && remainingRot < math.radians(1), + }; + } + + public virtual object Clone () { + var clone = (AgentOffMeshLinkTraversalContext)MemberwiseClone(); + clone.entity = Entity.Null; + clone.gameObjectCache = null; + clone.managedState = null; + unsafe { + linkInfoPtr = null; + movementControlPtr = null; + movementSettingsPtr = null; + transformPtr = null; + movementPlanePtr = null; + } + return clone; + } + } +} + +// ctx.MoveTowards (position, rotation, rvo = Auto | Disabled | AutoUnstoppable, gravity = auto|disabled) -> { reached() } + +// MovementTarget { ... } +// while (!movementTarget.reached) { +// ctx.SetMovementTarget(movementTarget); +// yield return null; +// } +// yield return ctx.MoveTo(position, rotation) +// ctx.TeleportTo(position, rotation) +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentOffMeshLinkTraversal.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentOffMeshLinkTraversal.cs.meta new file mode 100644 index 0000000..28f8e71 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AgentOffMeshLinkTraversal.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e7b7b15e5b39fc142a7dd409c4c3a18d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AutoRepathPolicy.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AutoRepathPolicy.cs new file mode 100644 index 0000000..a9324c1 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AutoRepathPolicy.cs @@ -0,0 +1,97 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + /// <summary> + /// Policy for how often to recalculate an agent's path. + /// + /// See: <see cref="FollowerEntity.autoRepath"/> + /// + /// This is the unmanaged equivalent of <see cref="Pathfinding.AutoRepathPolicy"/>. + /// </summary> + [System.Serializable] + public struct AutoRepathPolicy : IComponentData { + /// <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="AutoRepathPolicy.Mode"/> + /// </summary> + public const float Sensitivity = 10.0f; + + /// <summary> + /// Policy to use when recalculating paths. + /// + /// See: <see cref="Pathfinding.AutoRepathPolicy.Mode"/> for more details. + /// </summary> + public Pathfinding.AutoRepathPolicy.Mode mode; + + /// <summary>Number of seconds between each automatic path recalculation for Mode.EveryNSeconds, and the maximum interval for Mode.Dynamic</summary> + public float period; + + float3 lastDestination; + float lastRepathTime; + + public static AutoRepathPolicy Default => new AutoRepathPolicy { + mode = Pathfinding.AutoRepathPolicy.Mode.Dynamic, + period = 2, + lastDestination = float.PositiveInfinity, + lastRepathTime = float.NegativeInfinity + }; + + public AutoRepathPolicy (Pathfinding.AutoRepathPolicy policy) { + mode = policy.mode; + period = policy.mode == Pathfinding.AutoRepathPolicy.Mode.Dynamic ? policy.maximumPeriod : policy.period; + lastDestination = float.PositiveInfinity; + 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 bool ShouldRecalculatePath (float3 position, float radius, float3 destination, float time) { + if (mode == Pathfinding.AutoRepathPolicy.Mode.Never || float.IsPositiveInfinity(destination.x)) return false; + + float timeSinceLast = time - lastRepathTime; + if (mode == Pathfinding.AutoRepathPolicy.Mode.EveryNSeconds) { + return timeSinceLast >= period; + } else { + // cost = change in destination / max(distance to destination, radius) + float squaredCost = math.lengthsq(destination - lastDestination) / math.max(math.lengthsq(position - lastDestination), 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 >= period*(1 - math.sqrt(fraction)); + } + } + + public void Reset () { + lastDestination = float.PositiveInfinity; + lastRepathTime = float.NegativeInfinity; + } + + /// <summary>Must be called when a path request has been scheduled</summary> + public void DidRecalculatePath (float3 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 * period; + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AutoRepathPolicy.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AutoRepathPolicy.cs.meta new file mode 100644 index 0000000..0f99f79 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/AutoRepathPolicy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 49e745654af51f043a68a105c85e2bae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/DestinationPoint.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/DestinationPoint.cs new file mode 100644 index 0000000..d3ff623 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/DestinationPoint.cs @@ -0,0 +1,26 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + /// <summary>Holds an agent's destination point</summary> + public struct DestinationPoint : IComponentData { + /// <summary> + /// The destination point that the agent is moving towards. + /// + /// This is the point that the agent is trying to reach, but it may not always be possible to reach it. + /// + /// See: <see cref="AIDestinationSetter"/> + /// See: <see cref="IAstarAI.destination"/> + /// </summary> + public float3 destination; + + /// <summary> + /// The direction the agent should face when it reaches the destination. + /// + /// If zero, the agent will not try to face any particular direction when reaching the destination. + /// </summary> + public float3 facingDirection; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/DestinationPoint.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/DestinationPoint.cs.meta new file mode 100644 index 0000000..ca2370d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/DestinationPoint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87fc1fca9dfafa64b98ec33b24a358fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/GravityState.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/GravityState.cs new file mode 100644 index 0000000..4ab8700 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/GravityState.cs @@ -0,0 +1,16 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + /// <summary>Agent state related to gravity</summary> + public struct GravityState : IComponentData, IEnableableComponent { + /// <summary> + /// Current vertical velocity of the agent. + /// This is the velocity that the agent is moving with due to gravity. + /// It is not necessarily the same as the Y component of the estimated velocity. + /// </summary> + public float verticalVelocity; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/GravityState.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/GravityState.cs.meta new file mode 100644 index 0000000..2275bbb --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/GravityState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ed943911778141b4988cbdcd7f5b3a07 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedMovementOverride.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedMovementOverride.cs new file mode 100644 index 0000000..4f45dab --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedMovementOverride.cs @@ -0,0 +1,91 @@ +#if MODULE_ENTITIES +using Unity.Entities; + +namespace Pathfinding.ECS { + using Unity.Transforms; + + public delegate void BeforeControlDelegate(Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings); + public delegate void AfterControlDelegate(Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl); + public delegate void BeforeMovementDelegate(Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl, ref ResolvedMovement resolvedMovement); + + /// <summary> + /// Helper for adding and removing hooks to the FollowerEntity component. + /// This is used to allow other systems to override the movement of the agent. + /// + /// See: <see cref="FollowerEntity.movementOverrides"/> + /// </summary> + public ref struct ManagedMovementOverrides { + Entity entity; + World world; + + public ManagedMovementOverrides (Entity entity, World world) { + this.entity = entity; + this.world = world; + } + + public void AddBeforeControlCallback (BeforeControlDelegate value) { + AddCallback<ManagedMovementOverrideBeforeControl, BeforeControlDelegate>(value); + } + public void RemoveBeforeControlCallback (BeforeControlDelegate value) { + RemoveCallback<ManagedMovementOverrideBeforeControl, BeforeControlDelegate>(value); + } + + public void AddAfterControlCallback (AfterControlDelegate value) { + AddCallback<ManagedMovementOverrideAfterControl, AfterControlDelegate>(value); + } + public void RemoveAfterControlCallback (AfterControlDelegate value) { + RemoveCallback<ManagedMovementOverrideAfterControl, AfterControlDelegate>(value); + } + + public void AddBeforeMovementCallback (BeforeMovementDelegate value) { + AddCallback<ManagedMovementOverrideBeforeMovement, BeforeMovementDelegate>(value); + } + public void RemoveBeforeMovementCallback (BeforeMovementDelegate value) { + RemoveCallback<ManagedMovementOverrideBeforeMovement, BeforeMovementDelegate>(value); + } + + void AddCallback<C, T>(T callback) where T : System.Delegate where C : ManagedMovementOverride<T>, IComponentData, new() { + if (callback == null) throw new System.ArgumentNullException(nameof(callback)); + if (world == null || !world.EntityManager.Exists(entity)) throw new System.InvalidOperationException("The entity does not exist. You can only set a callback when the FollowerEntity is active and has been enabled. If you are trying to set this during Awake or OnEnable, try setting it during Start instead."); + if (!world.EntityManager.HasComponent<C>(entity)) world.EntityManager.AddComponentData(entity, new C()); + world.EntityManager.GetComponentData<C>(entity).AddCallback(callback); + } + + void RemoveCallback<C, T>(T callback) where T : System.Delegate where C : ManagedMovementOverride<T>, IComponentData, new() { + if (callback == null) throw new System.ArgumentNullException(nameof(callback)); + if (world == null || !world.EntityManager.Exists(entity)) return; + if (!world.EntityManager.HasComponent<C>(entity)) return; + + var comp = world.EntityManager.GetComponentData<C>(entity); + if (!comp.RemoveCallback(callback)) { + world.EntityManager.RemoveComponent<C>(entity); + } + } + } + + /// <summary> + /// Stores a delegate that can be used to override movement control and movement settings for a specific entity. + /// This is used by the FollowerEntity to allow other systems to override the movement of the entity. + /// + /// See: <see cref="FollowerEntity.movementOverrides"/> + /// </summary> + public class ManagedMovementOverride<T> : IComponentData where T : class, System.Delegate { + public T callback; + + public void AddCallback(T callback) => this.callback = (T)System.Delegate.Combine(this.callback, callback); + public bool RemoveCallback(T callback) => (this.callback = (T)System.Delegate.Remove(this.callback, callback)) != null; + } + + // IJobEntity does not support generic jobs yet, so we have to make concrete component types for each delegate type + public class ManagedMovementOverrideBeforeControl : ManagedMovementOverride<BeforeControlDelegate>, System.ICloneable { + // No fields in this class can be cloned safely + public object Clone() => new ManagedMovementOverrideBeforeControl(); + } + public class ManagedMovementOverrideAfterControl : ManagedMovementOverride<AfterControlDelegate> { + public object Clone() => new ManagedMovementOverrideAfterControl(); + } + public class ManagedMovementOverrideBeforeMovement : ManagedMovementOverride<BeforeMovementDelegate> { + public object Clone() => new ManagedMovementOverrideBeforeMovement(); + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedMovementOverride.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedMovementOverride.cs.meta new file mode 100644 index 0000000..409fc88 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedMovementOverride.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce6a314668bdcdd498d5d9d3ebf753c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedState.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedState.cs new file mode 100644 index 0000000..d219541 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedState.cs @@ -0,0 +1,222 @@ +#if MODULE_ENTITIES +using Unity.Entities; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.ECS.RVO; + using UnityEngine.Serialization; + + /// <summary> + /// Settings for agent movement that cannot be put anywhere else. + /// + /// The Unity ECS in general wants everything in components to be unmanaged types. + /// However, some things cannot be unmanaged types, for example delegates and interfaces. + /// There are also other things like path references and node references which are not unmanaged types at the moment. + /// + /// This component is used to store those things. + /// + /// It can also be used for things that are not used often, and so are best kept out-of-band to avoid bloating the ECS chunks too much. + /// </summary> + [System.Serializable] + public class ManagedState : IComponentData, System.IDisposable, System.ICloneable { + /// <summary> + /// Settings for when to recalculate the path. + /// + /// Deprecated: Use <see cref="FollowerEntity.autoRepath"/>, or the <see cref="Pathfinding.ECS.AutoRepathPolicy"/> component instead. + /// </summary> + [System.Obsolete("Use FollowerEntity.autoRepath, or the Pathfinding.ECS.AutoRepathPolicy component instead")] + public Pathfinding.AutoRepathPolicy autoRepath = new Pathfinding.AutoRepathPolicy(); + + /// <summary>Calculates in which direction to move to follow the path</summary> + public PathTracer pathTracer; + + /// <summary> + /// Local avoidance settings. + /// + /// When the agent has local avoidance enabled, these settings will be copied into a <see cref="Pathfinding.ECS.RVO.RVOAgent"/> component which is attached to the agent. + /// + /// See: <see cref="enableLocalAvoidance"/> + /// </summary> + [FormerlySerializedAs("rvoAgent")] + public RVOAgent rvoSettings = RVOAgent.Default; + + /// <summary>Callback for when the agent starts to traverse an off-mesh link</summary> + [System.NonSerialized] + public IOffMeshLinkHandler onTraverseOffMeshLink; + + public PathRequestSettings pathfindingSettings = PathRequestSettings.Default; + + /// <summary> + /// True if local avoidance is enabled for this agent. + /// + /// Enabling this will automatically add a <see cref="Pathfinding.ECS.RVO.RVOAgent"/> component to the entity. + /// + /// See: local-avoidance (view in online documentation for working links) + /// </summary> + [FormerlySerializedAs("rvoEnabled")] + public bool enableLocalAvoidance; + + /// <summary> + /// True if gravity is enabled for this agent. + /// + /// The agent will always fall down according to its own movement plane. + /// The gravity applied is Physics.gravity.y. + /// + /// Enabling this will add the <see cref="GravityState"/> component to the entity. + /// </summary> + public bool enableGravity = true; + + /// <summary>Path that is being calculated, if any</summary> + // Do not create a property visitor for this field, as otherwise the ECS infrastructure will try to patch entities inside it, and get very confused. + // I haven't been able to replicate this issue recently, but it has caused problems in the past. + // [Unity.Properties.DontCreateProperty] + public Path pendingPath { get; private set; } + + /// <summary> + /// Path that is being followed, if any. + /// + /// The agent may have moved away from this path since it was calculated. So it may not be up to date. + /// </summary> + // Do not create a property visitor for this field, as otherwise the ECS infrastructure will try to patch entities inside it, and get very confused. + // [Unity.Properties.DontCreateProperty] + public Path activePath { get; private set; } + + /// <summary> + /// \copydocref{IAstarAI.SetPath}. + /// + /// Warning: In almost all cases you should use <see cref="FollowerEntity.SetPath"/> instead of this method. + /// </summary> + public static void SetPath (Path path, ManagedState state, in AgentMovementPlane movementPlane, ref DestinationPoint destination) { + if (path == null) { + state.CancelCurrentPathRequest(); + state.ClearPath(); + } else if (path.PipelineState == PathState.Created) { + // Path has not started calculation yet + state.CancelCurrentPathRequest(); + state.pendingPath = path; + path.Claim(state); + AstarPath.StartPath(path); + } else if (path.PipelineState >= PathState.ReturnQueue) { + // Path has already been calculated + + if (state.pendingPath == path) { + // The pending path is now obviously no longer pending + state.pendingPath = null; + } else { + // We might be calculating another path at the same time, and we don't want that path to override this one. So cancel it. + state.CancelCurrentPathRequest(); + + // Increase the refcount on the path. + // If the path was already our pending path, then the refcount will have already been incremented + path.Claim(state); + } + + var abPath = path as ABPath; + if (abPath == null) throw new System.ArgumentException("This function only works with ABPaths, or paths inheriting from ABPath"); + + if (!abPath.error) { + try { + state.pathTracer.SetPath(abPath, movementPlane.value); + + // Release the previous path back to the pool, to reduce GC pressure + if (state.activePath != null) state.activePath.Release(state); + + state.activePath = abPath; + } catch (System.Exception e) { + // If the path was so invalid that the path tracer throws an exception, then we should not use it. + abPath.Release(state); + state.ClearPath(); + UnityEngine.Debug.LogException(e); + } + + // If a RandomPath or MultiTargetPath have just been calculated, then we need + // to patch our destination point, to ensure the agent continues to move towards the end of the path. + // For these path types, the end point of the path is not known before the calculation starts. + if (!abPath.endPointKnownBeforeCalculation) { + destination = new DestinationPoint { destination = abPath.originalEndPoint, facingDirection = default }; + } + + // Right now, the pathTracer is almost fully up to date. + // To make it fully up to date, we'd also have to call pathTracer.UpdateStart and pathTracer.UpdateEnd after this function. + // During normal path recalculations, the JobRepairPath will be scheduled right after this function, and it will + // call those functions. The incomplete state will not be observable outside the system. + // When called from FollowerEntity, the SetPath method on that component will ensure that these methods are called. + } else { + abPath.Release(state); + } + } else { + // Path calculation has been started, but it is not yet complete. Cannot really handle this. + throw new System.ArgumentException("You must call the SetPath method with a path that either has been completely calculated or one whose path calculation has not been started at all. It looks like the path calculation for the path you tried to use has been started, but is not yet finished."); + } + } + + public void ClearPath () { + pathTracer.Clear(); + if (activePath != null) { + activePath.Release(this); + activePath = null; + } + } + + public void CancelCurrentPathRequest () { + if (pendingPath != null) { + pendingPath.FailWithError("Canceled by script"); + pendingPath.Release(this); + pendingPath = null; + } + } + + public void Dispose () { + pathTracer.Dispose(); + if (pendingPath != null) { + pendingPath.FailWithError("Canceled because entity was destroyed"); + pendingPath.Release(this); + pendingPath = null; + } + if (activePath != null) { + activePath.Release(this); + activePath = null; + } + } + + /// <summary> + /// Pops the current part, and the next part from the start of the path. + /// + /// It is assumed that the agent is currently on a normal NodeSequence part, and that the next part in the path is an off-mesh link. + /// </summary> + public void PopNextLinkFromPath () { + if (pathTracer.partCount < 2 && pathTracer.GetPartType(1) != Funnel.PartType.OffMeshLink) { + throw new System.InvalidOperationException("The next part in the path is not an off-mesh link."); + } + pathTracer.PopParts(2, pathfindingSettings.traversalProvider, activePath); + } + + /// <summary> + /// Clones the managed state for when an entity is duplicated. + /// + /// Some fields are cleared instead of being cloned, such as the pending path, + /// which cannot reasonably be cloned. + /// </summary> + object System.ICloneable.Clone () { + return new ManagedState { + #pragma warning disable 618 + autoRepath = autoRepath.Clone(), + #pragma warning restore 618 + pathTracer = pathTracer.Clone(), + rvoSettings = rvoSettings, + pathfindingSettings = new PathRequestSettings { + graphMask = pathfindingSettings.graphMask, + tagPenalties = pathfindingSettings.tagPenalties != null ? (int[])pathfindingSettings.tagPenalties.Clone() : null, + traversableTags = pathfindingSettings.traversableTags, + traversalProvider = null, // Cannot be safely cloned or copied + }, + enableLocalAvoidance = enableLocalAvoidance, + enableGravity = enableGravity, + onTraverseOffMeshLink = null, // Cannot be safely cloned or copied + pendingPath = null, // Cannot be safely cloned or copied + activePath = null, // Cannot be safely cloned or copied + }; + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedState.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedState.cs.meta new file mode 100644 index 0000000..6320c9e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ManagedState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 33d1c95731798be41b90302b91409645 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementControl.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementControl.cs new file mode 100644 index 0000000..dfd03eb --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementControl.cs @@ -0,0 +1,85 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.Util; + + /// <summary> + /// Desired movement for an agent. + /// This data will be fed to the local avoidance system to calculate the final movement of the agent. + /// If no local avoidance is used, it will be directly copied to <see cref="ResolvedMovement"/>. + /// + /// See: <see cref="ResolvedMovement"/> + /// </summary> + public struct MovementControl : IComponentData { + /// <summary>The point the agent should move towards</summary> + public float3 targetPoint; + + /// <summary> + /// The end of the current path. + /// + /// This informs the local avoidance system about the final desired destination for the agent. + /// This is used to make agents stop if the destination is crowded and it cannot reach its destination. + /// + /// If this is not set, agents will often move forever around a crowded destination, always trying to find + /// some way to get closer, but never finding it. + /// </summary> + public float3 endOfPath; + + /// <summary>The speed at which the agent should move towards <see cref="targetPoint"/>, in meters per second</summary> + public float speed; + + /// <summary> + /// The maximum speed at which the agent may move, in meters per second. + /// + /// It is recommended to keep this slightly above <see cref="speed"/>, to allow the local avoidance system to move agents around more efficiently when necessary. + /// </summary> + public float maxSpeed; + + /// <summary> + /// The index of the hierarchical node that the agent is currently in. + /// Will be -1 if the hierarchical node index is not known. + /// See: <see cref="HierarchicalGraph"/> + /// </summary> + public int hierarchicalNodeIndex; + + /// <summary> + /// The desired rotation of the agent, in radians, relative to the current movement plane. + /// See: <see cref="NativeMovementPlane.ToWorldRotation"/> + /// </summary> + public float targetRotation; + + /// <summary> + /// The desired rotation of the agent, in radians, over a longer time horizon, relative to the current movement plane. + /// + /// The <see cref="targetRotation"/> is usually only over a very short time-horizon, usually a single simulation time step. + /// This variable is used to provide a hint of where the agent wants to rotate to over a slightly longer time scale (on the order of a second or so). + /// It is not used to control movement directly, but it may be used to guide animations, or rotation smoothing. + /// + /// If no better hint is available, this should be set to the same value as <see cref="targetRotation"/>. + /// + /// See: <see cref="NativeMovementPlane.ToWorldRotation"/> + /// </summary> + public float targetRotationHint; + + /// <summary> + /// Additive modifier to <see cref="targetRotation"/>, in radians. + /// This is used by the local avoidance system to rotate the agent, without this causing a feedback loop. + /// This extra rotation will be ignored by the control system which decides how the agent *wants* to move. + /// It will instead be directly applied to the agent. + /// </summary> + public float targetRotationOffset; + + /// <summary>The speed at which the agent should rotate towards <see cref="targetRotation"/> + <see cref="targetRotationOffset"/>, in radians per second</summary> + public float rotationSpeed; + + /// <summary> + /// If true, this agent will ignore other agents during local avoidance, but other agents will still avoid this one. + /// This is useful for example for a player character which should not avoid other agents, but other agents should avoid the player. + /// </summary> + public bool overrideLocalAvoidance; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementControl.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementControl.cs.meta new file mode 100644 index 0000000..4c0b87a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementControl.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 067b0510e83c84e43b21eb81fb804132 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementSettings.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementSettings.cs new file mode 100644 index 0000000..b6e2bef --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementSettings.cs @@ -0,0 +1,113 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using UnityEngine; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + using Pathfinding.PID; + + /// <summary>How to calculate which direction is "up" for the agent</summary> + public enum MovementPlaneSource : byte { + /// <summary> + /// The graph's natural up direction will be used to align the agent. + /// This is the most common option. + /// </summary> + Graph, + /// <summary> + /// The agent will be aligned with the normal of the navmesh. + /// + /// This is useful when you have a spherical world, or some other strange shape. + /// + /// The agent will look at the normal of the navmesh around the point it is currently standing on to determine which way is up. + /// The radius of the agent will be used to determine the size of the area to sample the normal from. + /// A bit of smoothing is done to make sure sharp changes in the normal do not cause the agent to rotate too fast. + /// + /// Note: If you have a somewhat flat world, and you want to align the agent to the ground, this is not the option you want. + /// Instead, you might want to disable <see cref="FollowerEntity.updateRotation"/> and then align the transform using a custom script. + /// + /// Warning: Using this option has a performance penalty. + /// + /// [Open online documentation to see videos] + /// + /// See: spherical (view in online documentation for working links) + /// </summary> + NavmeshNormal, + /// <summary> + /// The agent will be aligned with the ground normal. + /// + /// This is useful when you have a spherical world, or some other strange shape. + /// + /// You may want to use this instead of the NavmeshNormal option if your collider is smoother than your navmesh. + /// For example, if you have a spherical world with a sphere collider, you may want to use this option instead of the NavmeshNormal option. + /// + /// Note: If you have a somewhat flat world, and you want to align the agent to the ground, this is not the option you want. + /// Instead, you might want to disable <see cref="FollowerEntity.updateRotation"/> and then align the transform using a custom script. + /// + /// Warning: Using this option has a performance penalty. + /// </summary> + Raycast, + } + + [System.Serializable] + public struct MovementSettings : IComponentData { + /// <summary>Additional movement settings</summary> + public PIDMovement follower; + + /// <summary>Flags for enabling debug rendering in the scene view</summary> + public PIDMovement.DebugFlags debugFlags; + + /// <summary> + /// How far away from the destination should the agent aim to stop, in world units. + /// + /// If the agent is within this distance from the destination point it will be considered to have reached the destination. + /// + /// Even if you want the agent to stop precisely at a given point, it is recommended to keep this slightly above zero. + /// If it is exactly zero, the agent may have a hard time deciding that it + /// has actually reached the end of the path, due to floating point errors and such. + /// + /// Note: This will not be multiplied the agent's scale. + /// </summary> + public float stopDistance; + + /// <summary> + /// How much to smooth the visual rotation of the agent. + /// + /// This does not affect movement, but smoothes out how the agent rotates visually. + /// + /// Recommended values are between 0.0 and 0.5. + /// A value of zero will disable smoothing completely. + /// + /// The smoothing is done primarily using an exponential moving average, but with + /// a small linear term to make the rotation converge faster when the agent is almost facing the desired direction. + /// + /// Adding smoothing will make the visual rotation of the agent lag a bit behind the actual rotation. + /// Too much smoothing may make the agent seem sluggish, and appear to move sideways. + /// + /// The unit for this field is seconds. + /// </summary> + public float rotationSmoothing; + public float positionSmoothing; + + /// <summary> + /// Layer mask to use for ground placement. + /// Make sure this does not include the layer of any colliders attached to this gameobject. + /// + /// See: <see cref="GravityState"/> + /// See: https://docs.unity3d.com/Manual/Layers.html + /// </summary> + public LayerMask groundMask; + + /// <summary> + /// How to calculate which direction is "up" for the agent. + /// See: <see cref="MovementPlaneSource"/> + /// + /// Deprecated: Use the AgentMovementPlaneSource component instead, or the movementPlaneSource property on the FollowerEntity component + /// </summary> + [System.Obsolete("Use the AgentMovementPlaneSource component instead, or the movementPlaneSource property on the FollowerEntity component")] + public MovementPlaneSource movementPlaneSource; + + /// <summary>\copydocref{IAstarAI.isStopped}</summary> + public bool isStopped; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementSettings.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementSettings.cs.meta new file mode 100644 index 0000000..734fcc4 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4fafdd860735074e8ca2abad75c3992 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementState.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementState.cs new file mode 100644 index 0000000..0ea83ad --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementState.cs @@ -0,0 +1,196 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.PID; + + public struct MovementState : IComponentData { + /// <summary>State of the PID controller for the movement</summary> + public PIDMovement.PersistentState followerState; + + /// <summary>The next corner in the path</summary> + public float3 nextCorner; + + /// <summary> + /// The end of the current path. + /// Note that the agent may be heading towards an off-mesh link which is not the same as this point. + /// </summary> + public float3 endOfPath; + + /// <summary> + /// The closest point on the navmesh to the agent. + /// The agent will be snapped to this point. + /// </summary> + public float3 closestOnNavmesh; + + /// <summary> + /// Offset from the agent's internal position to its visual position. + /// + /// This is used when position smoothing is enabled. Otherwise it is zero. + /// </summary> + public float3 positionOffset; + + /// <summary> + /// The index of the hierarchical node that the agent is currently in. + /// Will be -1 if the hierarchical node index is not known. + /// + /// This field is valid during all system updates in the <see cref="AIMovementSystemGroup"/>. It is not guaranteed to be valid after that group has finished running, as graph updates may have changed the graph. + /// + /// See: <see cref="HierarchicalGraph"/> + /// </summary> + public int hierarchicalNodeIndex; + + /// <summary>The remaining distance until the end of the path, or the next off-mesh link</summary> + public float remainingDistanceToEndOfPart; + + /// <summary> + /// The current additional rotation that is applied to the agent. + /// This is used by the local avoidance system to rotate the agent, without this causing a feedback loop. + /// + /// See: <see cref="ResolvedMovement.targetRotationOffset"/> + /// </summary> + public float rotationOffset; + + /// <summary> + /// An additional, purely visual, rotation offset. + /// This is used for rotation smoothing, but does not affect the movement of the agent. + /// </summary> + public float rotationOffset2; + + /// <summary> + /// Version number of <see cref="PathTracer.version"/> when the movement state was last updated. + /// In particular, <see cref="closestOnNavmesh"/>, <see cref="nextCorner"/>, <see cref="endOfPath"/>, <see cref="remainingDistanceToEndOfPart"/>, <see cref="reachedDestination"/> and <see cref="reachedEndOfPath"/> will only + /// be considered up to date if this is equal to the current version number of the path tracer. + /// </summary> + public ushort pathTracerVersion; + + /// <summary>Bitmask for various flags</summary> + ushort flags; + + const int ReachedDestinationFlag = 1 << 0; + const int reachedDestinationAndOrientationFlag = 1 << 1; + const int ReachedEndOfPathFlag = 1 << 2; + const int reachedEndOfPathAndOrientationFlag = 1 << 3; + const int ReachedEndOfPartFlag = 1 << 4; + const int TraversingLastPartFlag = 1 << 5; + + /// <summary> + /// True if the agent has reached its destination. + /// The destination will be considered reached if all of these conditions are met: + /// - The agent has a path + /// - The path is not stale + /// - The destination is not significantly below the agent's feet. + /// - The destination is not significantly above the agent's head. + /// - The agent is on the last part of the path (there are no more remaining off-mesh links). + /// - The remaining distance to the end of the path + the distance from the end of the path to the destination is less than <see cref="MovementSettings.stopDistance"/>. + /// </summary> + public bool reachedDestination { + get => (flags & ReachedDestinationFlag) != 0; + set => flags = (ushort)((flags & ~ReachedDestinationFlag) | (value ? ReachedDestinationFlag : 0)); + } + + /// <summary> + /// True if the agent has reached its destination and is facing the desired orientation. + /// This will become true if all of these conditions are met: + /// - <see cref="reachedDestination"/> is true + /// - The agent is facing the desired facing direction as specified in <see cref="DestinationPoint.facingDirection"/>. + /// </summary> + public bool reachedDestinationAndOrientation { + get => (flags & reachedDestinationAndOrientationFlag) != 0; + set => flags = (ushort)((flags & ~reachedDestinationAndOrientationFlag) | (value ? reachedDestinationAndOrientationFlag : 0)); + } + + /// <summary> + /// True if the agent has reached the end of the path. + /// The end of the path will be considered reached if all of these conditions are met: + /// - The agent has a path + /// - The path is not stale + /// - The end of the path is not significantly below the agent's feet. + /// - The end of the path is not significantly above the agent's head. + /// - The agent is on the last part of the path (there are no more remaining off-mesh links). + /// - The remaining distance to the end of the path is less than <see cref="MovementSettings.stopDistance"/>. + /// </summary> + public bool reachedEndOfPath { + get => (flags & ReachedEndOfPathFlag) != 0; + set => flags = (ushort)((flags & ~ReachedEndOfPathFlag) | (value ? ReachedEndOfPathFlag : 0)); + } + + /// <summary> + /// True if the agent has reached its destination and is facing the desired orientation. + /// This will become true if all of these conditions are met: + /// - <see cref="reachedEndOfPath"/> is true + /// - The agent is facing the desired facing direction as specified in <see cref="DestinationPoint.facingDirection"/>. + /// </summary> + public bool reachedEndOfPathAndOrientation { + get => (flags & reachedEndOfPathAndOrientationFlag) != 0; + set => flags = (ushort)((flags & ~reachedEndOfPathAndOrientationFlag) | (value ? reachedEndOfPathAndOrientationFlag : 0)); + } + + /// <summary> + /// True if the agent has reached the end of the current part in the path. + /// The end of the current part will be considered reached if all of these conditions are met: + /// - The agent has a path + /// - The path is not stale + /// - The end of the current part is not significantly below the agent's feet. + /// - The end of the current part is not significantly above the agent's head. + /// - The remaining distance to the end of the part is not significantly larger than the agent's radius. + /// </summary> + public bool reachedEndOfPart { + get => (flags & ReachedEndOfPartFlag) != 0; + set => flags = (ushort)((flags & ~ReachedEndOfPartFlag) | (value ? ReachedEndOfPartFlag : 0)); + } + + /// <summary> + /// True if the agent is traversing the last part of the path. + /// + /// If false, the agent will have to traverse at least one off-mesh link before it gets to its destination. + /// </summary> + public bool traversingLastPart { + get => (flags & TraversingLastPartFlag) != 0; + set => flags = (ushort)((flags & ~TraversingLastPartFlag) | (value ? TraversingLastPartFlag : 0)); + } + + /// <summary> + /// The index of the graph that the agent is currently traversing. + /// + /// Will be <see cref="GraphNode.InvalidGraphIndex"/> if the agent has no path, or the node that the agent is traversing has been destroyed. + /// </summary> + public uint graphIndex { + get => (uint)(flags >> 8); + internal set => flags = (ushort)((flags & 0xFF) | (ushort)(value << 8)); + } + + /// <summary> + /// True if the agent is currently on a valid node. + /// + /// This is true if the agent has a path, and the node that the agent is traversing is walkable and not destroyed. + /// + /// If false, the <see cref="hierarchicalNodeIndex"/> and <see cref="graphIndex"/> fields are invalid. + /// </summary> + public bool isOnValidNode => hierarchicalNodeIndex != -1; + + public MovementState(UnityEngine.Vector3 agentPosition) { + this = default; + SetPathIsEmpty(agentPosition); + } + + /// <summary>Sets the appropriate fields to indicate that the agent has no path</summary> + public void SetPathIsEmpty (UnityEngine.Vector3 agentPosition) { + nextCorner = agentPosition; + endOfPath = agentPosition; + closestOnNavmesh = agentPosition; + hierarchicalNodeIndex = -1; + remainingDistanceToEndOfPart = float.PositiveInfinity; + reachedEndOfPath = false; + reachedDestination = false; + reachedEndOfPart = false; + reachedDestinationAndOrientation = false; + reachedEndOfPathAndOrientation = false; + traversingLastPart = true; + graphIndex = GraphNode.InvalidGraphIndex; + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementState.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementState.cs.meta new file mode 100644 index 0000000..71f1c38 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd27960b09d0034419af8c9451a551fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementStatistics.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementStatistics.cs new file mode 100644 index 0000000..d29781c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementStatistics.cs @@ -0,0 +1,17 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + public struct MovementStatistics : IComponentData { + /// <summary> + /// The estimated velocity that the agent is moving with. + /// This includes all form of movement, including local avoidance and gravity. + /// </summary> + public float3 estimatedVelocity; + + /// <summary>The position of the agent at the end of the last movement simulation step</summary> + public float3 lastPosition; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementStatistics.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementStatistics.cs.meta new file mode 100644 index 0000000..3b36a89 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/MovementStatistics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 40111712788bfc3409f6b39341f91e2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO.meta new file mode 100644 index 0000000..30598c4 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 233cdeb50c94c714ab4c82711f977368 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/AgentIndex.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/AgentIndex.cs new file mode 100644 index 0000000..72d4c4c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/AgentIndex.cs @@ -0,0 +1,58 @@ +using Pathfinding.RVO; +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Transforms; +#endif +using UnityEngine; +using Unity.Mathematics; + +namespace Pathfinding.ECS.RVO { + using Pathfinding.RVO; + + /// <summary> + /// Index of an RVO agent in the local avoidance simulation. + /// + /// If this component is present, that indicates that the agent is part of a local avoidance simulation. + /// The <see cref="RVOSystem"/> is responsible for adding and removing this component as necessary. + /// Any other systems should only concern themselves with the <see cref="RVOAgent"/> component. + /// + /// Warning: This component does not support cloning. You must not clone entities that use this component. + /// There doesn't seem to be any way to make this work with the Unity.Entities API at the moment. + /// </summary> +#if MODULE_ENTITIES + [WriteGroup(typeof(ResolvedMovement))] +#endif + public readonly struct AgentIndex +#if MODULE_ENTITIES + : Unity.Entities.ICleanupComponentData +#endif + { + internal const int DeletedBit = 1 << 31; + internal const int IndexMask = (1 << 24) - 1; + internal const int VersionOffset = 24; + internal const int VersionMask = 0b1111_111 << VersionOffset; + + public readonly int packedAgentIndex; + public int Index => packedAgentIndex & IndexMask; + public int Version => packedAgentIndex & VersionMask; + public bool Valid => (packedAgentIndex & DeletedBit) == 0; + + public AgentIndex(int packedAgentIndex) { + this.packedAgentIndex = packedAgentIndex; + } + + public AgentIndex(int version, int index) { + version <<= VersionOffset; + UnityEngine.Assertions.Assert.IsTrue((index & IndexMask) == index); + packedAgentIndex = (version & VersionMask) | (index & IndexMask); + } + + public AgentIndex WithIncrementedVersion () { + return new AgentIndex((((packedAgentIndex & VersionMask) + (1 << VersionOffset)) & VersionMask) | Index); + } + + public AgentIndex WithDeleted () { + return new AgentIndex(packedAgentIndex | DeletedBit); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/AgentIndex.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/AgentIndex.cs.meta new file mode 100644 index 0000000..b67b67e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/AgentIndex.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd00f859416fc5c4f984c5680d19fc7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/RVOAgent.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/RVOAgent.cs new file mode 100644 index 0000000..5049190 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/RVOAgent.cs @@ -0,0 +1,94 @@ +#if MODULE_ENTITIES +using Pathfinding.RVO; +using Unity.Entities; +using UnityEngine; +using Unity.Transforms; +using Unity.Mathematics; + +namespace Pathfinding.ECS.RVO { + using Pathfinding.RVO; + + /// <summary> + /// Agent data for the local avoidance system. + /// + /// See: local-avoidance (view in online documentation for working links) + /// </summary> + [System.Serializable] + public struct RVOAgent : IComponentData { + /// <summary>How far into the future to look for collisions with other agents (in seconds)</summary> + [Tooltip("How far into the future to look for collisions with other agents (in seconds)")] + public float agentTimeHorizon; + + /// <summary>How far into the future to look for collisions with obstacles (in seconds)</summary> + [Tooltip("How far into the future to look for collisions with obstacles (in seconds)")] + public float obstacleTimeHorizon; + + /// <summary> + /// Max number of other agents to take into account. + /// A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality. + /// </summary> + [Tooltip("Max number of other agents to take into account.\n" + + "A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.")] + public int maxNeighbours; + + /// <summary> + /// Specifies the avoidance layer for this agent. + /// The <see cref="collidesWith"/> mask on other agents will determine if they will avoid this agent. + /// </summary> + public RVOLayer layer; + + /// <summary> + /// Layer mask specifying which layers this agent will avoid. + /// You can set it as CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ... + /// + /// This can be very useful in games which have multiple teams of some sort. For example you usually + /// want the agents in one team to avoid each other, but you do not want them to avoid the enemies. + /// + /// This field only affects which other agents that this agent will avoid, it does not affect how other agents + /// react to this agent. + /// + /// See: bitmasks (view in online documentation for working links) + /// See: http://en.wikipedia.org/wiki/Mask_(computing) + /// </summary> + [Pathfinding.EnumFlag] + public RVOLayer collidesWith; + + /// <summary>\copydocref{Pathfinding.RVO.IAgent.Priority}</summary> + [Tooltip("How strongly other agents will avoid this agent")] + [UnityEngine.Range(0, 1)] + public float priority; + + /// <summary> + /// Priority multiplier. + /// This functions identically to the <see cref="priority"/>, however it is not exposed in the Unity inspector. + /// It is primarily used by the <see cref="Pathfinding.RVO.RVODestinationCrowdedBehavior"/>. + /// </summary> + [System.NonSerialized] + public float priorityMultiplier; + + [System.NonSerialized] + public float flowFollowingStrength; + + /// <summary>Enables drawing debug information in the scene view</summary> + public AgentDebugFlags debug; + + /// <summary>A locked unit cannot move. Other units will still avoid it but avoidance quality is not the best.</summary> + [Tooltip("A locked unit cannot move. Other units will still avoid it. But avoidance quality is not the best")] + public bool locked; + + /// <summary>Good default settings for an RVO agent</summary> + public static readonly RVOAgent Default = new RVOAgent { + locked = false, + agentTimeHorizon = 1.0f, + obstacleTimeHorizon = 0.5f, + maxNeighbours = 10, + layer = RVOLayer.DefaultAgent, + collidesWith = (RVOLayer)(-1), + priority = 0.5f, + priorityMultiplier = 1.0f, + flowFollowingStrength = 0.0f, + debug = AgentDebugFlags.Nothing, + }; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/RVOAgent.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/RVOAgent.cs.meta new file mode 100644 index 0000000..046cc35 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/RVO/RVOAgent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 910690cbba23a2745a85046d13e5c03b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ReadyToTraverseOffMeshLink.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ReadyToTraverseOffMeshLink.cs new file mode 100644 index 0000000..43e179e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ReadyToTraverseOffMeshLink.cs @@ -0,0 +1,10 @@ +#if MODULE_ENTITIES +using Unity.Entities; + +namespace Pathfinding.ECS { + /// <summary>Enabled if the agnet is ready to start traversing an off-mesh link</summary> + [System.Serializable] + public struct ReadyToTraverseOffMeshLink : IComponentData, IEnableableComponent { + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ReadyToTraverseOffMeshLink.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ReadyToTraverseOffMeshLink.cs.meta new file mode 100644 index 0000000..bcfbe64 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ReadyToTraverseOffMeshLink.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8473eb53e4d194545b1395c9301ffc55 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ResolvedMovement.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ResolvedMovement.cs new file mode 100644 index 0000000..d91a621 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ResolvedMovement.cs @@ -0,0 +1,35 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.Util; + + /// <summary> + /// Holds the final movement data for an entity. + /// This is the data that is used by the movement system to move the entity. + /// </summary> + public struct ResolvedMovement : IComponentData { + /// <summary>\copydocref{MovementControl.targetPoint}</summary> + public float3 targetPoint; + + /// <summary>\copydocref{MovementControl.speed}</summary> + public float speed; + + public float turningRadiusMultiplier; + + /// <summary>\copydocref{MovementControl.targetRotation}</summary> + public float targetRotation; + + /// <summary>\copydocref{MovementControl.targetRotationHint}</summary> + public float targetRotationHint; + + /// <summary>\copydocref{MovementControl.targetRotationOffset}</summary> + public float targetRotationOffset; + + /// <summary>\copydocref{MovementControl.rotationSpeed}</summary> + public float rotationSpeed; + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ResolvedMovement.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ResolvedMovement.cs.meta new file mode 100644 index 0000000..74542a5 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/ResolvedMovement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 659b0f455df2f744189544436f88bf05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SearchState.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SearchState.cs new file mode 100644 index 0000000..44c5eac --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SearchState.cs @@ -0,0 +1,11 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + using Pathfinding; + + public struct SearchState : IComponentData { + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SearchState.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SearchState.cs.meta new file mode 100644 index 0000000..5198363 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SearchState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe063c5087e811f47aec8d8889a66d68 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SimulateMovement.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SimulateMovement.cs new file mode 100644 index 0000000..49d316d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SimulateMovement.cs @@ -0,0 +1,46 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + using Pathfinding; + + /// <summary> + /// Tag component to enable movement for an entity. + /// Without this component, most systems will completely ignore the entity. + /// + /// There are some more specific components that can be used to selectively enable/disable some jobs: + /// - <see cref="SimulateMovementRepair"/> + /// - <see cref="SimulateMovementControl"/> + /// - <see cref="SimulateMovementFinalize"/> + /// + /// Removing one of the above components can be useful if you want to override the movement of an agent in some way. + /// </summary> + public struct SimulateMovement : IComponentData { + } + + /// <summary> + /// Tag component to allow the agent to repair its path and recalculate various statistics. + /// + /// Allows the <see cref="JobRepairPath"/> to run. + /// </summary> + public struct SimulateMovementRepair : IComponentData { + } + + /// <summary> + /// Tag component to allow the agent to calculate how it wants to move. + /// + /// Allows the <see cref="ControlJob"/> to run. + /// </summary> + public struct SimulateMovementControl : IComponentData { + } + + /// <summary> + /// Tag component to allow the agent to move according to its desired movement parameters. + /// + /// Allows <see cref="AIMoveSystem"/> to run the <see cref="JobApplyGravity"/>, <see cref="JobAlignAgentWithMovementDirection"/> and <see cref="JobMoveAgent"/> jobs. + /// </summary> + public struct SimulateMovementFinalize : IComponentData { + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SimulateMovement.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SimulateMovement.cs.meta new file mode 100644 index 0000000..485bb36 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SimulateMovement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38abdbf4c8ebfd64aa17d17cfd43cc8e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SyncWithTransform.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SyncWithTransform.cs new file mode 100644 index 0000000..497b8b5 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SyncWithTransform.cs @@ -0,0 +1,35 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + using Pathfinding; + + /// <summary> + /// Tag component to enable syncing between an agent's Transform and the agent entity's position. + /// + /// See: <see cref="FollowerEntity.updatePosition"/> + /// </summary> + public struct SyncPositionWithTransform : IComponentData { + } + + /// <summary> + /// Tag component to enable syncing between an agent's Transform and the agent entity's rotation. + /// + /// See: <see cref="FollowerEntity.updateRotation"/> + /// </summary> + public struct SyncRotationWithTransform : IComponentData { + } + + /// <summary> + /// Tag component to indicate that the agent's forward direction is along the Y axis. + /// + /// This is used to convert between the forward direction of the GameObject and the internal forward direction, which always uses +Z as forward. + /// + /// See: <see cref="FollowerEntity.orientation"/> + /// See: <see cref="OrientationMode"/> + /// </summary> + public struct OrientationYAxisForward : IComponentData { + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SyncWithTransform.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SyncWithTransform.cs.meta new file mode 100644 index 0000000..c3f179f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Components/SyncWithTransform.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04e21506cb33eb847838b0a75fda6bf0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/EntityAccess.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/EntityAccess.cs new file mode 100644 index 0000000..8264ac6 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/EntityAccess.cs @@ -0,0 +1,223 @@ +#if MODULE_ENTITIES +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; + +namespace Pathfinding.ECS { + /// <summary>Helper for EntityAccess</summary> + static class EntityAccessHelper { + public static readonly int GlobalSystemVersionOffset = UnsafeUtility.GetFieldOffset(typeof(ComponentTypeHandle<int>).GetField("m_GlobalSystemVersion", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)); + } + + /// <summary> + /// Wrapper for a pointer. + /// + /// Very similar to the entities package RefRW<T> struct. But unfortunately that one cannot be used because the required constructor is not exposed. + /// </summary> + public ref struct ComponentRef<T> where T : unmanaged { + unsafe byte* ptr; + + public unsafe ComponentRef(byte* ptr) { + this.ptr = ptr; + } + + public ref T value { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get { + unsafe { + return ref *(T*)ptr; + } + } + } + } + + /// <summary>Utility for efficient random access to entity storage data from the main thread</summary> + public struct EntityStorageCache { + EntityStorageInfo storage; + Entity entity; + int lastWorldHash; + + /// <summary> + /// Retrieves the storage for a given entity. + /// + /// This method is very fast if the entity is the same as the last call to this method. + /// If the entity is different, it will be slower. + /// + /// Returns: True if the entity exists, and false if it does not. + /// </summary> + // Inlining makes this method about 20% faster. It's hot when accessing properties on the FollowerEntity component. + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool Update (World world, Entity entity, out EntityManager entityManager, out EntityStorageInfo storage) { + entityManager = default; + storage = this.storage; + if (world == null) return false; + entityManager = world.EntityManager; + // We must use entityManager.EntityOrderVersion here, not GlobalSystemVersion, because + // the GlobalSystemVersion does not necessarily update when structural changes happen. + var worldHash = entityManager.EntityOrderVersion ^ ((int)world.SequenceNumber << 8); + if (worldHash != lastWorldHash || entity != this.entity) { + if (!entityManager.Exists(entity)) return false; + this.storage = storage = entityManager.GetStorageInfo(entity); + this.entity = entity; + lastWorldHash = worldHash; + } + return true; + } + + /// <summary> + /// Retrieves a component for a given entity. + /// + /// This is a convenience method to call <see cref="Update"/> on this object and update on the access object, and then retrieve the component data. + /// + /// This method is very fast if the entity is the same as the last call to this method. + /// If the entity is different, it will be slower. + /// + /// Warning: You must not store the returned reference past a structural change in the ECS world. + /// + /// Returns: True if the entity exists, and false if it does not. + /// Throws: An exception if the entity does not have the given component. + /// </summary> + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool GetComponentData<A>(Entity entity, ref EntityAccess<A> access, out ComponentRef<A> value) where A : unmanaged, IComponentData { + if (Update(World.DefaultGameObjectInjectionWorld, entity, out var entityManager, out var storage)) { + access.Update(entityManager); + unsafe { + value = new ComponentRef<A>((byte*)UnsafeUtility.AddressOf(ref access[storage])); + } + return true; + } else { + value = default; + return false; + } + } + } + + /// <summary> + /// Utility for efficient random access to entity component data from the main thread. + /// + /// Since this struct returns a reference to the component data, it is faster than using EntityManager.GetComponentData, + /// in particular for larger component types. + /// + /// Warning: Some checks are not enforced by this API. It is the user's responsibility to ensure that + /// this struct does not survive past an ECS system update. If you only use this struct from the main thread + /// and only store it locally on the stack, this should not be a problem. + /// This struct also does not enforce that you only read to the component data if the readOnly flag is set. + /// </summary> + public struct EntityAccess<T> where T : unmanaged, IComponentData { + public ComponentTypeHandle<T> handle; +#if ENABLE_UNITY_COLLECTIONS_CHECKS + SystemHandle systemHandle; +#endif + uint lastSystemVersion; + ulong worldSequenceNumber; + bool readOnly; + + public EntityAccess(bool readOnly) { + handle = default; + this.readOnly = readOnly; +#if ENABLE_UNITY_COLLECTIONS_CHECKS + systemHandle = default; +#endif + + // Version 0 is never used by EntityManager.GlobalSystemVersion + lastSystemVersion = 0; + worldSequenceNumber = 0; + } + + /// <summary> + /// Update the component type handle if necessary. + /// + /// This must be called if any jobs or system might have been scheduled since the struct was created or since the last call to Update. + /// </summary> + public void Update (EntityManager entityManager) { + // If the global system version has changed, jobs may have been scheduled which writes + // to the component data. Therefore we need to complete all dependencies before we can + // safely read or write to the component data. + + var systemVersion = entityManager.GlobalSystemVersion; + var sequenceNumber = entityManager.WorldUnmanaged.SequenceNumber; + if (systemVersion != lastSystemVersion || worldSequenceNumber != sequenceNumber) { + if (lastSystemVersion == 0 || worldSequenceNumber != sequenceNumber) { + handle = entityManager.GetComponentTypeHandle<T>(readOnly); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + entityManager.WorldUnmanaged.IsSystemValid(systemHandle); + systemHandle = entityManager.WorldUnmanaged.GetExistingUnmanagedSystem<AIMoveSystem>(); +#endif + } + + lastSystemVersion = systemVersion; + worldSequenceNumber = sequenceNumber; + if (readOnly) entityManager.CompleteDependencyBeforeRO<T>(); + else entityManager.CompleteDependencyBeforeRW<T>(); + } + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + handle.Update(ref entityManager.WorldUnmanaged.ResolveSystemStateRef(systemHandle)); +#else + // handle.Update just does the same thing as this unsafe code, but in a much more roundabout way + unsafe { + var ptr = (byte*)UnsafeUtility.AddressOf(ref handle); + *(uint*)(ptr + EntityAccessHelper.GlobalSystemVersionOffset) = systemVersion; + } +#endif + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool HasComponent (EntityStorageInfo storage) { + return storage.Chunk.Has<T>(ref handle); + } + + public ref T this[EntityStorageInfo storage] { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get { + unsafe { + var ptr = readOnly ? ((T*)storage.Chunk.GetRequiredComponentDataPtrRO(ref handle) + storage.IndexInChunk) : ((T*)storage.Chunk.GetRequiredComponentDataPtrRW(ref handle) + storage.IndexInChunk); + return ref *ptr; + } + } + } + } + + /// <summary> + /// Utility for efficient random access to managed entity component data from the main thread. + /// + /// Warning: Some checks are not enforced by this API. It is the user's responsibility to ensure that + /// this struct does not survive past an ECS system update. If you only use this struct from the main thread + /// and only store it locally on the stack, this should not be a problem. + /// This struct also does not enforce that you only read to the component data if the readOnly flag is set. + /// </summary> + public struct ManagedEntityAccess<T> where T : class, IComponentData { + EntityManager entityManager; + ComponentTypeHandle<T> handle; + bool readOnly; + + public ManagedEntityAccess(bool readOnly) { + entityManager = default; + handle = default; + this.readOnly = readOnly; + } + + public ManagedEntityAccess(EntityManager entityManager, bool readOnly) : this(readOnly) { + Update(entityManager); + } + + public void Update (EntityManager entityManager) { + if (readOnly) entityManager.CompleteDependencyBeforeRO<T>(); + else entityManager.CompleteDependencyBeforeRW<T>(); + handle = entityManager.GetComponentTypeHandle<T>(readOnly); + this.entityManager = entityManager; + } + + public T this[EntityStorageInfo storage] { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get { + return storage.Chunk.GetManagedComponentAccessor<T>(ref handle, entityManager)[storage.IndexInChunk]; + } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + set { + var accessor = storage.Chunk.GetManagedComponentAccessor<T>(ref handle, entityManager); + accessor[storage.IndexInChunk] = value; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/EntityAccess.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/EntityAccess.cs.meta new file mode 100644 index 0000000..5a10e58 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/EntityAccess.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eeb1b5067974c4947bb6751ab2a33627 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/IRuntimeBaker.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/IRuntimeBaker.cs new file mode 100644 index 0000000..077ba9e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/IRuntimeBaker.cs @@ -0,0 +1,9 @@ +#if MODULE_ENTITIES +using Unity.Entities; + +namespace Pathfinding.Util { + interface IRuntimeBaker { + void OnCreatedEntity(World world, Entity entity); + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/IRuntimeBaker.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/IRuntimeBaker.cs.meta new file mode 100644 index 0000000..afec79b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/IRuntimeBaker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c44cc7455a88b82419ef53a61dd5626c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs.meta new file mode 100644 index 0000000..d357f8a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fabe887d69022ae459e7619f5b0fbdf6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobAlignAgentWithMovementDirection.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobAlignAgentWithMovementDirection.cs new file mode 100644 index 0000000..4553462 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobAlignAgentWithMovementDirection.cs @@ -0,0 +1,43 @@ +#if MODULE_ENTITIES +using Unity.Burst; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; + +namespace Pathfinding.ECS { + [BurstCompile] + public partial struct JobAlignAgentWithMovementDirection : IJobEntity { + public float dt; + + public void Execute (ref LocalTransform transform, in MovementSettings movementSettings, in MovementState movementState, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, in MovementControl movementControl, ref ResolvedMovement resolvedMovement) { + if (math.lengthsq(movementControl.targetPoint - resolvedMovement.targetPoint) > 0.001f && resolvedMovement.speed > movementSettings.follower.speed * 0.1f) { + // If the agent is moving, align it with the movement direction + var desiredDirection = movementPlane.value.ToPlane(movementControl.targetPoint - transform.Position); + var actualDirection = movementPlane.value.ToPlane(resolvedMovement.targetPoint - transform.Position); + + float desiredAngle; + if (math.lengthsq(desiredDirection) > math.pow(movementSettings.follower.speed * 0.1f, 2)) { + desiredAngle = math.atan2(desiredDirection.y, desiredDirection.x); + } else { + // If the agent did not desire to move at all, use the agent's current rotation + desiredAngle = movementPlane.value.ToPlane(transform.Rotation) + math.PI*0.5f; + } + + // The agent only moves if the actual movement direction is non-zero + if (math.lengthsq(actualDirection) > math.pow(movementSettings.follower.speed * 0.1f, 2)) { + var actualAngle = math.atan2(actualDirection.y, actualDirection.x); + resolvedMovement.targetRotationOffset = AstarMath.DeltaAngle(desiredAngle, actualAngle); + return; + } + } + + { + // Decay the rotation offset + // var da = AstarMath.DeltaAngle(movementState.rotationOffset, 0); + // resolvedMovement.targetRotationOffset += da * dt * 2.0f; + resolvedMovement.targetRotationOffset = AstarMath.DeltaAngle(0, resolvedMovement.targetRotationOffset) * (1 - dt * 2.0f); + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobAlignAgentWithMovementDirection.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobAlignAgentWithMovementDirection.cs.meta new file mode 100644 index 0000000..e8059c1 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobAlignAgentWithMovementDirection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b348dcdafdb36a946afe26daf64739a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobApplyGravity.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobApplyGravity.cs new file mode 100644 index 0000000..347fbc9 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobApplyGravity.cs @@ -0,0 +1,75 @@ +#if MODULE_ENTITIES +using Pathfinding.Drawing; +using Pathfinding.Util; +using Unity.Burst; +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine; + +namespace Pathfinding.ECS { + [BurstCompile] + public partial struct JobApplyGravity : IJobEntity { + [ReadOnly] + public NativeArray<RaycastHit> raycastHits; + [ReadOnly] + public NativeArray<RaycastCommand> raycastCommands; + public CommandBuilder draw; + public float dt; + + public static void UpdateMovementPlaneFromNormal (float3 normal, ref AgentMovementPlane movementPlane) { + // Calculate a new movement plane that is perpendicular to the surface normal + // and is as similar to the previous movement plane as possible. + var forward = math.normalizesafe(math.mul(movementPlane.value.rotation, new float3(0, 0, 1))); + normal = math.normalizesafe(normal); + // TODO: This doesn't guarantee an orthogonal basis? forward and normal may not be perpendicular + movementPlane.value = new NativeMovementPlane(new quaternion(new float3x3( + math.cross(normal, forward), + normal, + forward + ))); + } + + void ResolveGravity (RaycastHit hit, bool grounded, ref LocalTransform transform, in AgentMovementPlane movementPlane, ref GravityState gravityState) { + var localPosition = movementPlane.value.ToPlane(transform.Position, out var currentElevation); + if (grounded) { + // Grounded + // Make the vertical velocity fall off exponentially. This is reasonable from a physical standpoint as characters + // are not completely stiff and touching the ground will not immediately negate all velocity downwards. The AI will + // stop moving completely due to the raycast penetration test but it will still *try* to move downwards. This helps + // significantly when moving down along slopes, because if the vertical velocity would be set to zero when the character + // was grounded it would lead to a kind of 'bouncing' behavior (try it, it's hard to explain). Ideally this should + // use a more physically correct formula but this is a good approximation and is much more performant. The constant + // CONVERGENCE_SPEED in the expression below determines how quickly it converges but high values can lead to too much noise. + const float CONVERGENCE_SPEED = 5f; + gravityState.verticalVelocity *= math.max(0, 1 - CONVERGENCE_SPEED * dt); + + movementPlane.value.ToPlane(hit.point, out var hitElevation); + var elevationDelta = gravityState.verticalVelocity * dt; + const float VERTICAL_COLLISION_ADJUSTMENT_SPEED = 6f; + if (hitElevation > currentElevation) { + // Already below ground, only allow upwards movement + currentElevation = Mathf.MoveTowards(currentElevation, hitElevation, VERTICAL_COLLISION_ADJUSTMENT_SPEED * math.sqrt(math.abs(hitElevation - currentElevation)) * dt); + } else { + // Was above the ground, allow downwards movement until we are at the ground + currentElevation = math.max(hitElevation, currentElevation + elevationDelta); + } + } else { + var elevationDelta = gravityState.verticalVelocity * dt; + currentElevation += elevationDelta; + } + transform.Position = movementPlane.value.ToWorld(localPosition, currentElevation); + } + + public void Execute (ref LocalTransform transform, in MovementSettings movementSettings, ref AgentMovementPlane movementPlane, ref GravityState gravityState, in AgentMovementPlaneSource movementPlaneSource, [Unity.Entities.EntityIndexInQuery] int entityIndexInQuery) { + var hit = raycastHits[entityIndexInQuery]; + var hitAnything = math.any((float3)hit.normal != 0f); + if (hitAnything && movementPlaneSource.value == MovementPlaneSource.Raycast) { + UpdateMovementPlaneFromNormal(hit.normal, ref movementPlane); + } + ResolveGravity(hit, hitAnything, ref transform, in movementPlane, ref gravityState); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobApplyGravity.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobApplyGravity.cs.meta new file mode 100644 index 0000000..a53f5e1 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobApplyGravity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 618ca53d45ce69442aaae615eb4f3291 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobControl.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobControl.cs new file mode 100644 index 0000000..49c804f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobControl.cs @@ -0,0 +1,146 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; +using Unity.Profiling; +using Unity.Transforms; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Pathfinding.Drawing; +using Pathfinding.PID; +using Unity.Burst.Intrinsics; + +namespace Pathfinding.ECS { + [BurstCompile] + public partial struct JobControl : IJobEntity, IJobEntityChunkBeginEnd { + public float dt; + public CommandBuilder draw; + [ReadOnly] + [NativeDisableContainerSafetyRestriction] + public NavmeshEdges.NavmeshBorderData navmeshEdgeData; + + [NativeDisableContainerSafetyRestriction] + public NativeList<float2> edgesScratch; + + private static readonly ProfilerMarker MarkerConvertObstacles = new ProfilerMarker("ConvertObstacles"); + + public static float3 ClampToNavmesh (float3 position, float3 closestOnNavmesh, in AgentCylinderShape shape, in AgentMovementPlane movementPlane) { + // Don't clamp the elevation except to make sure it's not too far below the navmesh. + var clamped2D = movementPlane.value.ToPlane(closestOnNavmesh, out float clampedElevation); + movementPlane.value.ToPlane(position, out float currentElevation); + currentElevation = math.max(currentElevation, clampedElevation - shape.height * 0.4f); + position = movementPlane.value.ToWorld(clamped2D, currentElevation); + return position; + } + + public bool OnChunkBegin (in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { + if (!edgesScratch.IsCreated) edgesScratch = new NativeList<float2>(64, Allocator.Temp); + return true; + } + + public void OnChunkEnd (in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask, bool chunkWasExecuted) {} + + public void Execute (ref LocalTransform transform, ref MovementState state, in DestinationPoint destination, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, in MovementSettings settings, in ResolvedMovement resolvedMovement, ref MovementControl controlOutput) { + // Clamp the agent to the navmesh. + var position = ClampToNavmesh(transform.Position, state.closestOnNavmesh, in shape, in movementPlane); + + edgesScratch.Clear(); + var scale = math.abs(transform.Scale); + var settingsTemp = settings.follower; + // Scale the settings by the agent's scale + settingsTemp.ScaleByAgentScale(scale); + settingsTemp.desiredWallDistance *= resolvedMovement.turningRadiusMultiplier; + + if (state.isOnValidNode) { + MarkerConvertObstacles.Begin(); + var localBounds = PIDMovement.InterestingEdgeBounds(ref settingsTemp, position, state.nextCorner, shape.height, movementPlane.value); + navmeshEdgeData.GetEdgesInRange(state.hierarchicalNodeIndex, localBounds, edgesScratch, movementPlane.value); + MarkerConvertObstacles.End(); + } + + // To ensure we detect that the end of the path is reached robustly we make the agent move slightly closer. + // to the destination than the stopDistance. + const float FUZZ = 0.005f; + // If we are moving towards an off-mesh link, then we want the agent to stop precisely at the off-mesh link. + // TODO: Depending on the link, we may want the agent to move towards the link at full speed, instead of slowing down. + var stopDistance = state.traversingLastPart ? math.max(0, settings.stopDistance - FUZZ) : 0f; + var distanceToSteeringTarget = math.max(0, state.remainingDistanceToEndOfPart - stopDistance); + var rotation = movementPlane.value.ToPlane(transform.Rotation) - state.rotationOffset - state.rotationOffset2; + + transform.Position = position; + + if (dt > 0.000001f) { + if (!math.isfinite(distanceToSteeringTarget)) { + // The agent has no path, just stay still + controlOutput = new MovementControl { + targetPoint = position, + speed = 0, + endOfPath = position, + maxSpeed = settings.follower.speed, + overrideLocalAvoidance = false, + hierarchicalNodeIndex = state.hierarchicalNodeIndex, + targetRotation = resolvedMovement.targetRotation, + rotationSpeed = settings.follower.maxRotationSpeed, + targetRotationOffset = state.rotationOffset, // May be modified by other systems + }; + } else if (settings.isStopped) { + // The user has requested that the agent slow down as quickly as possible. + // TODO: If the agent is not clamped to the navmesh, it should still move towards the navmesh if it is outside it. + controlOutput = new MovementControl { + // Keep moving in the same direction as during the last frame, but slow down + targetPoint = position + math.normalizesafe(resolvedMovement.targetPoint - position) * 10.0f, + speed = settings.follower.Accelerate(resolvedMovement.speed, settings.follower.slowdownTime, -dt), + endOfPath = state.endOfPath, + maxSpeed = settings.follower.speed, + overrideLocalAvoidance = false, + hierarchicalNodeIndex = state.hierarchicalNodeIndex, + targetRotation = resolvedMovement.targetRotation, + rotationSpeed = settings.follower.maxRotationSpeed, + targetRotationOffset = state.rotationOffset, // May be modified by other systems + }; + } else { + var controlParams = new PIDMovement.ControlParams { + edges = edgesScratch.AsArray(), + nextCorner = state.nextCorner, + agentRadius = shape.radius, + facingDirectionAtEndOfPath = destination.facingDirection, + endOfPath = state.endOfPath, + remainingDistance = distanceToSteeringTarget, + closestOnNavmesh = state.closestOnNavmesh, + debugFlags = settings.debugFlags, + p = position, + rotation = rotation, + maxDesiredWallDistance = state.followerState.maxDesiredWallDistance, + speed = controlOutput.speed, + movementPlane = movementPlane.value, + }; + + var control = PIDMovement.Control(ref settingsTemp, dt, ref controlParams, ref draw, out state.followerState.maxDesiredWallDistance); + var positionDelta = movementPlane.value.ToWorld(control.positionDelta, 0); + var speed = math.length(positionDelta) / dt; + + controlOutput = new MovementControl { + targetPoint = position + math.normalizesafe(positionDelta) * distanceToSteeringTarget, + speed = speed, + endOfPath = state.endOfPath, + maxSpeed = settingsTemp.speed * 1.1f, + overrideLocalAvoidance = false, + hierarchicalNodeIndex = state.hierarchicalNodeIndex, + // It may seem sketchy to use a target rotation so close to the current rotation. One might think + // there's risk of overshooting this target rotation if the frame rate is uneven. + // But the TimeScaledRateManager ensures that this is not the case. + // The cheap simulation's time (which is the one actually rotating the agent) is always guaranteed to be + // behind (or precisely caught up with) the full simulation's time (that's the simulation which runs this system). + targetRotation = rotation + control.rotationDelta, + targetRotationHint = rotation + AstarMath.DeltaAngle(rotation, control.targetRotation), + rotationSpeed = math.abs(control.rotationDelta / dt), + targetRotationOffset = state.rotationOffset, // May be modified by other systems + }; + } + } else { + controlOutput.hierarchicalNodeIndex = -1; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobControl.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobControl.cs.meta new file mode 100644 index 0000000..c4b5992 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobControl.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b24028677256624696bab00b6f43ac0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobDrawFollowerGizmos.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobDrawFollowerGizmos.cs new file mode 100644 index 0000000..df4652d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobDrawFollowerGizmos.cs @@ -0,0 +1,104 @@ +#if MODULE_ENTITIES +using System.Runtime.InteropServices; +using Pathfinding.Drawing; +using Pathfinding.PID; +using Pathfinding.Util; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; + +namespace Pathfinding.ECS { + [BurstCompile] + struct DrawGizmosJobUtils { + [BurstCompile] + internal static void DrawPath (ref CommandBuilder draw, ref UnsafeSpan<float3> vertices, ref AgentCylinderShape shape) { + draw.PushColor(Palette.Colorbrewer.Set1.Orange); + // Some people will set the agent's radius to zero. In that case we just draw the path as a polyline as we have no good reference for how to space the symbols. + if (shape.radius > 0.01f) { + var generator = new CommandBuilder.PolylineWithSymbol(CommandBuilder.SymbolDecoration.ArrowHead, shape.radius * 0.5f, shape.radius * 0.0f, shape.radius * 4f, true); + for (int i = vertices.Length - 1; i >= 0; i--) generator.MoveTo(ref draw, vertices[i]); + } else { + for (int i = 0; i < vertices.Length - 1; i++) draw.Line(vertices[i], vertices[i+1]); + } + draw.PopColor(); + } + } + + public partial struct JobDrawFollowerGizmos : IJobChunk { + public CommandBuilder draw; + public GCHandle entityManagerHandle; + [ReadOnly] + public ComponentTypeHandle<LocalTransform> LocalTransformTypeHandleRO; + [ReadOnly] + public ComponentTypeHandle<AgentCylinderShape> AgentCylinderShapeHandleRO; + [ReadOnly] + public ComponentTypeHandle<MovementSettings> MovementSettingsHandleRO; + [ReadOnly] + public ComponentTypeHandle<AgentMovementPlane> AgentMovementPlaneHandleRO; + // This is actually not read only, because the GetNextCorners function can modify internal state + // See JobRepairPath.Scheduler.ManagedStateTypeHandleRW for details about why NativeDisableContainerSafetyRestriction is required + [NativeDisableContainerSafetyRestriction] + public ComponentTypeHandle<ManagedState> ManagedStateHandleRW; + [ReadOnly] + public ComponentTypeHandle<MovementState> MovementStateHandleRO; + [ReadOnly] + public ComponentTypeHandle<ResolvedMovement> ResolvedMovementHandleRO; + + [NativeDisableContainerSafetyRestriction] + public NativeList<float3> scratchBuffer1; + [NativeDisableContainerSafetyRestriction] + public NativeArray<int> scratchBuffer2; + + public void Execute (in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in Unity.Burst.Intrinsics.v128 chunkEnabledMask) { + if (!scratchBuffer1.IsCreated) scratchBuffer1 = new NativeList<float3>(32, Allocator.Temp); + if (!scratchBuffer2.IsCreated) scratchBuffer2 = new NativeArray<int>(32, Allocator.Temp); + + unsafe { + var localTransforms = (LocalTransform*)chunk.GetNativeArray(ref LocalTransformTypeHandleRO).GetUnsafeReadOnlyPtr(); + var agentCylinderShapes = (AgentCylinderShape*)chunk.GetNativeArray(ref AgentCylinderShapeHandleRO).GetUnsafeReadOnlyPtr(); + var movementSettings = (MovementSettings*)chunk.GetNativeArray(ref MovementSettingsHandleRO).GetUnsafeReadOnlyPtr(); + var movementPlanes = (AgentMovementPlane*)chunk.GetNativeArray(ref AgentMovementPlaneHandleRO).GetUnsafeReadOnlyPtr(); + var managedStates = chunk.GetManagedComponentAccessor(ref ManagedStateHandleRW, (EntityManager)entityManagerHandle.Target); + var movementStates = (MovementState*)chunk.GetNativeArray(ref MovementStateHandleRO).GetUnsafeReadOnlyPtr(); + var resolvedMovement = (ResolvedMovement*)chunk.GetNativeArray(ref ResolvedMovementHandleRO).GetUnsafeReadOnlyPtr(); + + for (int i = 0; i < chunk.Count; i++) { + Execute(ref localTransforms[i], ref movementPlanes[i], ref agentCylinderShapes[i], managedStates[i], ref movementSettings[i], ref movementStates[i], ref resolvedMovement[i]); + } + } + } + + public void Execute (ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref AgentCylinderShape shape, ManagedState managedState, ref MovementSettings settings, ref MovementState movementState, ref ResolvedMovement resolvedMovement) { + if ((settings.debugFlags & PIDMovement.DebugFlags.Funnel) != 0) { + managedState.pathTracer.DrawFunnel(draw, movementPlane.value); + } + if ((settings.debugFlags & PIDMovement.DebugFlags.Rotation) != 0) { + var p2D = movementPlane.value.ToPlane(transform.Position, out float positionElevation); + draw.PushMatrix(math.mul(new float4x4(movementPlane.value.rotation, float3.zero), float4x4.Translate(new float3(0, positionElevation, 0)))); + var visualRotation = movementPlane.value.ToPlane(transform.Rotation); + var unsmoothedRotation = visualRotation - movementState.rotationOffset2; + var internalRotation = unsmoothedRotation - movementState.rotationOffset; + var targetInternalRotation = resolvedMovement.targetRotation; + var targetInternalRotationHint = resolvedMovement.targetRotationHint; + math.sincos(math.PI*0.5f + new float3(visualRotation, unsmoothedRotation, internalRotation), out var s, out var c); + draw.xz.ArrowheadArc(p2D, new float2(c.x, s.x), shape.radius * 1.1f, Palette.Colorbrewer.Set1.Blue); + draw.xz.ArrowheadArc(p2D, new float2(c.y, s.y), shape.radius * 1.1f, Palette.Colorbrewer.Set1.Purple); + draw.xz.ArrowheadArc(p2D, new float2(c.z, s.z), shape.radius * 1.1f, Palette.Colorbrewer.Set1.Orange); + math.sincos(math.PI*0.5f + new float2(targetInternalRotation, targetInternalRotationHint), out var s2, out var c2); + draw.xz.ArrowheadArc(p2D, new float2(c2.x, s2.x), shape.radius * 1.2f, Palette.Colorbrewer.Set1.Yellow); + draw.xz.ArrowheadArc(p2D, new float2(c2.y, s2.y), shape.radius * 1.2f, Palette.Colorbrewer.Set1.Pink); + draw.PopMatrix(); + } + if ((settings.debugFlags & PIDMovement.DebugFlags.Path) != 0 && managedState.pathTracer.hasPath) { + scratchBuffer1.Clear(); + managedState.pathTracer.GetNextCorners(scratchBuffer1, int.MaxValue, ref scratchBuffer2, Allocator.Temp, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + var span = scratchBuffer1.AsUnsafeSpan(); + DrawGizmosJobUtils.DrawPath(ref draw, ref span, ref shape); + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobDrawFollowerGizmos.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobDrawFollowerGizmos.cs.meta new file mode 100644 index 0000000..377d6ac --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobDrawFollowerGizmos.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f202855630112e347bc6d3e68ee6f314 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedMovementOverride.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedMovementOverride.cs new file mode 100644 index 0000000..abdd98e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedMovementOverride.cs @@ -0,0 +1,45 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Transforms; + +namespace Pathfinding.ECS { + public partial struct JobManagedMovementOverrideBeforeControl : IJobEntity { + public float dt; + + public void Execute (ManagedMovementOverrideBeforeControl managedOverride, Entity entity, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings) { + if (managedOverride.callback != null) { + managedOverride.callback(entity, dt, ref localTransform, ref shape, ref movementPlane, ref destination, ref movementState, ref movementSettings); + // The callback may have modified the movement state, so we need to reset the path tracer version to indicate that the movement state is not up to date. + // This will cause the repair job to avoid optimizing some updates away. + movementState.pathTracerVersion--; + } + } + } + + public partial struct JobManagedMovementOverrideAfterControl : IJobEntity { + public float dt; + + public void Execute (ManagedMovementOverrideAfterControl managedOverride, Entity entity, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl) { + if (managedOverride.callback != null) { + managedOverride.callback(entity, dt, ref localTransform, ref shape, ref movementPlane, ref destination, ref movementState, ref movementSettings, ref movementControl); + // The callback may have modified the movement state, so we need to reset the path tracer version to indicate that the movement state is not up to date. + // This will cause the repair job to avoid optimizing some updates away. + movementState.pathTracerVersion--; + } + } + } + + public partial struct JobManagedMovementOverrideBeforeMovement : IJobEntity { + public float dt; + + public void Execute (ManagedMovementOverrideBeforeMovement managedOverride, Entity entity, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl, ref ResolvedMovement resolvedMovement) { + if (managedOverride.callback != null) { + managedOverride.callback(entity, dt, ref localTransform, ref shape, ref movementPlane, ref destination, ref movementState, ref movementSettings, ref movementControl, ref resolvedMovement); + // The callback may have modified the movement state, so we need to reset the path tracer version to indicate that the movement state is not up to date. + // This will cause the repair job to avoid optimizing some updates away. + movementState.pathTracerVersion--; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedMovementOverride.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedMovementOverride.cs.meta new file mode 100644 index 0000000..24d261a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedMovementOverride.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e9dcafc22a15f547984558ee0fc626f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedOffMeshLinkTransition.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedOffMeshLinkTransition.cs new file mode 100644 index 0000000..8055cc8 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedOffMeshLinkTransition.cs @@ -0,0 +1,86 @@ +#pragma warning disable 0282 // Allows the 'partial' keyword without warnings +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Collections; +using UnityEngine; +using Unity.Transforms; +using Unity.Collections.LowLevel.Unsafe; + +namespace Pathfinding.ECS { + using Pathfinding; + + public partial struct JobManagedOffMeshLinkTransition : IJobEntity { + public EntityCommandBuffer commandBuffer; + public float deltaTime; + + public void Execute (Entity entity, ManagedState state, ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref MovementControl movementControl, ref MovementSettings movementSettings, ref AgentOffMeshLinkTraversal linkInfo, ManagedAgentOffMeshLinkTraversal managedLinkInfo) { + if (!MoveNext(entity, state, ref transform, ref movementPlane, ref movementControl, ref movementSettings, ref linkInfo, managedLinkInfo, deltaTime)) { + commandBuffer.RemoveComponent<AgentOffMeshLinkTraversal>(entity); + commandBuffer.RemoveComponent<ManagedAgentOffMeshLinkTraversal>(entity); + } + } + + public static bool MoveNext (Entity entity, ManagedState state, ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref MovementControl movementControl, ref MovementSettings movementSettings, ref AgentOffMeshLinkTraversal linkInfo, ManagedAgentOffMeshLinkTraversal managedLinkInfo, float deltaTime) { + unsafe { + managedLinkInfo.context.SetInternalData(entity, ref transform, ref movementPlane, ref movementControl, ref movementSettings, ref linkInfo, state, deltaTime); + } + + // Initialize the coroutine during the first step. + // This can also happen if the entity is duplicated, since the coroutine cannot be cloned. + if (managedLinkInfo.coroutine == null) { + // If we are calculating a path right now, cancel that path calculation. + // We don't want to calculate a path while we are traversing an off-mesh link. + state.CancelCurrentPathRequest(); + + if (managedLinkInfo.stateMachine == null) { + managedLinkInfo.stateMachine = managedLinkInfo.handler != null? managedLinkInfo.handler.GetOffMeshLinkStateMachine(managedLinkInfo.context) : null; + } + managedLinkInfo.coroutine = managedLinkInfo.stateMachine != null? managedLinkInfo.stateMachine.OnTraverseOffMeshLink(managedLinkInfo.context).GetEnumerator() : JobStartOffMeshLinkTransition.DefaultOnTraverseOffMeshLink(managedLinkInfo.context).GetEnumerator(); + } + + bool finished; + bool error = false; + bool popParts = true; + try { + finished = !managedLinkInfo.coroutine.MoveNext(); + } catch (AgentOffMeshLinkTraversalContext.AbortOffMeshLinkTraversal) { + error = true; + finished = true; + popParts = false; + } + catch (System.Exception e) { + Debug.LogException(e, managedLinkInfo.context.gameObject); + // Teleport the agent to the end of the link as a fallback, if there's an exception + managedLinkInfo.context.Teleport(managedLinkInfo.context.link.relativeEnd); + finished = true; + error = true; + } + + if (finished) { + try { + if (managedLinkInfo.stateMachine != null) { + if (error) managedLinkInfo.stateMachine.OnAbortTraversingOffMeshLink(); + else managedLinkInfo.stateMachine.OnFinishTraversingOffMeshLink(managedLinkInfo.context); + } + } catch (System.Exception e) { + // If an exception happens when exiting the state machine, log it, and then continue with the cleanup + Debug.LogException(e, managedLinkInfo.context.gameObject); + } + + managedLinkInfo.context.Restore(); + if (popParts) { + // Pop the part leading up to the link, and the link itself + state.PopNextLinkFromPath(); + } + } + return !finished; + } + } + + public partial struct JobManagedOffMeshLinkTransitionCleanup : IJobEntity { + public void Execute (ManagedAgentOffMeshLinkTraversal managedLinkInfo) { + managedLinkInfo.stateMachine.OnAbortTraversingOffMeshLink(); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedOffMeshLinkTransition.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedOffMeshLinkTransition.cs.meta new file mode 100644 index 0000000..1c23899 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobManagedOffMeshLinkTransition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a279ed93b8648d4982ea23f87877a3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobMoveAgent.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobMoveAgent.cs new file mode 100644 index 0000000..331ab82 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobMoveAgent.cs @@ -0,0 +1,95 @@ +#if MODULE_ENTITIES +using Unity.Burst; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; + +namespace Pathfinding.ECS { + [BurstCompile] + public partial struct JobMoveAgent : IJobEntity { + public float dt; + + static void UpdateVelocityEstimate (ref LocalTransform transform, ref MovementStatistics movementStatistics, float dt) { + if (dt > 0.000001f) { + movementStatistics.estimatedVelocity = (transform.Position - movementStatistics.lastPosition) / dt; + } + } + + static void ResolveRotation (ref LocalTransform transform, ref MovementState state, in ResolvedMovement resolvedMovement, in MovementSettings movementSettings, in AgentMovementPlane movementPlane, float dt) { + var currentRotation = movementPlane.value.ToPlane(transform.Rotation); + var currentInternalRotation = currentRotation - state.rotationOffset - state.rotationOffset2; + var deltaRotation = math.clamp(AstarMath.DeltaAngle(currentInternalRotation, resolvedMovement.targetRotation), -resolvedMovement.rotationSpeed * dt, resolvedMovement.rotationSpeed * dt); + var extraRotationSpeed = math.radians(movementSettings.follower.maxRotationSpeed) * 0.5f; + var deltaExtraRotation = math.clamp(AstarMath.DeltaAngle(state.rotationOffset, resolvedMovement.targetRotationOffset), -extraRotationSpeed * dt, extraRotationSpeed * dt); + var currentUnsmoothedRotation = currentInternalRotation + state.rotationOffset; + var newInternalRotation = currentInternalRotation + deltaRotation; + // Keep track of how much extra rotation we are applying. This is done so that + // the movement calculation can separate this out when doing its movement calculations. + state.rotationOffset += deltaExtraRotation; + // Make sure the rotation offset is between -pi/2 and pi/2 radians + state.rotationOffset = AstarMath.DeltaAngle(0, state.rotationOffset); + var newUnsmoothedRotation = newInternalRotation + state.rotationOffset; + + if (movementSettings.rotationSmoothing > 0) { + // Apply compensation to rotationOffset2 to precisely cancel out the agent's rotation during this frame + state.rotationOffset2 += currentUnsmoothedRotation - newUnsmoothedRotation; + + state.rotationOffset2 = AstarMath.DeltaAngle(0, state.rotationOffset2); + + // Decay the rotationOffset2. This implicitly adds an exponential moving average to the visual rotation + var decay = math.abs(AstarMath.DeltaAngle(currentRotation, resolvedMovement.targetRotationHint)) / movementSettings.rotationSmoothing; + var exponentialDecay = decay*dt; + + // In addition to an exponential decay, we also add a linear decay. + // This is important to relatively quickly zero out the error when the agent is almost + // facing the right direction. With an exponential decay, it would take far too long to look good. + const float LINEAR_DECAY_AMOUNT = 0.1f; + var linearDecay = (LINEAR_DECAY_AMOUNT/movementSettings.rotationSmoothing)*dt; + + if (math.abs(state.rotationOffset2) > 0) state.rotationOffset2 *= math.max(0, 1 - exponentialDecay - linearDecay/math.abs(state.rotationOffset2)); + } else if (state.rotationOffset2 != 0) { + // Rotation smoothing is disabled, decay the rotation offset very quickly, but still avoid jarring changes + state.rotationOffset2 += math.clamp(-state.rotationOffset2, -extraRotationSpeed * dt, extraRotationSpeed * dt); + } + + transform.Rotation = movementPlane.value.ToWorldRotation(newInternalRotation + state.rotationOffset + state.rotationOffset2); + } + + public static float3 MoveWithoutGravity (ref LocalTransform transform, in ResolvedMovement resolvedMovement, in AgentMovementPlane movementPlane, float dt) { + UnityEngine.Assertions.Assert.IsTrue(math.all(math.isfinite(resolvedMovement.targetPoint))); + // Move only along the movement plane + var localDir = movementPlane.value.ToPlane(resolvedMovement.targetPoint - transform.Position); + var magn = math.length(localDir); + var localDelta = math.select(localDir, localDir * math.clamp(resolvedMovement.speed * dt / magn, 0, 1.0f), magn > 0.0001f); + var delta = movementPlane.value.ToWorld(localDelta, 0); + return delta; + } + + public static void ResolvePositionSmoothing (float3 movementDelta, ref MovementState state, in MovementSettings movementSettings, float dt) { + if (movementSettings.positionSmoothing > 0) { + state.positionOffset -= movementDelta; + var exponentialDecay = 1f/movementSettings.positionSmoothing*dt; + var linearDecay = 0.1f/movementSettings.positionSmoothing*dt; + var positionOffsetMagnitude = math.length(state.positionOffset); + if (positionOffsetMagnitude > 0) state.positionOffset *= math.max(0, 1 - exponentialDecay - linearDecay/positionOffsetMagnitude); + } else { + state.positionOffset = float3.zero; + } + } + + public void Execute (ref LocalTransform transform, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, ref MovementState state, in MovementSettings movementSettings, in ResolvedMovement resolvedMovement, ref MovementStatistics movementStatistics) { + MoveAgent(ref transform, in shape, in movementPlane, ref state, in movementSettings, in resolvedMovement, ref movementStatistics, dt); + } + + public static void MoveAgent (ref LocalTransform transform, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, ref MovementState state, in MovementSettings movementSettings, in ResolvedMovement resolvedMovement, ref MovementStatistics movementStatistics, float dt) { + var delta = MoveWithoutGravity(ref transform, in resolvedMovement, in movementPlane, dt); + UnityEngine.Assertions.Assert.IsTrue(math.all(math.isfinite(delta)), "Refusing to set the agent's position to a non-finite vector"); + transform.Position += delta; + ResolvePositionSmoothing(delta, ref state, in movementSettings, dt); + ResolveRotation(ref transform, ref state, in resolvedMovement, in movementSettings, in movementPlane, dt); + UpdateVelocityEstimate(ref transform, ref movementStatistics, dt); + movementStatistics.lastPosition = transform.Position; + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobMoveAgent.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobMoveAgent.cs.meta new file mode 100644 index 0000000..c989a0d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobMoveAgent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae237f49f264df546bb908b1d4f3d658 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobPrepareAgentRaycasts.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobPrepareAgentRaycasts.cs new file mode 100644 index 0000000..c6c4081 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobPrepareAgentRaycasts.cs @@ -0,0 +1,47 @@ +#if MODULE_ENTITIES +using Pathfinding.Drawing; +using Unity.Burst; +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine; + +namespace Pathfinding.ECS { + [BurstCompile] + public partial struct JobPrepareAgentRaycasts : IJobEntity { + public NativeArray<RaycastCommand> raycastCommands; + public QueryParameters raycastQueryParameters; + public CommandBuilder draw; + public float dt; + public float gravity; + + void ApplyGravity (ref GravityState gravityState) { + gravityState.verticalVelocity += gravity * dt; + } + + RaycastCommand CalculateRaycastCommands (ref LocalTransform transform, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, in MovementSettings movementSettings, ref GravityState gravityState) { + // TODO: Might be more performant to convert the movement plane to two matrices + + movementPlane.value.ToPlane(transform.Position, out var lastElevation); + + var elevationDelta = gravityState.verticalVelocity * dt; + var localPosition = movementPlane.value.ToPlane(transform.Position, out var elevation); + var rayStartElevation = math.max(elevation + elevationDelta, lastElevation) + shape.height * 0.5f; + var rayStopElevation = math.min(elevation + elevationDelta, lastElevation); + float rayLength = rayStartElevation - rayStopElevation; // TODO: Multiply by scale + var down = movementPlane.value.ToWorld(0, -1); + raycastQueryParameters.layerMask = movementSettings.groundMask; + return new RaycastCommand(movementPlane.value.ToWorld(localPosition, rayStartElevation), down, raycastQueryParameters, rayLength); + } + + public void Execute (ref LocalTransform transform, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, ref MovementState state, in MovementSettings movementSettings, ref ResolvedMovement resolvedMovement, ref MovementStatistics movementStatistics, ref GravityState gravityState, [Unity.Entities.EntityIndexInQuery] int entityIndexInQuery) { + // Move only along the movement plane + // JobMoveAgent.MoveAgent(ref transform, in shape, in movementPlane, ref state, in movementSettings, in resolvedMovement, ref movementStatistics, dt); + + ApplyGravity(ref gravityState); + raycastCommands[entityIndexInQuery] = CalculateRaycastCommands(ref transform, in shape, in movementPlane, in movementSettings, ref gravityState); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobPrepareAgentRaycasts.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobPrepareAgentRaycasts.cs.meta new file mode 100644 index 0000000..7d0a011 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobPrepareAgentRaycasts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9de01002f503a3e4abd6467cd6de7a44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobRepairPath.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobRepairPath.cs new file mode 100644 index 0000000..7f0e193 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobRepairPath.cs @@ -0,0 +1,277 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; +using Unity.Profiling; +using Unity.Transforms; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using GCHandle = System.Runtime.InteropServices.GCHandle; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.Util; + using Unity.Burst; + using Unity.Jobs; + + /// <summary> + /// Repairs the path of agents. + /// + /// This job will repair the agent's path based on the agent's current position and its destination. + /// It will also recalculate various statistics like how far the agent is from the destination, + /// and if it has reached the destination or not. + /// </summary> + public struct JobRepairPath : IJobChunk { + public struct Scheduler { + [ReadOnly] + public ComponentTypeHandle<LocalTransform> LocalTransformTypeHandleRO; + public ComponentTypeHandle<MovementState> MovementStateTypeHandleRW; + [ReadOnly] + public ComponentTypeHandle<AgentCylinderShape> AgentCylinderShapeTypeHandleRO; + // NativeDisableContainerSafetyRestriction seems to be necessary because otherwise we will get an error: + // "The ComponentTypeHandle<Pathfinding.ECS.ManagedState> ... can not be accessed. Nested native containers are illegal in jobs." + // However, Unity doesn't seem to check for this at all times. Currently, I can only replicate the error if DoTween Pro is also installed. + // I have no idea how this unrelated package influences unity to actually do the check. + // We know it is safe to access the managed state because we make sure to never access an entity from multiple threads at the same time. + [NativeDisableContainerSafetyRestriction] + public ComponentTypeHandle<ManagedState> ManagedStateTypeHandleRW; + [ReadOnly] + public ComponentTypeHandle<MovementSettings> MovementSettingsTypeHandleRO; + [ReadOnly] + public ComponentTypeHandle<DestinationPoint> DestinationPointTypeHandleRO; + [ReadOnly] + public ComponentTypeHandle<AgentMovementPlane> AgentMovementPlaneTypeHandleRO; + public ComponentTypeHandle<ReadyToTraverseOffMeshLink> ReadyToTraverseOffMeshLinkTypeHandleRW; + public GCHandle entityManagerHandle; + public bool onlyApplyPendingPaths; + + public EntityQueryBuilder GetEntityQuery (Allocator allocator) { + return new EntityQueryBuilder(Allocator.Temp) + .WithAllRW<MovementState>() + .WithAllRW<ManagedState>() + .WithAllRW<LocalTransform>() + .WithAll<MovementSettings, DestinationPoint, AgentMovementPlane, AgentCylinderShape>() + // .WithAny<ReadyToTraverseOffMeshLink>() // TODO: Use WithPresent in newer versions + .WithAbsent<AgentOffMeshLinkTraversal>(); + } + + public Scheduler(ref SystemState systemState) { + entityManagerHandle = GCHandle.Alloc(systemState.EntityManager); + LocalTransformTypeHandleRO = systemState.GetComponentTypeHandle<LocalTransform>(true); + MovementStateTypeHandleRW = systemState.GetComponentTypeHandle<MovementState>(false); + AgentCylinderShapeTypeHandleRO = systemState.GetComponentTypeHandle<AgentCylinderShape>(true); + DestinationPointTypeHandleRO = systemState.GetComponentTypeHandle<DestinationPoint>(true); + AgentMovementPlaneTypeHandleRO = systemState.GetComponentTypeHandle<AgentMovementPlane>(true); + MovementSettingsTypeHandleRO = systemState.GetComponentTypeHandle<MovementSettings>(true); + ReadyToTraverseOffMeshLinkTypeHandleRW = systemState.GetComponentTypeHandle<ReadyToTraverseOffMeshLink>(false); + // Need to bypass the T : unmanaged check in systemState.GetComponentTypeHandle + ManagedStateTypeHandleRW = systemState.EntityManager.GetComponentTypeHandle<ManagedState>(false); + onlyApplyPendingPaths = false; + } + + public void Dispose () { + entityManagerHandle.Free(); + } + + void Update (ref SystemState systemState) { + LocalTransformTypeHandleRO.Update(ref systemState); + MovementStateTypeHandleRW.Update(ref systemState); + AgentCylinderShapeTypeHandleRO.Update(ref systemState); + DestinationPointTypeHandleRO.Update(ref systemState); + ManagedStateTypeHandleRW.Update(ref systemState); + MovementSettingsTypeHandleRO.Update(ref systemState); + AgentMovementPlaneTypeHandleRO.Update(ref systemState); + ReadyToTraverseOffMeshLinkTypeHandleRW.Update(ref systemState); + } + + public JobHandle ScheduleParallel (ref SystemState systemState, EntityQuery query, JobHandle dependency) { + Update(ref systemState); + return new JobRepairPath { + scheduler = this, + onlyApplyPendingPaths = onlyApplyPendingPaths + }.ScheduleParallel(query, dependency); + } + } + + public Scheduler scheduler; + + [NativeDisableContainerSafetyRestriction] + public NativeArray<int> indicesScratch; + [NativeDisableContainerSafetyRestriction] + public NativeList<float3> nextCornersScratch; + public bool onlyApplyPendingPaths; + + + public void Execute (in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in Unity.Burst.Intrinsics.v128 chunkEnabledMask) { + if (!indicesScratch.IsCreated) { + nextCornersScratch = new NativeList<float3>(Allocator.Temp); + indicesScratch = new NativeArray<int>(8, Allocator.Temp); + } + + unsafe { + var localTransforms = (LocalTransform*)chunk.GetNativeArray(ref scheduler.LocalTransformTypeHandleRO).GetUnsafeReadOnlyPtr(); + var movementStates = (MovementState*)chunk.GetNativeArray(ref scheduler.MovementStateTypeHandleRW).GetUnsafePtr(); + var agentCylinderShapes = (AgentCylinderShape*)chunk.GetNativeArray(ref scheduler.AgentCylinderShapeTypeHandleRO).GetUnsafeReadOnlyPtr(); + var destinationPoints = (DestinationPoint*)chunk.GetNativeArray(ref scheduler.DestinationPointTypeHandleRO).GetUnsafeReadOnlyPtr(); + var movementSettings = (MovementSettings*)chunk.GetNativeArray(ref scheduler.MovementSettingsTypeHandleRO).GetUnsafeReadOnlyPtr(); + var agentMovementPlanes = (AgentMovementPlane*)chunk.GetNativeArray(ref scheduler.AgentMovementPlaneTypeHandleRO).GetUnsafeReadOnlyPtr(); + var mask = chunk.GetEnabledMask(ref scheduler.ReadyToTraverseOffMeshLinkTypeHandleRW); + + var managedStates = chunk.GetManagedComponentAccessor(ref scheduler.ManagedStateTypeHandleRW, (EntityManager)scheduler.entityManagerHandle.Target); + + for (int i = 0; i < chunk.Count; i++) { + Execute( + ref localTransforms[i], + ref movementStates[i], + ref agentCylinderShapes[i], + ref agentMovementPlanes[i], + ref destinationPoints[i], + mask.GetEnabledRefRW<ReadyToTraverseOffMeshLink>(i), + managedStates[i], + in movementSettings[i], + nextCornersScratch, + ref indicesScratch, + Allocator.Temp, + onlyApplyPendingPaths + ); + } + } + } + + private static readonly ProfilerMarker MarkerRepair = new ProfilerMarker("Repair"); + private static readonly ProfilerMarker MarkerGetNextCorners = new ProfilerMarker("GetNextCorners"); + private static readonly ProfilerMarker MarkerUpdateReachedEndInfo = new ProfilerMarker("UpdateReachedEndInfo"); + + public static void Execute (ref LocalTransform transform, ref MovementState state, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, EnabledRefRW<ReadyToTraverseOffMeshLink> readyToTraverseOffMeshLink, ManagedState managedState, in MovementSettings settings, NativeList<float3> nextCornersScratch, ref NativeArray<int> indicesScratch, Allocator allocator, bool onlyApplyPendingPaths) { + // Only enabled by the PollPendingPathsSystem + if (onlyApplyPendingPaths) { + if (managedState.pendingPath != null && managedState.pendingPath.IsDone()) { + // The path has been calculated, apply it to the agent + // Immediately after this we must also repair the path to ensure that it is valid and that + // all properties like #MovementState.reachedEndOfPath are correct. + ManagedState.SetPath(managedState.pendingPath, managedState, in movementPlane, ref destination); + } else { + // The agent has no path that has just been calculated, skip it + return; + } + } + + ref var pathTracer = ref managedState.pathTracer; + + if (pathTracer.hasPath) { + MarkerRepair.Begin(); + // Update the start and end points of the path based on the current position and destination. + // This will repair the path if necessary, ensuring that the agent has a valid, if not necessarily optimal, path. + // If it cannot be repaired well, the path will be marked as stale. + state.closestOnNavmesh = pathTracer.UpdateStart(transform.Position, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + state.endOfPath = pathTracer.UpdateEnd(destination.destination, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + MarkerRepair.End(); + + if (state.pathTracerVersion != pathTracer.version) { + nextCornersScratch.Clear(); + + MarkerGetNextCorners.Begin(); + // Find the next corners of the path. The first corner is our current position, + // the second corner is the one we are moving towards and the third corner is the one after that. + // + // Using GetNextCorners with the default transformation instead of ConvertCornerIndicesToPathProjected + // is about 20% faster, but it does not work well at all on spherical worlds. + // In the future we might want to switch dynamically between these modes, + // but on the other hand, it is very nice to be able to use the exact same code path for everything. + // pathTracer.GetNextCorners(nextCornersScratch, 3, ref indicesScratch, allocator); + var numCorners = pathTracer.GetNextCornerIndices(ref indicesScratch, pathTracer.desiredCornersForGoodSimplification, allocator, out bool lastCorner, managedState.pathfindingSettings.traversalProvider, managedState.activePath); + pathTracer.ConvertCornerIndicesToPathProjected(indicesScratch, numCorners, lastCorner, nextCornersScratch, movementPlane.value.up); + MarkerGetNextCorners.End(); + + // We need to copy a few fields to a new struct, in order to be able to pass it to a burstified function + var pathTracerInfo = new JobRepairPathHelpers.PathTracerInfo { + endPointOfFirstPart = pathTracer.endPointOfFirstPart, + partCount = pathTracer.partCount, + isStale = pathTracer.isStale + }; + var nextCorners = nextCornersScratch.AsUnsafeSpan(); + JobRepairPathHelpers.UpdateReachedEndInfo(ref nextCorners, ref state, ref movementPlane, ref transform, ref shape, ref destination, settings.stopDistance, ref pathTracerInfo); + state.pathTracerVersion = pathTracer.version; + } else { + JobRepairPathHelpers.UpdateReachedOrientation(ref state, ref transform, ref movementPlane, ref destination); + } + + if (pathTracer.startNode != null && !pathTracer.startNode.Destroyed && pathTracer.startNode.Walkable) { + state.graphIndex = pathTracer.startNode.GraphIndex; + state.hierarchicalNodeIndex = pathTracer.startNode.HierarchicalNodeIndex; + } else { + state.graphIndex = GraphNode.InvalidGraphIndex; + state.hierarchicalNodeIndex = -1; + } + } else { + state.SetPathIsEmpty(transform.Position); + } + + if (readyToTraverseOffMeshLink.IsValid) readyToTraverseOffMeshLink.ValueRW = state.reachedEndOfPart && managedState.pathTracer.isNextPartValidLink; + } + } + + [BurstCompile] + static class JobRepairPathHelpers { + public struct PathTracerInfo { + public float3 endPointOfFirstPart; + public int partCount; + // Bools are not blittable by burst so we must use a byte instead. Very ugly, but it is what it is. + byte isStaleBacking; + public bool isStale { + get => isStaleBacking != 0; + set => isStaleBacking = value ? (byte)1 : (byte)0; + } + } + + /// <summary>Checks if the agent has reached its destination, or the end of the path</summary> + [BurstCompile] + public static void UpdateReachedEndInfo (ref UnsafeSpan<float3> nextCorners, ref MovementState state, ref AgentMovementPlane movementPlane, ref LocalTransform transform, ref AgentCylinderShape shape, ref DestinationPoint destination, float stopDistance, ref PathTracerInfo pathTracer) { + // TODO: Edit GetNextCorners so that it gets corners until at least stopDistance units from the agent + state.nextCorner = nextCorners.length > 1 ? nextCorners[1] : transform.Position; + state.remainingDistanceToEndOfPart = PathTracer.RemainingDistanceLowerBound(in nextCorners, in pathTracer.endPointOfFirstPart, in movementPlane.value); + + // TODO: Check if end node is the globally closest node + movementPlane.value.ToPlane(pathTracer.endPointOfFirstPart - transform.Position, out var elevationDiffEndOfPart); + var validHeightRangeEndOfPart = elevationDiffEndOfPart< shape.height && elevationDiffEndOfPart > -0.5f*shape.height; + + movementPlane.value.ToPlane(destination.destination - transform.Position, out var elevationDiffDestination); + var validHeightRangeDestination = elevationDiffDestination< shape.height && elevationDiffDestination > -0.5f*shape.height; + var endOfPathToDestination = math.length(movementPlane.value.ToPlane(destination.destination - state.endOfPath)); + // If reachedEndOfPath is true we allow a slightly larger margin of error for reachedDestination. + // This is to ensure that if reachedEndOfPath becomes true, it is very likely that reachedDestination becomes + // true during the same frame. + const float FUZZ = 0.01f; + // When checking if the agent has reached the end of the current part (mostly used for off-mesh-links), we check against + // the agent's radius. This is because when there are many agents trying to reach the same off-mesh-link, the agents will + // crowd up and it may become hard to get to a point closer than the agent's radius. + state.reachedEndOfPart = !pathTracer.isStale && validHeightRangeEndOfPart && state.remainingDistanceToEndOfPart <= shape.radius*1.1f; + state.reachedEndOfPath = !pathTracer.isStale && validHeightRangeEndOfPart && pathTracer.partCount == 1 && state.remainingDistanceToEndOfPart <= stopDistance; + state.reachedDestination = !pathTracer.isStale && validHeightRangeDestination && pathTracer.partCount == 1 && state.remainingDistanceToEndOfPart + endOfPathToDestination <= stopDistance + (state.reachedEndOfPath ? FUZZ : 0); + state.traversingLastPart = pathTracer.partCount == 1; + UpdateReachedOrientation(ref state, ref transform, ref movementPlane, ref destination); + } + + /// <summary>Checks if the agent is oriented towards the desired facing direction</summary> + public static void UpdateReachedOrientation (ref MovementState state, ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref DestinationPoint destination) { + state.reachedEndOfPathAndOrientation = state.reachedEndOfPath; + state.reachedDestinationAndOrientation = state.reachedDestination; + if (state.reachedEndOfPathAndOrientation || state.reachedDestinationAndOrientation) { + var reachedOrientation = ReachedDesiredOrientation(ref transform, ref movementPlane, ref destination); + state.reachedEndOfPathAndOrientation &= reachedOrientation; + state.reachedDestinationAndOrientation &= reachedOrientation; + } + } + + static bool ReachedDesiredOrientation (ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref DestinationPoint destination) { + var facingDirection2D = math.normalizesafe(movementPlane.value.ToPlane(destination.facingDirection)); + + // If no desired facing direction is set, then we always treat the orientation as correct + if (math.all(facingDirection2D == 0)) return true; + + var forward2D = math.normalizesafe(movementPlane.value.ToPlane(transform.Forward())); + const float ANGLE_THRESHOLD_COS = 0.9999f; + return math.dot(forward2D, facingDirection2D) >= ANGLE_THRESHOLD_COS; + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobRepairPath.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobRepairPath.cs.meta new file mode 100644 index 0000000..d1ba13b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobRepairPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: db3650f148245bf4ba822d172a34cc65 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobStartOffMeshLinkTransition.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobStartOffMeshLinkTransition.cs new file mode 100644 index 0000000..994c68d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobStartOffMeshLinkTransition.cs @@ -0,0 +1,33 @@ +#pragma warning disable 0282 // Allows the 'partial' keyword without warnings +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; + +namespace Pathfinding.ECS { + public partial struct JobStartOffMeshLinkTransition { + public EntityCommandBuffer commandBuffer; + + /// <summary> + /// This is a fallback for traversing off-mesh links in case the user has not specified a custom traversal method. + /// It is a coroutine which will move the agent from the start point of the link to the end point of the link. + /// It will also disable RVO for the agent while traversing the link. + /// </summary> + public static System.Collections.Generic.IEnumerable<object> DefaultOnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext ctx) { + var linkInfo = ctx.link; + var up = ctx.movementPlane.ToWorld(float2.zero, 1); + var dirInPlane = ctx.movementPlane.ToWorld(ctx.movementPlane.ToPlane(linkInfo.relativeEnd - linkInfo.relativeStart), 0); + var rot = quaternion.LookRotationSafe(dirInPlane, up); + + while (!ctx.MoveTowards(linkInfo.relativeStart, rot, true, false).reached) yield return null; + + ctx.DisableLocalAvoidance(); + + while (!ctx.MoveTowards(linkInfo.relativeEnd, rot, true, false).reached) yield return null; + + // ctx.Teleport(linkInfo.endPoint); + + yield break; + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobStartOffMeshLinkTransition.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobStartOffMeshLinkTransition.cs.meta new file mode 100644 index 0000000..69baaa9 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobStartOffMeshLinkTransition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fee7292c827674f4eb6ef066b3fd1c99 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobSyncEntitiesToTransforms.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobSyncEntitiesToTransforms.cs new file mode 100644 index 0000000..041f03d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobSyncEntitiesToTransforms.cs @@ -0,0 +1,47 @@ +#if MODULE_ENTITIES +using Unity.Burst; +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine.Jobs; + +namespace Pathfinding.ECS { + [BurstCompile] + struct JobSyncEntitiesToTransforms : IJobParallelForTransform { + [ReadOnly] + public NativeArray<Entity> entities; + [ReadOnly] + public ComponentLookup<LocalTransform> entityPositions; + [ReadOnly] + public ComponentLookup<MovementState> movementState; + [ReadOnly] + public ComponentLookup<SyncPositionWithTransform> syncPositionWithTransform; + [ReadOnly] + public ComponentLookup<SyncRotationWithTransform> syncRotationWithTransform; + [ReadOnly] + public ComponentLookup<OrientationYAxisForward> orientationYAxisForward; + + public void Execute (int index, TransformAccess transform) { + var entity = entities[index]; + if (!entityPositions.HasComponent(entity)) return; + float3 offset = float3.zero; + if (movementState.TryGetComponent(entity, out var ms)) { + offset = ms.positionOffset; + } + + var tr = entityPositions.GetRefRO(entity); + if (syncPositionWithTransform.HasComponent(entity)) transform.position = tr.ValueRO.Position + offset; + if (syncRotationWithTransform.HasComponent(entity)) { + if (orientationYAxisForward.HasComponent(entity)) { + // Y axis forward + transform.rotation = math.mul(tr.ValueRO.Rotation, SyncTransformsToEntitiesSystem.ZAxisForwardToYAxisForward); + } else { + // Z axis forward + transform.rotation = tr.ValueRO.Rotation; + } + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobSyncEntitiesToTransforms.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobSyncEntitiesToTransforms.cs.meta new file mode 100644 index 0000000..8edba2e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Jobs/JobSyncEntitiesToTransforms.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bbb93f3ffd0760d4e9b68f4376e30aeb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems.meta new file mode 100644 index 0000000..ad610e5 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 380dbe46ad736aa4ab00113f94f0ddf2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs new file mode 100644 index 0000000..c68ad7f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs @@ -0,0 +1,268 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Transforms; +using Unity.Burst; +using Unity.Jobs; +using Unity.Collections; +using UnityEngine; +using UnityEngine.Jobs; +using GCHandle = System.Runtime.InteropServices.GCHandle; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.ECS.RVO; + using Pathfinding.Drawing; + using Pathfinding.Util; + using Unity.Profiling; + using UnityEngine.Profiling; + + [BurstCompile] + [UpdateAfter(typeof(FollowerControlSystem))] + [UpdateAfter(typeof(RVOSystem))] + [UpdateAfter(typeof(FallbackResolveMovementSystem))] + [UpdateInGroup(typeof(AIMovementSystemGroup))] + [RequireMatchingQueriesForUpdate] + public partial struct AIMoveSystem : ISystem { + EntityQuery entityQueryPrepareMovement; + EntityQuery entityQueryWithGravity; + EntityQuery entityQueryMove; + EntityQuery entityQueryRotation; + EntityQuery entityQueryGizmos; + EntityQuery entityQueryMovementOverride; + JobRepairPath.Scheduler jobRepairPathScheduler; + ComponentTypeHandle<MovementState> MovementStateTypeHandleRO; + ComponentTypeHandle<ResolvedMovement> ResolvedMovementHandleRO; + + public static EntityQueryBuilder EntityQueryPrepareMovement () { + return new EntityQueryBuilder(Allocator.Temp) + .WithAllRW<MovementState>() + .WithAllRW<ManagedState>() + .WithAllRW<LocalTransform>() + .WithAll<MovementSettings, DestinationPoint, AgentMovementPlane, AgentCylinderShape>() + // .WithAny<ReadyToTraverseOffMeshLink>() // TODO: Use WithPresent in newer versions + .WithAbsent<AgentOffMeshLinkTraversal>(); + } + + public void OnCreate (ref SystemState state) { + jobRepairPathScheduler = new JobRepairPath.Scheduler(ref state); + MovementStateTypeHandleRO = state.GetComponentTypeHandle<MovementState>(true); + ResolvedMovementHandleRO = state.GetComponentTypeHandle<ResolvedMovement>(true); + + entityQueryRotation = state.GetEntityQuery( + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadOnly<MovementSettings>(), + ComponentType.ReadOnly<MovementState>(), + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadOnly<AgentMovementPlane>(), + ComponentType.ReadOnly<MovementControl>(), + ComponentType.ReadWrite<ResolvedMovement>(), + ComponentType.ReadOnly<SimulateMovement>(), + ComponentType.ReadOnly<SimulateMovementFinalize>() + ); + + entityQueryMove = state.GetEntityQuery( + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadOnly<AgentMovementPlane>(), + ComponentType.ReadWrite<MovementState>(), + ComponentType.ReadOnly<MovementSettings>(), + ComponentType.ReadOnly<ResolvedMovement>(), + ComponentType.ReadWrite<MovementStatistics>(), + + ComponentType.ReadOnly<SimulateMovement>(), + ComponentType.ReadOnly<SimulateMovementFinalize>() + ); + + entityQueryWithGravity = state.GetEntityQuery( + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadWrite<AgentMovementPlane>(), + ComponentType.ReadWrite<MovementState>(), + ComponentType.ReadOnly<MovementSettings>(), + ComponentType.ReadWrite<ResolvedMovement>(), + ComponentType.ReadWrite<MovementStatistics>(), + ComponentType.ReadOnly<MovementControl>(), + ComponentType.ReadWrite<GravityState>(), + + ComponentType.ReadOnly<AgentMovementPlaneSource>(), + ComponentType.ReadOnly<SimulateMovement>(), + ComponentType.ReadOnly<SimulateMovementFinalize>() + ); + + entityQueryPrepareMovement = jobRepairPathScheduler.GetEntityQuery(Allocator.Temp).WithAll<SimulateMovement, SimulateMovementRepair>().Build(ref state); + + entityQueryGizmos = state.GetEntityQuery( + ComponentType.ReadOnly<LocalTransform>(), + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadOnly<MovementSettings>(), + ComponentType.ReadOnly<AgentMovementPlane>(), + ComponentType.ReadOnly<ManagedState>(), + ComponentType.ReadOnly<MovementState>(), + ComponentType.ReadOnly<ResolvedMovement>(), + + ComponentType.ReadOnly<SimulateMovement>() + ); + + entityQueryMovementOverride = state.GetEntityQuery( + ComponentType.ReadWrite<ManagedMovementOverrideBeforeMovement>(), + + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadWrite<AgentCylinderShape>(), + ComponentType.ReadWrite<AgentMovementPlane>(), + ComponentType.ReadWrite<DestinationPoint>(), + ComponentType.ReadWrite<MovementState>(), + ComponentType.ReadWrite<MovementStatistics>(), + ComponentType.ReadWrite<ManagedState>(), + ComponentType.ReadWrite<MovementSettings>(), + ComponentType.ReadWrite<ResolvedMovement>(), + ComponentType.ReadWrite<MovementControl>(), + + ComponentType.Exclude<AgentOffMeshLinkTraversal>(), + ComponentType.ReadOnly<SimulateMovement>(), + ComponentType.ReadOnly<SimulateMovementControl>() + ); + } + + static readonly ProfilerMarker MarkerMovementOverride = new ProfilerMarker("MovementOverrideBeforeMovement"); + + public void OnDestroy (ref SystemState state) { + jobRepairPathScheduler.Dispose(); + } + + public void OnUpdate (ref SystemState systemState) { + var draw = DrawingManager.GetBuilder(); + + // This system is executed at least every frame to make sure the agent is moving smoothly even at high fps. + // The control loop and local avoidance may be running less often. + // So this is designated a "cheap" system, and we use the corresponding delta time for that. + var dt = AIMovementSystemGroup.TimeScaledRateManager.CheapStepDeltaTime; + + systemState.Dependency = new JobAlignAgentWithMovementDirection { + dt = dt, + }.Schedule(entityQueryRotation, systemState.Dependency); + + RunMovementOverrideBeforeMovement(ref systemState, dt); + + // Move all agents which do not have a GravityState component + systemState.Dependency = new JobMoveAgent { + dt = dt, + }.ScheduleParallel(entityQueryMove, systemState.Dependency); + + ScheduleApplyGravity(ref systemState, draw, dt); + var gizmosDependency = systemState.Dependency; + + UpdateTypeHandles(ref systemState); + + systemState.Dependency = ScheduleRepairPaths(ref systemState, systemState.Dependency); + + // Draw gizmos only in the editor, and at most once per frame. + // The movement calculations may run multiple times per frame when using high time-scales, + // but rendering gizmos more than once would just lead to clutter. + if (Application.isEditor && AIMovementSystemGroup.TimeScaledRateManager.IsLastSubstep) { + gizmosDependency = ScheduleDrawGizmos(draw, systemState.Dependency); + } + + // Render gizmos as soon as all relevant jobs are done + draw.DisposeAfter(gizmosDependency); + systemState.Dependency = ScheduleSyncEntitiesToTransforms(ref systemState, systemState.Dependency); + systemState.Dependency = JobHandle.CombineDependencies(systemState.Dependency, gizmosDependency); + } + + void ScheduleApplyGravity (ref SystemState systemState, CommandBuilder draw, float dt) { + Profiler.BeginSample("Gravity"); + // Note: We cannot use CalculateEntityCountWithoutFiltering here, because the GravityState component can be disabled + var count = entityQueryWithGravity.CalculateEntityCount(); + var raycastCommands = CollectionHelper.CreateNativeArray<RaycastCommand>(count, systemState.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + var raycastHits = CollectionHelper.CreateNativeArray<RaycastHit>(count, systemState.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + + // Prepare raycasts for all entities that have a GravityState component + systemState.Dependency = new JobPrepareAgentRaycasts { + raycastQueryParameters = new QueryParameters(-1, false, QueryTriggerInteraction.Ignore, false), + raycastCommands = raycastCommands, + draw = draw, + dt = dt, + gravity = Physics.gravity.y, + }.ScheduleParallel(entityQueryWithGravity, systemState.Dependency); + + var raycastJob = RaycastCommand.ScheduleBatch(raycastCommands, raycastHits, 32, 1, systemState.Dependency); + + // Apply gravity and move all agents that have a GravityState component + systemState.Dependency = new JobApplyGravity { + raycastHits = raycastHits, + raycastCommands = raycastCommands, + draw = draw, + dt = dt, + }.ScheduleParallel(entityQueryWithGravity, JobHandle.CombineDependencies(systemState.Dependency, raycastJob)); + + Profiler.EndSample(); + } + + void RunMovementOverrideBeforeMovement (ref SystemState systemState, float dt) { + if (!entityQueryMovementOverride.IsEmptyIgnoreFilter) { + MarkerMovementOverride.Begin(); + // The movement overrides always run on the main thread. + // This adds a sync point, but only if people actually add a movement override (which is rare). + systemState.CompleteDependency(); + new JobManagedMovementOverrideBeforeMovement { + dt = dt, + // TODO: Add unit test to make sure it fires/not fires when it should + }.Run(entityQueryMovementOverride); + MarkerMovementOverride.End(); + } + } + + void UpdateTypeHandles (ref SystemState systemState) { + MovementStateTypeHandleRO.Update(ref systemState); + ResolvedMovementHandleRO.Update(ref systemState); + } + + JobHandle ScheduleRepairPaths (ref SystemState systemState, JobHandle dependency) { + Profiler.BeginSample("RepairPaths"); + // This job accesses graph data, but this is safe because the AIMovementSystemGroup + // holds a read lock on the graph data while its subsystems are running. + dependency = jobRepairPathScheduler.ScheduleParallel(ref systemState, entityQueryPrepareMovement, dependency); + Profiler.EndSample(); + return dependency; + } + + JobHandle ScheduleDrawGizmos (CommandBuilder commandBuilder, JobHandle dependency) { + // Note: The ScheduleRepairPaths job runs right before this, so those handles are still valid + return new JobDrawFollowerGizmos { + draw = commandBuilder, + entityManagerHandle = jobRepairPathScheduler.entityManagerHandle, + LocalTransformTypeHandleRO = jobRepairPathScheduler.LocalTransformTypeHandleRO, + AgentCylinderShapeHandleRO = jobRepairPathScheduler.AgentCylinderShapeTypeHandleRO, + MovementSettingsHandleRO = jobRepairPathScheduler.MovementSettingsTypeHandleRO, + AgentMovementPlaneHandleRO = jobRepairPathScheduler.AgentMovementPlaneTypeHandleRO, + ManagedStateHandleRW = jobRepairPathScheduler.ManagedStateTypeHandleRW, + MovementStateHandleRO = MovementStateTypeHandleRO, + ResolvedMovementHandleRO = ResolvedMovementHandleRO, + }.ScheduleParallel(entityQueryGizmos, dependency); + } + + JobHandle ScheduleSyncEntitiesToTransforms (ref SystemState systemState, JobHandle dependency) { + Profiler.BeginSample("SyncEntitiesToTransforms"); + int numComponents = BatchedEvents.GetComponents<FollowerEntity>(BatchedEvents.Event.None, out var transforms, out var components); + if (numComponents == 0) { + Profiler.EndSample(); + return dependency; + } + + var entities = CollectionHelper.CreateNativeArray<Entity>(numComponents, systemState.WorldUpdateAllocator); + for (int i = 0; i < numComponents; i++) entities[i] = components[i].entity; + + dependency = new JobSyncEntitiesToTransforms { + entities = entities, + syncPositionWithTransform = SystemAPI.GetComponentLookup<SyncPositionWithTransform>(true), + syncRotationWithTransform = SystemAPI.GetComponentLookup<SyncRotationWithTransform>(true), + orientationYAxisForward = SystemAPI.GetComponentLookup<OrientationYAxisForward>(true), + entityPositions = SystemAPI.GetComponentLookup<LocalTransform>(true), + movementState = SystemAPI.GetComponentLookup<MovementState>(true), + }.Schedule(transforms, dependency); + Profiler.EndSample(); + return dependency; + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs.meta new file mode 100644 index 0000000..31a2d28 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f857e04ce9382d74989b3d469a0b956e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs new file mode 100644 index 0000000..a97172c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs @@ -0,0 +1,194 @@ +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Transforms; +using UnityEngine; +using Unity.Collections; +using Unity.Core; +using Unity.Jobs; + +namespace Pathfinding.ECS { + [UpdateAfter(typeof(TransformSystemGroup))] + public partial class AIMovementSystemGroup : ComponentSystemGroup { + /// <summary>Rate manager which runs a system group multiple times if the delta time is higher than desired, but always executes the group at least once per frame</summary> + public class TimeScaledRateManager : IRateManager, System.IDisposable { + int numUpdatesThisFrame; + int updateIndex; + float stepDt; + float maximumDt = 1.0f / 30.0f; + NativeList<TimeData> cheapTimeDataQueue; + NativeList<TimeData> timeDataQueue; + double lastFullSimulation; + double lastCheapSimulation; + static bool cheapSimulationOnly; + static bool isLastSubstep; + static bool inGroup; + static TimeData cheapTimeData; + + /// <summary> + /// True if it was determined that zero substeps should be simulated. + /// In this case all systems will get an opportunity to run a single update, + /// but they should avoid systems that don't have to run every single frame. + /// </summary> + public static bool CheapSimulationOnly { + get { + if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager"); + return cheapSimulationOnly; + } + } + + public static float CheapStepDeltaTime { + get { + if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager"); + return cheapTimeData.DeltaTime; + } + } + + /// <summary>True when this is the last substep of the current simulation</summary> + public static bool IsLastSubstep { + get { + if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager"); + return isLastSubstep; + } + } + + public TimeScaledRateManager () { + cheapTimeDataQueue = new NativeList<TimeData>(Allocator.Persistent); + timeDataQueue = new NativeList<TimeData>(Allocator.Persistent); + } + + public void Dispose () { + cheapTimeDataQueue.Dispose(); + timeDataQueue.Dispose(); + } + + public bool ShouldGroupUpdate (ComponentSystemGroup group) { + // if this is true, means we're being called a second or later time in a loop. + if (inGroup) { + group.World.PopTime(); + updateIndex++; + if (updateIndex >= numUpdatesThisFrame) { + inGroup = false; + return false; + } + } else { + cheapTimeDataQueue.Clear(); + timeDataQueue.Clear(); + + if (inGroup) throw new System.InvalidOperationException("Cannot nest simulation groups using TimeScaledRateManager"); + var fullDt = (float)(group.World.Time.ElapsedTime - lastFullSimulation); + + // It has been observed that the time move backwards. + // Not quite sure when it happens, but we need to guard against it. + if (fullDt < 0) fullDt = 0; + + // If the delta time is large enough we may want to perform multiple simulation sub-steps per frame. + // This is done to improve simulation stability. In particular at high time scales, but it also + // helps at low fps, or if the game has a sudden long stutter. + // We raise the value to a power slightly smaller than 1 to make the number of sub-steps increase + // more slowly as the delta time increases. This is important to avoid the edge case when + // the time it takes to run the simulation is longer than maximumDt. Otherwise the number of + // simulation sub-steps would increase without bound. However, the simulation quality + // may decrease a bit as the number of sub-steps increases. + numUpdatesThisFrame = Mathf.FloorToInt(Mathf.Pow(fullDt / maximumDt, 0.8f)); + var currentTime = group.World.Time.ElapsedTime; + cheapSimulationOnly = numUpdatesThisFrame == 0; + if (cheapSimulationOnly) { + timeDataQueue.Add(new TimeData( + lastFullSimulation, + 0.0f + )); + cheapTimeDataQueue.Add(new TimeData( + currentTime, + (float)(currentTime - lastCheapSimulation) + )); + lastCheapSimulation = currentTime; + } else { + stepDt = fullDt / numUpdatesThisFrame; + // Push the time for each sub-step + for (int i = 0; i < numUpdatesThisFrame; i++) { + var stepTime = lastFullSimulation + (i+1) * stepDt; + timeDataQueue.Add(new TimeData( + stepTime, + stepDt + )); + cheapTimeDataQueue.Add(new TimeData( + stepTime, + (float)(stepTime - lastCheapSimulation) + )); + lastCheapSimulation = stepTime; + } + lastFullSimulation = currentTime; + } + numUpdatesThisFrame = Mathf.Max(1, numUpdatesThisFrame); + inGroup = true; + updateIndex = 0; + } + + group.World.PushTime(timeDataQueue[updateIndex]); + cheapTimeData = cheapTimeDataQueue[updateIndex]; + isLastSubstep = updateIndex + 1 >= numUpdatesThisFrame; + + return true; + } + + public float Timestep { + get => maximumDt; + set => maximumDt = value; + } + } + + protected override void OnUpdate () { + // Various jobs (e.g. the JobRepairPath) in this system group may use graph data, + // and they also need the graph data to be consistent during the whole update. + // For example the MovementState.hierarchicalNodeIndex field needs to be valid + // during the whole group update, as it may be used by the RVOSystem and FollowerControlSystem. + // Locking the graph data as read-only here means that no graph updates will be performed + // while these jobs are running. + var readLock = AstarPath.active != null? AstarPath.active.LockGraphDataForReading() : default; + + // And here I thought the entities package reaching 1.0 would mean that they wouldn't just rename + // properties without any compatibility code... but nope... +#if MODULE_ENTITIES_1_0_8_OR_NEWER + var systems = this.GetUnmanagedSystems(); + for (int i = 0; i < systems.Length; i++) { + ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i]); + state.Dependency = JobHandle.CombineDependencies(state.Dependency, readLock.dependency); + } +#else + var systems = this.Systems; + for (int i = 0; i < systems.Count; i++) { + ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i].SystemHandle); + state.Dependency = JobHandle.CombineDependencies(state.Dependency, readLock.dependency); + } +#endif + + base.OnUpdate(); + + JobHandle readDependency = default; +#if MODULE_ENTITIES_1_0_8_OR_NEWER + for (int i = 0; i < systems.Length; i++) { + ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i]); + readDependency = JobHandle.CombineDependencies(readDependency, state.Dependency); + } + systems.Dispose(); +#else + for (int i = 0; i < systems.Count; i++) { + ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i].SystemHandle); + readDependency = JobHandle.CombineDependencies(readDependency, state.Dependency); + } +#endif + readLock.UnlockAfter(readDependency); + } + + protected override void OnDestroy () { + base.OnDestroy(); + (this.RateManager as TimeScaledRateManager).Dispose(); + } + + protected override void OnCreate () { + base.OnCreate(); + this.RateManager = new TimeScaledRateManager(); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs.meta new file mode 100644 index 0000000..da24779 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84577891deac65d458d801d960c6fcee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs new file mode 100644 index 0000000..a4d590a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs @@ -0,0 +1,51 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Burst; +using Unity.Collections; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.ECS.RVO; + + /// <summary>Copies <see cref="MovementControl"/> to <see cref="ResolvedMovement"/> when no local avoidance is used</summary> + [BurstCompile] + [UpdateAfter(typeof(FollowerControlSystem))] + [UpdateAfter(typeof(RVOSystem))] // Has to execute after RVOSystem in case that system detects that some agents should not be simulated using the RVO system anymore. + [UpdateInGroup(typeof(AIMovementSystemGroup))] + [RequireMatchingQueriesForUpdate] + public partial struct FallbackResolveMovementSystem : ISystem { + EntityQuery entityQuery; + + public void OnCreate (ref SystemState state) { + entityQuery = state.GetEntityQuery(new EntityQueryDesc { + All = new ComponentType[] { + ComponentType.ReadWrite<ResolvedMovement>(), + ComponentType.ReadOnly<MovementControl>(), + ComponentType.ReadOnly<SimulateMovement>() + }, + Options = EntityQueryOptions.FilterWriteGroup + }); + } + + public void OnDestroy (ref SystemState state) { } + + public void OnUpdate (ref SystemState systemState) { + new CopyJob {}.Schedule(entityQuery); + } + + [BurstCompile] + public partial struct CopyJob : IJobEntity { + public void Execute (in MovementControl control, ref ResolvedMovement resolved) { + resolved.targetPoint = control.targetPoint; + resolved.speed = control.speed; + resolved.turningRadiusMultiplier = 1.0f; + resolved.targetRotation = control.targetRotation; + resolved.targetRotationHint = control.targetRotationHint; + resolved.targetRotationOffset = control.targetRotationOffset; + resolved.rotationSpeed = control.rotationSpeed; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs.meta new file mode 100644 index 0000000..fbac068 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5502c9f6a3e7fc448803d8b0607c6eac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs new file mode 100644 index 0000000..bc50184 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs @@ -0,0 +1,322 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Entities; +using UnityEngine.Profiling; +using Unity.Profiling; +using Unity.Transforms; +using Unity.Burst; +using Unity.Jobs; +using GCHandle = System.Runtime.InteropServices.GCHandle; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.ECS.RVO; + using Pathfinding.Drawing; + using Pathfinding.RVO; + using Unity.Collections; + using Unity.Burst.Intrinsics; + using System.Diagnostics; + + [UpdateInGroup(typeof(AIMovementSystemGroup))] + [BurstCompile] + public partial struct FollowerControlSystem : ISystem { + EntityQuery entityQueryPrepare; + EntityQuery entityQueryControl; + EntityQuery entityQueryControlManaged; + EntityQuery entityQueryControlManaged2; + EntityQuery entityQueryOffMeshLink; + EntityQuery entityQueryOffMeshLinkCleanup; + public JobRepairPath.Scheduler jobRepairPathScheduler; + RedrawScope redrawScope; + + static readonly ProfilerMarker MarkerMovementOverrideBeforeControl = new ProfilerMarker("MovementOverrideBeforeControl"); + static readonly ProfilerMarker MarkerMovementOverrideAfterControl = new ProfilerMarker("MovementOverrideAfterControl"); + + public void OnCreate (ref SystemState state) { + jobRepairPathScheduler = new JobRepairPath.Scheduler(ref state); + + redrawScope = DrawingManager.GetRedrawScope(); + + entityQueryPrepare = jobRepairPathScheduler.GetEntityQuery(Unity.Collections.Allocator.Temp).WithAll<SimulateMovement, SimulateMovementRepair>().Build(ref state); + entityQueryControl = state.GetEntityQuery( + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadOnly<AgentMovementPlane>(), + ComponentType.ReadOnly<DestinationPoint>(), + ComponentType.ReadWrite<MovementState>(), + ComponentType.ReadOnly<MovementStatistics>(), + ComponentType.ReadWrite<ManagedState>(), + ComponentType.ReadOnly<MovementSettings>(), + ComponentType.ReadOnly<ResolvedMovement>(), + ComponentType.ReadWrite<MovementControl>(), + + ComponentType.Exclude<AgentOffMeshLinkTraversal>(), + ComponentType.ReadOnly<SimulateMovement>(), + ComponentType.ReadOnly<SimulateMovementControl>() + ); + + entityQueryControlManaged = state.GetEntityQuery( + ComponentType.ReadWrite<ManagedMovementOverrideBeforeControl>(), + + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadWrite<AgentCylinderShape>(), + ComponentType.ReadWrite<AgentMovementPlane>(), + ComponentType.ReadWrite<DestinationPoint>(), + ComponentType.ReadWrite<MovementState>(), + ComponentType.ReadWrite<MovementStatistics>(), + ComponentType.ReadWrite<ManagedState>(), + ComponentType.ReadWrite<MovementSettings>(), + ComponentType.ReadWrite<ResolvedMovement>(), + ComponentType.ReadWrite<MovementControl>(), + + ComponentType.Exclude<AgentOffMeshLinkTraversal>(), + ComponentType.ReadOnly<SimulateMovement>(), + ComponentType.ReadOnly<SimulateMovementControl>() + ); + + entityQueryControlManaged2 = state.GetEntityQuery( + ComponentType.ReadWrite<ManagedMovementOverrideAfterControl>(), + + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadWrite<AgentCylinderShape>(), + ComponentType.ReadWrite<AgentMovementPlane>(), + ComponentType.ReadWrite<DestinationPoint>(), + ComponentType.ReadWrite<MovementState>(), + ComponentType.ReadWrite<MovementStatistics>(), + ComponentType.ReadWrite<ManagedState>(), + ComponentType.ReadWrite<MovementSettings>(), + ComponentType.ReadWrite<ResolvedMovement>(), + ComponentType.ReadWrite<MovementControl>(), + + ComponentType.Exclude<AgentOffMeshLinkTraversal>(), + ComponentType.ReadOnly<SimulateMovement>(), + ComponentType.ReadOnly<SimulateMovementControl>() + ); + + entityQueryOffMeshLink = state.GetEntityQuery( + ComponentType.ReadWrite<LocalTransform>(), + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadWrite<AgentMovementPlane>(), + ComponentType.ReadOnly<DestinationPoint>(), + ComponentType.ReadWrite<MovementState>(), + ComponentType.ReadOnly<MovementStatistics>(), + ComponentType.ReadWrite<ManagedState>(), + ComponentType.ReadWrite<MovementSettings>(), + ComponentType.ReadOnly<ResolvedMovement>(), + ComponentType.ReadWrite<MovementControl>(), + ComponentType.ReadWrite<AgentOffMeshLinkTraversal>(), + ComponentType.ReadWrite<ManagedAgentOffMeshLinkTraversal>(), + ComponentType.ReadOnly<SimulateMovement>() + ); + + entityQueryOffMeshLinkCleanup = state.GetEntityQuery( + // ManagedAgentOffMeshLinkTraversal is a cleanup component. + // If it exists, but the AgentOffMeshLinkTraversal does not exist, + // then the agent must have been destroyed while traversing the off-mesh link. + ComponentType.ReadWrite<ManagedAgentOffMeshLinkTraversal>(), + ComponentType.Exclude<AgentOffMeshLinkTraversal>() + ); + } + + public void OnDestroy (ref SystemState state) { + redrawScope.Dispose(); + jobRepairPathScheduler.Dispose(); + } + + public void OnUpdate (ref SystemState systemState) { + if (AstarPath.active == null) return; + + var commandBuffer = new EntityCommandBuffer(systemState.WorldUpdateAllocator); + + SyncLocalAvoidanceComponents(ref systemState, commandBuffer); + SchedulePaths(ref systemState); + StartOffMeshLinkTraversal(ref systemState, commandBuffer); + + commandBuffer.Playback(systemState.EntityManager); + commandBuffer.Dispose(); + + ProcessActiveOffMeshLinkTraversal(ref systemState); + RepairPaths(ref systemState); + + // The full movement calculations do not necessarily need to be done every frame if the fps is high + if (!AIMovementSystemGroup.TimeScaledRateManager.CheapSimulationOnly) { + ProcessControlLoop(ref systemState, SystemAPI.Time.DeltaTime); + } + } + + void SyncLocalAvoidanceComponents (ref SystemState systemState, EntityCommandBuffer commandBuffer) { + var simulator = RVOSimulator.active?.GetSimulator(); + // First check if we have a simulator. If not, we can skip handling RVO components + if (simulator == null) return; + + Profiler.BeginSample("AddRVOComponents"); + foreach (var(managedState, entity) in SystemAPI.Query<ManagedState>().WithNone<RVOAgent>().WithEntityAccess()) { + if (managedState.enableLocalAvoidance) { + commandBuffer.AddComponent<RVOAgent>(entity, managedState.rvoSettings); + } + } + Profiler.EndSample(); + Profiler.BeginSample("CopyRVOSettings"); + foreach (var(managedState, rvoAgent, entity) in SystemAPI.Query<ManagedState, RefRW<RVOAgent> >().WithEntityAccess()) { + rvoAgent.ValueRW = managedState.rvoSettings; + if (!managedState.enableLocalAvoidance) { + commandBuffer.RemoveComponent<RVOAgent>(entity); + } + } + + Profiler.EndSample(); + } + + void RepairPaths (ref SystemState systemState) { + Profiler.BeginSample("RepairPaths"); + // This job accesses managed component data in a somewhat unsafe way. + // It should be safe to run it in parallel with other systems, but I'm not 100% sure. + // This job also accesses graph data, but this is safe because the AIMovementSystemGroup + // holds a read lock on the graph data while its subsystems are running. + systemState.Dependency = jobRepairPathScheduler.ScheduleParallel(ref systemState, entityQueryPrepare, systemState.Dependency); + Profiler.EndSample(); + } + + [BurstCompile] + [WithAbsent(typeof(ManagedAgentOffMeshLinkTraversal))] // Do not recalculate the path of agents that are currently traversing an off-mesh link. + partial struct JobShouldRecalculatePaths : IJobEntity { + public float time; + public NativeBitArray shouldRecalculatePath; + int index; + + public void Execute (ref ECS.AutoRepathPolicy autoRepathPolicy, in LocalTransform transform, in AgentCylinderShape shape, in DestinationPoint destination) { + if (index >= shouldRecalculatePath.Length) { + shouldRecalculatePath.Resize(shouldRecalculatePath.Length * 2, NativeArrayOptions.ClearMemory); + } + shouldRecalculatePath.Set(index++, autoRepathPolicy.ShouldRecalculatePath(transform.Position, shape.radius, destination.destination, time)); + } + } + + [WithAbsent(typeof(ManagedAgentOffMeshLinkTraversal))] // Do not recalculate the path of agents that are currently traversing an off-mesh link. + public partial struct JobRecalculatePaths : IJobEntity { + public float time; + public NativeBitArray shouldRecalculatePath; + int index; + + public void Execute (ManagedState state, ref ECS.AutoRepathPolicy autoRepathPolicy, ref LocalTransform transform, ref DestinationPoint destination, ref AgentMovementPlane movementPlane) { + MaybeRecalculatePath(state, ref autoRepathPolicy, ref transform, ref destination, ref movementPlane, time, shouldRecalculatePath.IsSet(index++)); + } + + public static void MaybeRecalculatePath (ManagedState state, ref ECS.AutoRepathPolicy autoRepathPolicy, ref LocalTransform transform, ref DestinationPoint destination, ref AgentMovementPlane movementPlane, float time, bool wantsToRecalculatePath) { + if ((state.pathTracer.isStale || wantsToRecalculatePath) && state.pendingPath == null) { + if (autoRepathPolicy.mode != Pathfinding.AutoRepathPolicy.Mode.Never && float.IsFinite(destination.destination.x)) { + var path = ABPath.Construct(transform.Position, destination.destination, null); + path.UseSettings(state.pathfindingSettings); + path.nnConstraint.distanceMetric = DistanceMetric.ClosestAsSeenFromAboveSoft(movementPlane.value.up); + ManagedState.SetPath(path, state, in movementPlane, ref destination); + autoRepathPolicy.DidRecalculatePath(destination.destination, time); + } + } + } + } + + void SchedulePaths (ref SystemState systemState) { + Profiler.BeginSample("Schedule search"); + // Block the pathfinding threads from starting new path calculations while this loop is running. + // This is done to reduce lock contention and significantly improve performance. + // If we did not do this, all pathfinding threads would immediately wake up when a path was pushed to the queue. + // Immediately when they wake up they will try to acquire a lock on the path queue. + // If we are scheduling a lot of paths, this causes significant contention, and can make this loop take 100 times + // longer to complete, compared to if we block the pathfinding threads. + // TODO: Switch to a lock-free queue to avoid this issue altogether. + var bits = new NativeBitArray(512, Allocator.TempJob); + systemState.CompleteDependency(); + var pathfindingLock = AstarPath.active.PausePathfindingSoon(); + // Calculate which agents want to recalculate their path (using burst) + new JobShouldRecalculatePaths { + time = (float)SystemAPI.Time.ElapsedTime, + shouldRecalculatePath = bits, + }.Run(); + // Schedule the path calculations + new JobRecalculatePaths { + time = (float)SystemAPI.Time.ElapsedTime, + shouldRecalculatePath = bits, + }.Run(); + pathfindingLock.Release(); + bits.Dispose(); + Profiler.EndSample(); + } + + void StartOffMeshLinkTraversal (ref SystemState systemState, EntityCommandBuffer commandBuffer) { + Profiler.BeginSample("Start off-mesh link traversal"); + foreach (var(state, entity) in SystemAPI.Query<ManagedState>().WithAll<ReadyToTraverseOffMeshLink>() + .WithEntityAccess() + // Do not try to add another off-mesh link component to agents that already have one. + .WithNone<AgentOffMeshLinkTraversal>()) { + // UnityEngine.Assertions.Assert.IsTrue(movementState.ValueRO.reachedEndOfPart && state.pathTracer.isNextPartValidLink); + var linkInfo = NextLinkToTraverse(state); + var ctx = new AgentOffMeshLinkTraversalContext(linkInfo.link); + // Add the AgentOffMeshLinkTraversal and ManagedAgentOffMeshLinkTraversal components when the agent should start traversing an off-mesh link. + commandBuffer.AddComponent(entity, new AgentOffMeshLinkTraversal(linkInfo)); + commandBuffer.AddComponent(entity, new ManagedAgentOffMeshLinkTraversal(ctx, ResolveOffMeshLinkHandler(state, ctx))); + } + Profiler.EndSample(); + } + + public static OffMeshLinks.OffMeshLinkTracer NextLinkToTraverse (ManagedState state) { + return state.pathTracer.GetLinkInfo(1); + } + + public static IOffMeshLinkHandler ResolveOffMeshLinkHandler (ManagedState state, AgentOffMeshLinkTraversalContext ctx) { + var handler = state.onTraverseOffMeshLink ?? ctx.concreteLink.handler; + return handler; + } + + void ProcessActiveOffMeshLinkTraversal (ref SystemState systemState) { + var commandBuffer = new EntityCommandBuffer(systemState.WorldUpdateAllocator); + systemState.CompleteDependency(); + new JobManagedOffMeshLinkTransition { + commandBuffer = commandBuffer, + deltaTime = AIMovementSystemGroup.TimeScaledRateManager.CheapStepDeltaTime, + }.Run(entityQueryOffMeshLink); + + new JobManagedOffMeshLinkTransitionCleanup().Run(entityQueryOffMeshLinkCleanup); +#if MODULE_ENTITIES_1_0_8_OR_NEWER + commandBuffer.RemoveComponent<ManagedAgentOffMeshLinkTraversal>(entityQueryOffMeshLinkCleanup, EntityQueryCaptureMode.AtPlayback); +#else + commandBuffer.RemoveComponent<ManagedAgentOffMeshLinkTraversal>(entityQueryOffMeshLinkCleanup); +#endif + commandBuffer.Playback(systemState.EntityManager); + commandBuffer.Dispose(); + } + + void ProcessControlLoop (ref SystemState systemState, float dt) { + // This is a hook for other systems to modify the movement of agents. + // Normally it is not used. + if (!entityQueryControlManaged.IsEmpty) { + MarkerMovementOverrideBeforeControl.Begin(); + systemState.Dependency.Complete(); + new JobManagedMovementOverrideBeforeControl { + dt = dt, + }.Run(entityQueryControlManaged); + MarkerMovementOverrideBeforeControl.End(); + } + + redrawScope.Rewind(); + var draw = DrawingManager.GetBuilder(redrawScope); + var navmeshEdgeData = AstarPath.active.GetNavmeshBorderData(out var readLock); + systemState.Dependency = new JobControl { + navmeshEdgeData = navmeshEdgeData, + draw = draw, + dt = dt, + }.ScheduleParallel(entityQueryControl, JobHandle.CombineDependencies(systemState.Dependency, readLock.dependency)); + readLock.UnlockAfter(systemState.Dependency); + draw.DisposeAfter(systemState.Dependency); + + if (!entityQueryControlManaged2.IsEmpty) { + MarkerMovementOverrideAfterControl.Begin(); + systemState.Dependency.Complete(); + new JobManagedMovementOverrideAfterControl { + dt = dt, + }.Run(entityQueryControlManaged2); + MarkerMovementOverrideAfterControl.End(); + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs.meta new file mode 100644 index 0000000..653caa6 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce754b44fa448624dac9bbefe12d03e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs new file mode 100644 index 0000000..de40655 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs @@ -0,0 +1,188 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; +using Unity.Burst; +using Unity.Collections; + +namespace Pathfinding.ECS { + using System.Collections.Generic; + using System.Runtime.InteropServices; + using Pathfinding; + using Pathfinding.Drawing; + using Pathfinding.Util; + using Unity.Transforms; + using UnityEngine.Profiling; + + [UpdateBefore(typeof(FollowerControlSystem))] + [UpdateInGroup(typeof(AIMovementSystemGroup))] + [RequireMatchingQueriesForUpdate] + [BurstCompile] + public partial struct MovementPlaneFromGraphSystem : ISystem { + public EntityQuery entityQueryGraph; + public EntityQuery entityQueryNormal; + NativeArray<float3> sphereSamplePoints; + // Store the queue in a GCHandle to avoid restrictions on ISystem + GCHandle graphNodeQueue; + + public void OnCreate (ref SystemState state) { + entityQueryGraph = state.GetEntityQuery(ComponentType.ReadOnly<MovementState>(), ComponentType.ReadWrite<AgentMovementPlane>(), ComponentType.ReadOnly<AgentMovementPlaneSource>()); + entityQueryGraph.SetSharedComponentFilter(new AgentMovementPlaneSource { value = MovementPlaneSource.Graph }); + entityQueryNormal = state.GetEntityQuery( + ComponentType.ReadWrite<ManagedState>(), + ComponentType.ReadOnly<LocalTransform>(), + ComponentType.ReadWrite<AgentMovementPlane>(), + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadOnly<AgentMovementPlaneSource>() + ); + entityQueryNormal.AddSharedComponentFilter(new AgentMovementPlaneSource { value = MovementPlaneSource.NavmeshNormal }); + + // Number of samples to use when approximating the normal, when using the NavmeshNormal mode. + const int Samples = 16; + sphereSamplePoints = new NativeArray<float3>(Samples, Allocator.Persistent); + UnityEngine.Random.InitState(0); + for (int i = 0; i < Samples; i++) { + sphereSamplePoints[i] = (float3)UnityEngine.Random.insideUnitSphere; + } + + graphNodeQueue = GCHandle.Alloc(new List<GraphNode>(32)); + } + + public void OnDestroy (ref SystemState state) { + sphereSamplePoints.Dispose(); + graphNodeQueue.Free(); + } + + public void OnUpdate (ref SystemState systemState) { + var graphs = AstarPath.active?.data.graphs; + if (graphs == null) return; + + var movementPlanes = CollectionHelper.CreateNativeArray<AgentMovementPlane>(graphs.Length, systemState.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory); + for (int i = 0; i < graphs.Length; i++) { + var graph = graphs[i]; + var plane = new NativeMovementPlane(quaternion.identity); + if (graph is NavmeshBase navmesh) { + plane = new NativeMovementPlane(navmesh.transform.rotation); + } else if (graph is GridGraph grid) { + plane = new NativeMovementPlane(grid.transform.rotation); + } + movementPlanes[i] = new AgentMovementPlane { + value = plane, + }; + } + + if (!entityQueryNormal.IsEmpty) { + systemState.CompleteDependency(); + var vertices = new NativeList<float3>(16, Allocator.Temp); + new JobMovementPlaneFromNavmeshNormal { + dt = AIMovementSystemGroup.TimeScaledRateManager.CheapStepDeltaTime, + sphereSamplePoints = sphereSamplePoints, + vertices = vertices, + que = (List<GraphNode>)graphNodeQueue.Target, + }.Run(entityQueryNormal); + } + + systemState.Dependency = new JobMovementPlaneFromGraph { + movementPlanes = movementPlanes, + }.Schedule(entityQueryGraph, systemState.Dependency); + } + + partial struct JobMovementPlaneFromNavmeshNormal : IJobEntity { + public float dt; + [ReadOnly] + public NativeArray<float3> sphereSamplePoints; + public NativeList<float3> vertices; + public List<GraphNode> que; + + public void Execute (ManagedState managedState, in LocalTransform localTransform, ref AgentMovementPlane agentMovementPlane, in AgentCylinderShape shape) { + var sphereSamplePointsSpan = sphereSamplePoints.AsUnsafeSpan(); + var node = managedState.pathTracer.startNode; + // TODO: Expose these parameters? + float size = shape.radius * 1.5f; + const float InverseSmoothness = 20f; + if (node != null) { + vertices.Clear(); + que.Clear(); + var position = localTransform.Position; + var bounds = new UnityEngine.Bounds(position, new float3(size, size, size)); + int queStart = 0; + node.TemporaryFlag1 = true; + que.Add(node); + + while (queStart < que.Count) { + var current = que[queStart++] as TriangleMeshNode; + + current.GetVertices(out var v0, out var v1, out var v2); + var p0 = (float3)v0; + var p1 = (float3)v1; + var p2 = (float3)v2; + Polygon.ClosestPointOnTriangleByRef(in p0, in p1, in p2, in position, out var closest); + if (math.lengthsq(closest - position) < size*size) { + vertices.Add(p0); + vertices.Add(p1); + vertices.Add(p2); + current.GetConnections((GraphNode con, ref List<GraphNode> que) => { + if (!con.TemporaryFlag1) { + con.TemporaryFlag1 = true; + que.Add(con); + } + }, ref que); + } + } + + // Reset temporary flags + for (int i = 0; i < que.Count; i++) { + que[i].TemporaryFlag1 = false; + } + + var verticesSpan = vertices.AsUnsafeSpan(); + SampleTriangleNormals(ref sphereSamplePointsSpan, ref position, size, ref verticesSpan, ref agentMovementPlane, dt * InverseSmoothness); + } + } + } + + [BurstCompile] + partial struct JobMovementPlaneFromGraph : IJobEntity { + [ReadOnly] + public NativeArray<AgentMovementPlane> movementPlanes; + + public void Execute (in MovementState movementState, ref AgentMovementPlane movementPlane) { + if (movementState.graphIndex < (uint)movementPlanes.Length) { + movementPlane = movementPlanes[(int)movementState.graphIndex]; + } else { + // This can happen if the agent has no path, or if the path is stale. + // Potentially also if a graph has been removed. + } + } + } + + [BurstCompile(FloatMode = FloatMode.Fast)] + static void SampleTriangleNormals (ref UnsafeSpan<float3> samplePoints, ref float3 sampleOrigin, float sampleScale, ref UnsafeSpan<float3> triangleVertices, ref AgentMovementPlane agentMovementPlane, float alpha) { + var targetNormal = float3.zero; + int normalWeight = 0; + for (int i = 0; i < triangleVertices.Length; i += 3) { + var p0 = triangleVertices[i + 0]; + var p1 = triangleVertices[i + 1]; + var p2 = triangleVertices[i + 2]; + var triangleNormal = math.normalizesafe(math.cross(p1 - p0, p2 - p0)); + + for (int j = 0; j < samplePoints.Length; j++) { + var p = samplePoints[j] * sampleScale + sampleOrigin; + if (Polygon.ClosestPointOnTriangleByRef(in p0, in p1, in p2, in p, out var closest) && math.lengthsq(closest - sampleOrigin) < sampleScale*sampleScale) { + targetNormal += triangleNormal; + normalWeight++; + } + } + } + + if (normalWeight > 0) { + targetNormal = math.normalizesafe(targetNormal / normalWeight); + + var currentNormal = agentMovementPlane.value.up; + var nextNormal = math.lerp(currentNormal, targetNormal, math.clamp(0, 1, alpha)); + JobApplyGravity.UpdateMovementPlaneFromNormal(nextNormal, ref agentMovementPlane); + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs.meta new file mode 100644 index 0000000..f8563f3 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e03b8b6eb150263419cf52d753bc4bc5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs new file mode 100644 index 0000000..35ea267 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs @@ -0,0 +1,84 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Burst; +using GCHandle = System.Runtime.InteropServices.GCHandle; +using Unity.Transforms; + +namespace Pathfinding.ECS { + /// <summary> + /// Checks if paths have been calculated, and updates the agent's paths if they have. + /// + /// This is essentially a replacement for <see cref="Path.callback"/> for ECS agents. + /// + /// This system is a bit different in that it doesn't run in the normal update loop, + /// but instead it will run when the <see cref="AstarPath.OnPathsCalculated"/> event fires. + /// This is to avoid having to call a separate callback for every agent, since that + /// would result in excessive overhead as it would have to synchronize with the ECS world + /// on every such call. + /// + /// See: <see cref="AstarPath.OnPathsCalculated"/> + /// </summary> + [BurstCompile] + public partial struct PollPendingPathsSystem : ISystem { + GCHandle onPathsCalculated; + static bool anyPendingPaths; + + JobRepairPath.Scheduler jobRepairPathScheduler; + EntityQuery entityQueryPrepare; + + public void OnCreate (ref SystemState state) { + jobRepairPathScheduler = new JobRepairPath.Scheduler(ref state) { + onlyApplyPendingPaths = true, + }; + entityQueryPrepare = jobRepairPathScheduler.GetEntityQuery(Unity.Collections.Allocator.Temp).Build(ref state); + + var world = state.WorldUnmanaged; + System.Action onPathsCalculated = () => { + // Allow the system to run + anyPendingPaths = true; + try { + // Update the system manually + world.GetExistingUnmanagedSystem<PollPendingPathsSystem>().Update(world); + } finally { + anyPendingPaths = false; + } + }; + AstarPath.OnPathsCalculated += onPathsCalculated; + // Store the callback in a GCHandle to get around limitations on unmanaged systems. + this.onPathsCalculated = GCHandle.Alloc(onPathsCalculated); + } + + public void OnDestroy (ref SystemState state) { + AstarPath.OnPathsCalculated -= (System.Action)onPathsCalculated.Target; + onPathsCalculated.Free(); + jobRepairPathScheduler.Dispose(); + } + + void OnUpdate (ref SystemState systemState) { + // Only run the system when we have triggered it manually + if (!anyPendingPaths) return; + + // During an off-mesh link traversal, we shouldn't calculate any paths, because it's somewhat undefined where they should start. + // Paths are already cancelled when the off-mesh link traversal starts, but just in case it has been started by a user manually in some way, we also cancel them every frame. + foreach (var state in SystemAPI.Query<ManagedState>().WithAll<AgentOffMeshLinkTraversal>()) state.CancelCurrentPathRequest(); + + // The JobRepairPath may access graph data, so we need to lock it for reading. + // Otherwise a graph update could start while the job was running, which could cause all kinds of problems. + var readLock = AstarPath.active.LockGraphDataForReading(); + + // Iterate over all agents and check if they have any pending paths, and if they have been calculated. + // If they have, we update the agent's current path to the newly calculated one. + // + // We do this by running the JobRepairPath for all agents that have just had their path calculated. + // This ensures that all properties like remainingDistance are up to date immediately after + // a path recalculation. + // This may seem wasteful, but during the next update, the regular JobRepairPath job + // will most likely be able to early out, because we did most of the work here. + systemState.Dependency = jobRepairPathScheduler.ScheduleParallel(ref systemState, entityQueryPrepare, systemState.Dependency); + + readLock.UnlockAfter(systemState.Dependency); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs.meta new file mode 100644 index 0000000..6f6c2c7 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12ab30eb86c3d4841b72f49aa252574c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs new file mode 100644 index 0000000..3f6b217 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs @@ -0,0 +1,254 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Mathematics; +using Unity.Burst; +using Unity.Entities; +using Unity.Transforms; +using Unity.Collections; +using GCHandle = System.Runtime.InteropServices.GCHandle; + +namespace Pathfinding.ECS.RVO { + using Pathfinding.RVO; + + [BurstCompile] + [UpdateAfter(typeof(FollowerControlSystem))] + [UpdateInGroup(typeof(AIMovementSystemGroup))] + public partial struct RVOSystem : ISystem { + EntityQuery entityQuery; + /// <summary> + /// Keeps track of the last simulator that this RVOSystem saw. + /// This is a weak GCHandle to allow it to be stored in an ISystem. + /// </summary> + GCHandle lastSimulator; + EntityQuery withAgentIndex; + EntityQuery shouldBeAddedToSimulation; + EntityQuery shouldBeRemovedFromSimulation; + ComponentLookup<AgentOffMeshLinkTraversal> agentOffMeshLinkTraversalLookup; + + public void OnCreate (ref SystemState state) { + entityQuery = state.GetEntityQuery( + ComponentType.ReadOnly<AgentCylinderShape>(), + ComponentType.ReadOnly<LocalTransform>(), + ComponentType.ReadOnly<RVOAgent>(), + ComponentType.ReadOnly<AgentIndex>(), + ComponentType.ReadOnly<AgentMovementPlane>(), + ComponentType.ReadOnly<MovementControl>(), + ComponentType.ReadWrite<ResolvedMovement>(), + ComponentType.ReadOnly<SimulateMovement>() + ); + withAgentIndex = state.GetEntityQuery( + ComponentType.ReadWrite<AgentIndex>() + ); + shouldBeAddedToSimulation = state.GetEntityQuery( + ComponentType.ReadOnly<RVOAgent>(), + ComponentType.Exclude<AgentIndex>() + ); + shouldBeRemovedFromSimulation = state.GetEntityQuery( + ComponentType.ReadOnly<AgentIndex>(), + ComponentType.Exclude<RVOAgent>() + ); + lastSimulator = GCHandle.Alloc(null, System.Runtime.InteropServices.GCHandleType.Weak); + agentOffMeshLinkTraversalLookup = state.GetComponentLookup<AgentOffMeshLinkTraversal>(true); + } + + public void OnDestroy (ref SystemState state) { + lastSimulator.Free(); + } + + public void OnUpdate (ref SystemState systemState) { + var simulator = RVOSimulator.active?.GetSimulator(); + + if (simulator != lastSimulator.Target) { + // If the simulator has been destroyed, we need to remove all AgentIndex components + RemoveAllAgentsFromSimulation(ref systemState); + lastSimulator.Target = simulator; + } + if (simulator == null) return; + + AddAndRemoveAgentsFromSimulation(ref systemState, simulator); + + // The full movement calculations do not necessarily need to be done every frame if the fps is high + if (AIMovementSystemGroup.TimeScaledRateManager.CheapSimulationOnly) { + return; + } + + CopyFromEntitiesToRVOSimulator(ref systemState, simulator, SystemAPI.Time.DeltaTime); + + // Schedule RVO update + systemState.Dependency = simulator.Update( + systemState.Dependency, + SystemAPI.Time.DeltaTime, + AIMovementSystemGroup.TimeScaledRateManager.IsLastSubstep, + systemState.WorldUpdateAllocator + ); + + CopyFromRVOSimulatorToEntities(ref systemState, simulator); + simulator.LockSimulationDataReadOnly(systemState.Dependency); + } + + void RemoveAllAgentsFromSimulation (ref SystemState systemState) { + var buffer = new EntityCommandBuffer(Allocator.Temp); + var entities = withAgentIndex.ToEntityArray(systemState.WorldUpdateAllocator); + buffer.RemoveComponent<AgentIndex>(entities); + buffer.Playback(systemState.EntityManager); + buffer.Dispose(); + } + + void AddAndRemoveAgentsFromSimulation (ref SystemState systemState, SimulatorBurst simulator) { + // Remove all agents from the simulation that do not have an RVOAgent component, but have an AgentIndex + var indicesToRemove = shouldBeRemovedFromSimulation.ToComponentDataArray<AgentIndex>(systemState.WorldUpdateAllocator); + // Add all agents to the simulation that have an RVOAgent component, but not AgentIndex component + var entitiesToAdd = shouldBeAddedToSimulation.ToEntityArray(systemState.WorldUpdateAllocator); + // Avoid a sync point in the common case + if (indicesToRemove.Length > 0 || entitiesToAdd.Length > 0) { + var buffer = new EntityCommandBuffer(Allocator.Temp); +#if MODULE_ENTITIES_1_0_8_OR_NEWER + buffer.RemoveComponent<AgentIndex>(shouldBeRemovedFromSimulation, EntityQueryCaptureMode.AtPlayback); +#else + buffer.RemoveComponent<AgentIndex>(shouldBeRemovedFromSimulation); +#endif + for (int i = 0; i < indicesToRemove.Length; i++) { + simulator.RemoveAgent(indicesToRemove[i]); + } + for (int i = 0; i < entitiesToAdd.Length; i++) { + buffer.AddComponent<AgentIndex>(entitiesToAdd[i], simulator.AddAgentBurst(UnityEngine.Vector3.zero)); + } + + buffer.Playback(systemState.EntityManager); + buffer.Dispose(); + } + } + + void CopyFromEntitiesToRVOSimulator (ref SystemState systemState, SimulatorBurst simulator, float dt) { + agentOffMeshLinkTraversalLookup.Update(ref systemState); + systemState.Dependency = new JobCopyFromEntitiesToRVOSimulator { + agentData = simulator.simulationData, + agentOutputData = simulator.outputData, + movementPlaneMode = simulator.movementPlane, + agentOffMeshLinkTraversalLookup = agentOffMeshLinkTraversalLookup, + dt = dt, + }.ScheduleParallel(entityQuery, systemState.Dependency); + } + + void CopyFromRVOSimulatorToEntities (ref SystemState systemState, SimulatorBurst simulator) { + systemState.Dependency = new JobCopyFromRVOSimulatorToEntities { + quadtree = simulator.quadtree, + agentData = simulator.simulationData, + agentOutputData = simulator.outputData, + }.ScheduleParallel(entityQuery, systemState.Dependency); + } + + [BurstCompile] + public partial struct JobCopyFromEntitiesToRVOSimulator : IJobEntity { + [NativeDisableParallelForRestriction] + public SimulatorBurst.AgentData agentData; + [ReadOnly] + public SimulatorBurst.AgentOutputData agentOutputData; + public MovementPlane movementPlaneMode; + [ReadOnly] + public ComponentLookup<AgentOffMeshLinkTraversal> agentOffMeshLinkTraversalLookup; + public float dt; + + public void Execute (Entity entity, in LocalTransform transform, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, in AgentIndex agentIndex, in RVOAgent controller, in MovementControl target) { + var scale = math.abs(transform.Scale); + var index = agentIndex.Index; + + if (agentData.version[index].Version != agentIndex.Version) throw new System.InvalidOperationException("RVOAgent has an invalid entity index"); + + // Actual infinity is not handled well by some algorithms, but very large values are ok. + // This should be larger than any reasonable value a user might want to use. + const float VERY_LARGE = 100000; + + // Copy all fields to the rvo simulator, and clamp them to reasonable values + agentData.radius[index] = math.clamp(shape.radius * scale, 0.001f, VERY_LARGE); + agentData.agentTimeHorizon[index] = math.clamp(controller.agentTimeHorizon, 0, VERY_LARGE); + agentData.obstacleTimeHorizon[index] = math.clamp(controller.obstacleTimeHorizon, 0, VERY_LARGE); + agentData.locked[index] = controller.locked; + agentData.maxNeighbours[index] = math.max(controller.maxNeighbours, 0); + agentData.debugFlags[index] = controller.debug; + agentData.layer[index] = controller.layer; + agentData.collidesWith[index] = controller.collidesWith; + agentData.targetPoint[index] = target.targetPoint; + agentData.desiredSpeed[index] = math.clamp(target.speed, 0, VERY_LARGE); + agentData.maxSpeed[index] = math.clamp(target.maxSpeed, 0, VERY_LARGE); + agentData.manuallyControlled[index] = target.overrideLocalAvoidance; + agentData.endOfPath[index] = target.endOfPath; + agentData.hierarchicalNodeIndex[index] = target.hierarchicalNodeIndex; + // control.endOfPath // TODO + agentData.movementPlane[index] = movementPlane.value; + + // Use the position from the movement script if one is attached + // as the movement script's position may not be the same as the transform's position + // (in particular if IAstarAI.updatePosition is false). + var pos = movementPlane.value.ToPlane(transform.Position, out float elevation); + var center = 0.5f * shape.height; + if (movementPlaneMode == MovementPlane.XY) { + // In 2D it is assumed the Z coordinate differences of agents is ignored. + agentData.height[index] = 1; + agentData.position[index] = movementPlane.value.ToWorld(pos, 0); + } else { + agentData.height[index] = math.clamp(shape.height * scale, 0, VERY_LARGE); + agentData.position[index] = movementPlane.value.ToWorld(pos, elevation + (center - 0.5f * shape.height) * scale); + } + + + // TODO: Move this to a separate file + var reached = agentOutputData.effectivelyReachedDestination[index]; + var prio = math.clamp(controller.priority * controller.priorityMultiplier, 0, VERY_LARGE); + var flow = math.clamp(controller.flowFollowingStrength, 0, 1); + if (reached == ReachedEndOfPath.Reached) { + flow = math.lerp(agentData.flowFollowingStrength[index], 1.0f, 6.0f * dt); + prio *= 0.3f; + } else if (reached == ReachedEndOfPath.ReachedSoon) { + flow = math.lerp(agentData.flowFollowingStrength[index], 1.0f, 6.0f * dt); + prio *= 0.45f; + } + agentData.priority[index] = prio; + agentData.flowFollowingStrength[index] = flow; + + if (agentOffMeshLinkTraversalLookup.HasComponent(entity)) { + // Agents traversing off-mesh links should not avoid other agents, + // but other agents may still avoid them. + agentData.manuallyControlled[index] = true; + } + } + } + + [BurstCompile] + public partial struct JobCopyFromRVOSimulatorToEntities : IJobEntity { + [ReadOnly] + public SimulatorBurst.AgentData agentData; + [ReadOnly] + public RVOQuadtreeBurst quadtree; + [ReadOnly] + public SimulatorBurst.AgentOutputData agentOutputData; + + /// <summary>See https://en.wikipedia.org/wiki/Circle_packing</summary> + const float MaximumCirclePackingDensity = 0.9069f; + + public void Execute (in LocalTransform transform, in AgentCylinderShape shape, in AgentIndex agentIndex, in RVOAgent controller, in MovementControl control, ref ResolvedMovement resolved) { + var index = agentIndex.Index; + + if (agentData.version[index].Version != agentIndex.Version) return; + + var scale = math.abs(transform.Scale); + var r = shape.radius * scale * 3f; + var area = quadtree.QueryArea(transform.Position, r); + var density = area / (MaximumCirclePackingDensity * math.PI * r * r); + + + resolved.targetPoint = agentOutputData.targetPoint[index]; + resolved.speed = agentOutputData.speed[index]; + var rnd = 1.0f; // (agentIndex.Index % 1024) / 1024f; + resolved.turningRadiusMultiplier = math.max(1f, math.pow(density * 2.0f, 4.0f) * rnd); + + // Pure copy + resolved.targetRotation = control.targetRotation; + resolved.targetRotationHint = control.targetRotationHint; + resolved.targetRotationOffset = control.targetRotationOffset; + resolved.rotationSpeed = control.rotationSpeed; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs.meta new file mode 100644 index 0000000..a291705 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ab994574a30005439b0db78c01279f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs new file mode 100644 index 0000000..30c0205 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Entities; +using UnityEngine; + +namespace Pathfinding.ECS { + using Pathfinding; + + [UpdateBefore(typeof(FollowerControlSystem))] + [UpdateInGroup(typeof(AIMovementSystemGroup))] + [RequireMatchingQueriesForUpdate] + public partial struct SyncDestinationTransformSystem : ISystem { + public void OnCreate (ref SystemState state) {} + public void OnDestroy (ref SystemState state) {} + + public void OnUpdate (ref SystemState systemState) { + foreach (var(point, destinationSetter) in SystemAPI.Query<RefRW<DestinationPoint>, AIDestinationSetter>()) { + if (destinationSetter.target != null) { + point.ValueRW = new DestinationPoint { + destination = destinationSetter.target.position, + facingDirection = destinationSetter.useRotation ? destinationSetter.target.forward : Vector3.zero + }; + } + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs.meta new file mode 100644 index 0000000..274915b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6ec5674da0fa5043bc982e1a4afed11 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs new file mode 100644 index 0000000..0fcbf60 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs @@ -0,0 +1,91 @@ +#pragma warning disable CS0282 +#if MODULE_ENTITIES +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine.Profiling; +using Unity.Transforms; +using Unity.Burst; +using Unity.Jobs; +using Unity.Collections; +using UnityEngine.Jobs; + +namespace Pathfinding.ECS { + using Pathfinding; + using Pathfinding.Util; + + [UpdateBefore(typeof(TransformSystemGroup))] + [UpdateBefore(typeof(AIMovementSystemGroup))] + [UpdateInGroup(typeof(SimulationSystemGroup))] + public partial struct SyncTransformsToEntitiesSystem : ISystem { + public static readonly quaternion ZAxisForwardToYAxisForward = quaternion.Euler(math.PI / 2, 0, 0); + public static readonly quaternion YAxisForwardToZAxisForward = quaternion.Euler(-math.PI / 2, 0, 0); + + public void OnCreate (ref SystemState state) {} + public void OnDestroy (ref SystemState state) {} + + public void OnUpdate (ref SystemState systemState) { + int numComponents = BatchedEvents.GetComponents<FollowerEntity>(BatchedEvents.Event.None, out var transforms, out var components); + if (numComponents > 0) { + var entities = new NativeArray<Entity>(numComponents, Allocator.TempJob); + + for (int i = 0; i < numComponents; i++) entities[i] = components[i].entity; + + systemState.Dependency = new SyncTransformsToEntitiesJob { + entities = entities, + entityPositions = SystemAPI.GetComponentLookup<LocalTransform>(), + syncPositionWithTransform = SystemAPI.GetComponentLookup<SyncPositionWithTransform>(true), + syncRotationWithTransform = SystemAPI.GetComponentLookup<SyncRotationWithTransform>(true), + orientationYAxisForward = SystemAPI.GetComponentLookup<OrientationYAxisForward>(true), + movementState = SystemAPI.GetComponentLookup<MovementState>(true), + }.Schedule(transforms, systemState.Dependency); + } + } + + [BurstCompile] + struct SyncTransformsToEntitiesJob : IJobParallelForTransform { + [ReadOnly] + [DeallocateOnJobCompletion] + public NativeArray<Entity> entities; + + // Safety: All entities are unique + [NativeDisableParallelForRestriction] + public ComponentLookup<LocalTransform> entityPositions; + [ReadOnly] + public ComponentLookup<SyncPositionWithTransform> syncPositionWithTransform; + [ReadOnly] + public ComponentLookup<SyncRotationWithTransform> syncRotationWithTransform; + [ReadOnly] + public ComponentLookup<OrientationYAxisForward> orientationYAxisForward; + [ReadOnly] + public ComponentLookup<MovementState> movementState; + + public void Execute (int index, TransformAccess transform) { + var entity = entities[index]; + if (entityPositions.HasComponent(entity)) { +#if MODULE_ENTITIES_1_0_8_OR_NEWER + ref var tr = ref entityPositions.GetRefRW(entity).ValueRW; +#else + ref var tr = ref entityPositions.GetRefRW(entity, false).ValueRW; +#endif + + float3 offset = float3.zero; + if (movementState.TryGetComponent(entity, out var ms)) { + offset = ms.positionOffset; + } + + if (syncPositionWithTransform.HasComponent(entity)) tr.Position = (float3)transform.position - offset; + if (syncRotationWithTransform.HasComponent(entity)) { + if (orientationYAxisForward.HasComponent(entity)) { + tr.Rotation = math.mul(transform.rotation, YAxisForwardToZAxisForward); + } else { + // Z axis forward + tr.Rotation = transform.rotation; + } + } + tr.Scale = transform.localScale.y; + } + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs.meta new file mode 100644 index 0000000..70b3391 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea4380d40e1bfa745a1ddb6638ed46b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry.meta new file mode 100644 index 0000000..39515fa --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b7af884023dec8a49b7bdaf160ea6110 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/Int3.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/Int3.cs new file mode 100644 index 0000000..62acae8 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/Int3.cs @@ -0,0 +1,358 @@ +using UnityEngine; +using Unity.Mathematics; +using System.Runtime.CompilerServices; + +namespace Pathfinding { + /// <summary>Holds a coordinate in integers</summary> + public struct Int3 : System.IEquatable<Int3> { + public int x; + public int y; + public int z; + + //These should be set to the same value (only PrecisionFactor should be 1 divided by Precision) + + /// <summary> + /// Precision for the integer coordinates. + /// One world unit is divided into [value] pieces. A value of 1000 would mean millimeter precision, a value of 1 would mean meter precision (assuming 1 world unit = 1 meter). + /// This value affects the maximum coordinates for nodes as well as how large the cost values are for moving between two nodes. + /// A higher value means that you also have to set all penalty values to a higher value to compensate since the normal cost of moving will be higher. + /// </summary> + public const int Precision = 1000; + + /// <summary><see cref="Precision"/> as a float</summary> + public const float FloatPrecision = 1000F; + + /// <summary>1 divided by <see cref="Precision"/></summary> + public const float PrecisionFactor = 0.001F; + + public static Int3 zero => new Int3(); + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public Int3 (Vector3 position) { + x = (int)System.Math.Round(position.x*FloatPrecision); + y = (int)System.Math.Round(position.y*FloatPrecision); + z = (int)System.Math.Round(position.z*FloatPrecision); + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public Int3 (int _x, int _y, int _z) { + x = _x; + y = _y; + z = _z; + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static bool operator == (Int3 lhs, Int3 rhs) { + return lhs.x == rhs.x && + lhs.y == rhs.y && + lhs.z == rhs.z; + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static bool operator != (Int3 lhs, Int3 rhs) { + return lhs.x != rhs.x || + lhs.y != rhs.y || + lhs.z != rhs.z; + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static explicit operator Int3 (Vector3 ob) { + return new Int3( + (int)System.Math.Round(ob.x*FloatPrecision), + (int)System.Math.Round(ob.y*FloatPrecision), + (int)System.Math.Round(ob.z*FloatPrecision) + ); + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static explicit operator Vector3 (Int3 ob) { + return new Vector3(ob.x*PrecisionFactor, ob.y*PrecisionFactor, ob.z*PrecisionFactor); + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static explicit operator float3 (Int3 ob) { + return (float3)(int3)ob*PrecisionFactor; + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static explicit operator int3 (Int3 ob) { + return new int3(ob.x, ob.y, ob.z); + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static Int3 operator - (Int3 lhs, Int3 rhs) { + lhs.x -= rhs.x; + lhs.y -= rhs.y; + lhs.z -= rhs.z; + return lhs; + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static Int3 operator - (Int3 lhs) { + lhs.x = -lhs.x; + lhs.y = -lhs.y; + lhs.z = -lhs.z; + return lhs; + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static Int3 operator + (Int3 lhs, Int3 rhs) { + lhs.x += rhs.x; + lhs.y += rhs.y; + lhs.z += rhs.z; + return lhs; + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static Int3 operator * (Int3 lhs, int rhs) { + lhs.x *= rhs; + lhs.y *= rhs; + lhs.z *= rhs; + + return lhs; + } + + public static Int3 operator * (Int3 lhs, float rhs) { + lhs.x = (int)System.Math.Round(lhs.x * rhs); + lhs.y = (int)System.Math.Round(lhs.y * rhs); + lhs.z = (int)System.Math.Round(lhs.z * rhs); + + return lhs; + } + + public static Int3 operator * (Int3 lhs, double rhs) { + lhs.x = (int)System.Math.Round(lhs.x * rhs); + lhs.y = (int)System.Math.Round(lhs.y * rhs); + lhs.z = (int)System.Math.Round(lhs.z * rhs); + + return lhs; + } + + public static Int3 operator / (Int3 lhs, float rhs) { + lhs.x = (int)System.Math.Round(lhs.x / rhs); + lhs.y = (int)System.Math.Round(lhs.y / rhs); + lhs.z = (int)System.Math.Round(lhs.z / rhs); + return lhs; + } + + public int this[int i] { + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + get { + return i == 0 ? x : (i == 1 ? y : z); + } + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + set { + if (i == 0) x = value; + else if (i == 1) y = value; + else z = value; + } + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static Int3 Max (Int3 lhs, Int3 rhs) { + return new Int3(System.Math.Max(lhs.x, rhs.x), System.Math.Max(lhs.y, rhs.y), System.Math.Max(lhs.z, rhs.z)); + } + + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + public static Int3 Min (Int3 lhs, Int3 rhs) { + return new Int3(System.Math.Min(lhs.x, rhs.x), System.Math.Min(lhs.y, rhs.y), System.Math.Min(lhs.z, rhs.z)); + } + + /// <summary>Angle between the vectors in radians</summary> + public static float Angle (Int3 lhs, Int3 rhs) { + double cos = Dot(lhs, rhs)/ ((double)lhs.magnitude*(double)rhs.magnitude); + + cos = cos < -1 ? -1 : (cos > 1 ? 1 : cos); + return (float)System.Math.Acos(cos); + } + + public static int Dot (Int3 lhs, Int3 rhs) { + return + lhs.x * rhs.x + + lhs.y * rhs.y + + lhs.z * rhs.z; + } + + public static long DotLong (Int3 lhs, Int3 rhs) { + return + (long)lhs.x * (long)rhs.x + + (long)lhs.y * (long)rhs.y + + (long)lhs.z * (long)rhs.z; + } + + /// <summary> + /// Normal in 2D space (XZ). + /// Equivalent to Cross(this, Int3(0,1,0) ) + /// except that the Y coordinate is left unchanged with this operation. + /// </summary> + public Int3 Normal2D () { + return new Int3(z, y, -x); + } + + /// <summary> + /// Returns the magnitude of the vector. The magnitude is the 'length' of the vector from 0,0,0 to this point. Can be used for distance calculations: + /// <code> Debug.Log ("Distance between 3,4,5 and 6,7,8 is: "+(new Int3(3,4,5) - new Int3(6,7,8)).magnitude); </code> + /// </summary> + public float magnitude { + get { + //It turns out that using doubles is just as fast as using ints with Mathf.Sqrt. And this can also handle larger numbers (possibly with small errors when using huge numbers)! + + double _x = x; + double _y = y; + double _z = z; + + return (float)System.Math.Sqrt(_x*_x+_y*_y+_z*_z); + } + } + + /// <summary> + /// Magnitude used for the cost between two nodes. The default cost between two nodes can be calculated like this: + /// <code> int cost = (node1.position-node2.position).costMagnitude; </code> + /// + /// This is simply the magnitude, rounded to the nearest integer + /// </summary> + public int costMagnitude { + get { + return (int)System.Math.Round(magnitude); + } + } + + /// <summary>The squared magnitude of the vector</summary> + public float sqrMagnitude { + get { + double _x = x; + double _y = y; + double _z = z; + return (float)(_x*_x+_y*_y+_z*_z); + } + } + + /// <summary>The squared magnitude of the vector</summary> + public long sqrMagnitudeLong { + get { + long _x = x; + long _y = y; + long _z = z; + return (_x*_x+_y*_y+_z*_z); + } + } + + public static implicit operator string (Int3 obj) { + return obj.ToString(); + } + + /// <summary>Returns a nicely formatted string representing the vector</summary> + public override string ToString () { + return "( "+x+", "+y+", "+z+")"; + } + + public override bool Equals (System.Object obj) { + if (!(obj is Int3)) return false; + + var rhs = (Int3)obj; + + return x == rhs.x && + y == rhs.y && + z == rhs.z; + } + + #region IEquatable implementation + + public bool Equals (Int3 other) { + return x == other.x && y == other.y && z == other.z; + } + + #endregion + + public override int GetHashCode () { + return x*73856093 ^ y*19349669 ^ z*83492791; + } + } + + /// <summary>Two Dimensional Integer Coordinate Pair</summary> + public struct Int2 : System.IEquatable<Int2> { + public int x; + public int y; + + public Int2 (int x, int y) { + this.x = x; + this.y = y; + } + + public long sqrMagnitudeLong { + get { + return (long)x*(long)x+(long)y*(long)y; + } + } + + public static explicit operator int2 (Int2 a) { + return new int2(a.x, a.y); + } + + public static Int2 operator - (Int2 lhs) { + lhs.x = -lhs.x; + lhs.y = -lhs.y; + return lhs; + } + + public static Int2 operator + (Int2 a, Int2 b) { + return new Int2(a.x+b.x, a.y+b.y); + } + + public static Int2 operator - (Int2 a, Int2 b) { + return new Int2(a.x-b.x, a.y-b.y); + } + + public static bool operator == (Int2 a, Int2 b) { + return a.x == b.x && a.y == b.y; + } + + public static bool operator != (Int2 a, Int2 b) { + return a.x != b.x || a.y != b.y; + } + + /// <summary>Dot product of the two coordinates</summary> + public static long DotLong (Int2 a, Int2 b) { + return (long)a.x*(long)b.x + (long)a.y*(long)b.y; + } + + public override bool Equals (System.Object o) { + if (!(o is Int2)) return false; + var rhs = (Int2)o; + + return x == rhs.x && y == rhs.y; + } + + #region IEquatable implementation + + public bool Equals (Int2 other) { + return x == other.x && y == other.y; + } + + #endregion + + public override int GetHashCode () { + return x*49157+y*98317; + } + + public static Int2 Min (Int2 a, Int2 b) { + return new Int2(System.Math.Min(a.x, b.x), System.Math.Min(a.y, b.y)); + } + + public static Int2 Max (Int2 a, Int2 b) { + return new Int2(System.Math.Max(a.x, b.x), System.Math.Max(a.y, b.y)); + } + + public static Int2 FromInt3XZ (Int3 o) { + return new Int2(o.x, o.z); + } + + public static Int3 ToInt3XZ (Int2 o) { + return new Int3(o.x, 0, o.y); + } + + public override string ToString () { + return "("+x+", " +y+")"; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/Int3.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/Int3.cs.meta new file mode 100644 index 0000000..99f9118 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/Int3.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5826dd4a1809b448291582cd06deadc1 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/IntBounds.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/IntBounds.cs new file mode 100644 index 0000000..008b16f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/IntBounds.cs @@ -0,0 +1,64 @@ +using Unity.Mathematics; + +namespace Pathfinding { + /// <summary> + /// Integer bounding box. + /// Works almost like UnityEngine.BoundsInt but with a slightly nicer and more efficient api. + /// + /// Uses an exclusive upper bound (max field). + /// </summary> + public struct IntBounds { + public int3 min, max; + + public IntBounds (int xmin, int ymin, int zmin, int xmax, int ymax, int zmax) { + min = new int3(xmin, ymin, zmin); + max = new int3(xmax, ymax, zmax); + } + + public IntBounds(int3 min, int3 max) { + this.min = min; + this.max = max; + } + + public int3 size => max - min; + public int volume { + get { + var s = size; + return s.x * s.y * s.z; + } + } + + /// <summary> + /// Returns the intersection bounding box between the two bounds. + /// The intersection bounds is the volume which is inside both bounds. + /// If the rects do not have an intersection, an invalid rect is returned. + /// See: IsValid + /// </summary> + public static IntBounds Intersection (IntBounds a, IntBounds b) { + return new IntBounds( + math.max(a.min, b.min), + math.min(a.max, b.max) + ); + } + + public IntBounds Offset (int3 offset) { + return new IntBounds(min + offset, max + offset); + } + + public bool Contains (IntBounds other) { + return math.all(other.min >= min & other.max <= max); + } + + public override string ToString() => "(" + min.ToString() + " <= x < " + max.ToString() + ")"; + public override bool Equals (object _b) { + var b = (IntBounds)_b; + return this == b; + } + + public override int GetHashCode() => min.GetHashCode() ^ (max.GetHashCode() << 2); + + public static bool operator ==(IntBounds a, IntBounds b) => math.all(a.min == b.min & a.max == b.max); + + public static bool operator !=(IntBounds a, IntBounds b) => !(a == b); + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/IntBounds.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/IntBounds.cs.meta new file mode 100644 index 0000000..476fc03 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Geometry/IntBounds.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fd3215aff835b0e49925b61562b0a83e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateScene.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateScene.cs new file mode 100644 index 0000000..1e9d131 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateScene.cs @@ -0,0 +1,390 @@ +using UnityEngine; + +namespace Pathfinding { + using Pathfinding.Drawing; + + [AddComponentMenu("Pathfinding/GraphUpdateScene")] + /// <summary> + /// Helper class for easily updating graphs. + /// + /// To use the GraphUpdateScene component, create a new empty GameObject and add the component to it, it can be found under Components-->Pathfinding-->GraphUpdateScene. + /// + /// The region which the component will affect is defined by creating a polygon in the scene. + /// If you make sure you have the Position tool enabled (top-left corner of the Unity window) you can shift+click in the scene view to add more points to the polygon. + /// You can remove points using shift+alt+click. + /// By clicking on the points you can bring up a positioning tool. You can also open the "points" array in the inspector to set each point's coordinates manually. + /// [Open online documentation to see images] + /// In the inspector there are a number of variables. The first one is named "Convex", it sets if the convex hull of the points should be calculated or if the polygon should be used as-is. + /// Using the convex hull is faster when applying the changes to the graph, but with a non-convex polygon you can specify more complicated areas. + /// The next two variables, called "Apply On Start" and "Apply On Scan" determine when to apply the changes. If the object is in the scene from the beginning, both can be left on, it doesn't + /// matter since the graph is also scanned at start. However if you instantiate it later in the game, you can make it apply it's setting directly, or wait until the next scan (if any). + /// If the graph is rescanned, all GraphUpdateScene components which have the Apply On Scan variable toggled will apply their settings again to the graph since rescanning clears all previous changes. + /// You can also make it apply it's changes using scripting. + /// <code> GetComponent<GraphUpdateScene>().Apply (); </code> + /// The above code will make it apply its changes to the graph (assuming a GraphUpdateScene component is attached to the same GameObject). + /// + /// Next there is "Modify Walkability" and "Set Walkability" (which appears when "Modify Walkability" is toggled). + /// If Modify Walkability is set, then all nodes inside the area will either be set to walkable or unwalkable depending on the value of the "Set Walkability" variable. + /// + /// Penalty can also be applied to the nodes. A higher penalty (aka weight) makes the nodes harder to traverse so it will try to avoid those areas. + /// + /// The tagging variables can be read more about on this page: tags (view in online documentation for working links) "Working with tags". + /// + /// Note: The Y (up) axis of the transform that this component is attached to should be in the same direction as the up direction of the graph. + /// So if you for example have a grid in the XY plane then the transform should have the rotation (-90,0,0). + /// </summary> + [HelpURL("https://arongranberg.com/astar/documentation/stable/graphupdatescene.html")] + public class GraphUpdateScene : GraphModifier { + /// <summary>Points which define the region to update</summary> + public Vector3[] points; + + /// <summary>Private cached convex hull of the <see cref="points"/></summary> + private Vector3[] convexPoints; + + /// <summary> + /// Use the convex hull of the points instead of the original polygon. + /// + /// See: https://en.wikipedia.org/wiki/Convex_hull + /// </summary> + public bool convex = true; + + /// <summary> + /// Minumum height of the bounds of the resulting Graph Update Object. + /// Useful when all points are laid out on a plane but you still need a bounds with a height greater than zero since a + /// zero height graph update object would usually result in no nodes being updated. + /// </summary> + public float minBoundsHeight = 1; + + /// <summary> + /// Penalty to add to nodes. + /// Usually you need quite large values, at least 1000-10000. A higher penalty means that agents will try to avoid those nodes more. + /// + /// Be careful when setting negative values since if a node gets a negative penalty it will underflow and instead get + /// really large. In most cases a warning will be logged if that happens. + /// + /// See: tags (view in online documentation for working links) for another way of applying penalties. + /// </summary> + public int penaltyDelta; + + /// <summary>If true, then all affected nodes will be made walkable or unwalkable according to <see cref="setWalkability"/></summary> + public bool modifyWalkability; + + /// <summary>Nodes will be made walkable or unwalkable according to this value if <see cref="modifyWalkability"/> is true</summary> + public bool setWalkability; + + /// <summary>Apply this graph update object on start</summary> + public bool applyOnStart = true; + + /// <summary>Apply this graph update object whenever a graph is rescanned</summary> + public bool applyOnScan = true; + + /// <summary> + /// Update node's walkability and connectivity using physics functions. + /// For grid graphs, this will update the node's position and walkability exactly like when doing a scan of the graph. + /// If enabled for grid graphs, <see cref="modifyWalkability"/> will be ignored. + /// + /// For Point Graphs, this will recalculate all connections which passes through the bounds of the resulting Graph Update Object + /// using raycasts (if enabled). + /// </summary> + public bool updatePhysics; + + /// <summary>\copydoc Pathfinding::GraphUpdateObject::resetPenaltyOnPhysics</summary> + public bool resetPenaltyOnPhysics = true; + + /// <summary>\copydoc Pathfinding::GraphUpdateObject::updateErosion</summary> + public bool updateErosion = true; + + /// <summary> + /// Should the tags of the nodes be modified. + /// If enabled, set all nodes' tags to <see cref="setTag"/> + /// </summary> + public bool modifyTag; + + /// <summary>If <see cref="modifyTag"/> is enabled, set all nodes' tags to this value</summary> + public PathfindingTag setTag; + + /// <summary>Emulates behavior from before version 4.0</summary> + [HideInInspector] + public bool legacyMode = false; + + /// <summary> + /// Private cached inversion of <see cref="setTag"/>. + /// Used for InvertSettings() + /// </summary> + private PathfindingTag setTagInvert; + + /// <summary> + /// Has apply been called yet. + /// Used to prevent applying twice when both applyOnScan and applyOnStart are enabled + /// </summary> + private bool firstApplied; + + /// <summary> + /// Use world space for coordinates. + /// If true, the shape will not follow when moving around the transform. + /// </summary> + [SerializeField] + [UnityEngine.Serialization.FormerlySerializedAs("useWorldSpace")] + private bool legacyUseWorldSpace; + + [SerializeField] + [UnityEngine.Serialization.FormerlySerializedAs("setTag")] + private int setTagCompatibility = -1; + + /// <summary>Do some stuff at start</summary> + public void Start () { + if (!Application.isPlaying) return; + + // If firstApplied is true, that means the graph was scanned during Awake. + // So we shouldn't apply it again because then we would end up applying it two times + if (!firstApplied && applyOnStart) { + Apply(); + } + } + + public override void OnPostScan () { + if (applyOnScan) Apply(); + } + + /// <summary> + /// Inverts all invertable settings for this GUS. + /// Namely: penalty delta, walkability, tags. + /// + /// Penalty delta will be changed to negative penalty delta. + /// <see cref="setWalkability"/> will be inverted. + /// <see cref="setTag"/> will be stored in a private variable, and the new value will be 0. When calling this function again, the saved + /// value will be the new value. + /// + /// Calling this function an even number of times without changing any settings in between will be identical to no change in settings. + /// </summary> + public virtual void InvertSettings () { + setWalkability = !setWalkability; + penaltyDelta = -penaltyDelta; + if (setTagInvert == 0) { + setTagInvert = setTag; + setTag = 0; + } else { + setTag = setTagInvert; + setTagInvert = 0; + } + } + + /// <summary> + /// Recalculate convex hull. + /// Will not do anything if <see cref="convex"/> is disabled. + /// </summary> + public void RecalcConvex () { + convexPoints = convex ? Polygon.ConvexHullXZ(points) : null; + } + + /// <summary> + /// Calculates the bounds for this component. + /// This is a relatively expensive operation, it needs to go through all points and + /// run matrix multiplications. + /// </summary> + public Bounds GetBounds () { + if (points == null || points.Length == 0) { + Bounds bounds; + var coll = GetComponent<Collider>(); + var coll2D = GetComponent<Collider2D>(); + var rend = GetComponent<Renderer>(); + + if (coll != null) bounds = coll.bounds; + else if (coll2D != null) { + bounds = coll2D.bounds; + bounds.size = new Vector3(bounds.size.x, bounds.size.y, Mathf.Max(bounds.size.z, 1f)); + } else if (rend != null) { + bounds = rend.bounds; + } else { + return new Bounds(Vector3.zero, Vector3.zero); + } + + if (legacyMode && bounds.size.y < minBoundsHeight) bounds.size = new Vector3(bounds.size.x, minBoundsHeight, bounds.size.z); + return bounds; + } else { + if (convexPoints == null) RecalcConvex(); + return GraphUpdateShape.GetBounds(convex ? convexPoints : points, legacyMode && legacyUseWorldSpace ? Matrix4x4.identity : transform.localToWorldMatrix, minBoundsHeight); + } + } + + /// <summary> + /// The GraphUpdateObject which would be applied by this component. + /// + /// No graphs are actually updated by this function. Call AstarPath.active.UpdateGraphs and pass this object if you want that. + /// This method is useful if you want to modify the object before passing it to the UpdateGraphs function. + /// + /// See: <see cref="Apply"/> + /// </summary> + public virtual GraphUpdateObject GetGraphUpdate () { + GraphUpdateObject guo; + + if (points == null || points.Length == 0) { + var polygonCollider = GetComponent<PolygonCollider2D>(); + if (polygonCollider != null) { + var points2D = polygonCollider.points; + Vector3[] pts = new Vector3[points2D.Length]; + for (int i = 0; i < pts.Length; i++) { + var p = points2D[i] + polygonCollider.offset; + pts[i] = new Vector3(p.x, 0, p.y); + } + + var mat = transform.localToWorldMatrix * Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(-90, 0, 0), Vector3.one); + var shape = new GraphUpdateShape(pts, convex, mat, minBoundsHeight); + guo = new GraphUpdateObject(GetBounds()); + guo.shape = shape; + } else { + var bounds = GetBounds(); + if (bounds.center == Vector3.zero && bounds.size == Vector3.zero) { + Debug.LogError("Cannot apply GraphUpdateScene, no points defined and no renderer or collider attached", this); + return null; + } else if (bounds.size == Vector3.zero) { + Debug.LogWarning("Collider bounding box was empty. Are you trying to apply the GraphUpdateScene before the collider has been enabled or initialized?", this); + // Note: This is technically valid, so we don't return null here + } + + guo = new GraphUpdateObject(bounds); + } + } else { + GraphUpdateShape shape; + if (legacyMode && !legacyUseWorldSpace) { + // Used for compatibility with older versions + var worldPoints = new Vector3[points.Length]; + for (int i = 0; i < points.Length; i++) worldPoints[i] = transform.TransformPoint(points[i]); + shape = new GraphUpdateShape(worldPoints, convex, Matrix4x4.identity, minBoundsHeight); + } else { + shape = new GraphUpdateShape(points, convex, legacyMode && legacyUseWorldSpace ? Matrix4x4.identity : transform.localToWorldMatrix, minBoundsHeight); + } + var bounds = shape.GetBounds(); + guo = new GraphUpdateObject(bounds); + guo.shape = shape; + } + + firstApplied = true; + + guo.modifyWalkability = modifyWalkability; + guo.setWalkability = setWalkability; + guo.addPenalty = penaltyDelta; + guo.updatePhysics = updatePhysics; + guo.updateErosion = updateErosion; + guo.resetPenaltyOnPhysics = resetPenaltyOnPhysics; + + guo.modifyTag = modifyTag; + guo.setTag = setTag; + return guo; + } + + /// <summary> + /// Updates graphs with a created GUO. + /// Creates a Pathfinding.GraphUpdateObject with a Pathfinding.GraphUpdateShape + /// representing the polygon of this object and update all graphs using AstarPath.UpdateGraphs. + /// This will not update graphs immediately. See AstarPath.UpdateGraph for more info. + /// </summary> + public void Apply () { + if (AstarPath.active == null) { + Debug.LogError("There is no AstarPath object in the scene", this); + return; + } + + var guo = GetGraphUpdate(); + if (guo != null) AstarPath.active.UpdateGraphs(guo); + } + + static readonly Color GizmoColorSelected = new Color(227/255f, 61/255f, 22/255f, 1.0f); + static readonly Color GizmoColorUnselected = new Color(227/255f, 61/255f, 22/255f, 0.9f); + + /// <summary>Draws some gizmos</summary> + public override void DrawGizmos () { + bool selected = GizmoContext.InActiveSelection(this); + Color c = selected ? GizmoColorSelected : GizmoColorUnselected; + + if (selected) { + var col = Color.Lerp(c, new Color(1, 1, 1, 0.2f), 0.9f); + + Bounds b = GetBounds(); + Draw.SolidBox(b.center, b.size, col); + Draw.WireBox(b.center, b.size, col); + } + + if (points == null) return; + + if (convex) c.a *= 0.5f; + + Matrix4x4 matrix = legacyMode && legacyUseWorldSpace ? Matrix4x4.identity : transform.localToWorldMatrix; + + if (convex) { + c.r -= 0.1f; + c.g -= 0.2f; + c.b -= 0.1f; + } + + using (Draw.WithMatrix(matrix)) { + if (selected || !convex) { + var fadedColor = c; + fadedColor.a *= 0.7f; + Draw.Polyline(points, true, convex ? fadedColor : c); + } + + if (convex) { + if (convexPoints == null) RecalcConvex(); + Draw.Polyline(convexPoints, true, selected ? GizmoColorSelected : GizmoColorUnselected); + } + + // Draw the full 3D shape + var pts = convex ? convexPoints : points; + if (selected && pts != null && pts.Length > 0) { + float miny = pts[0].y, maxy = pts[0].y; + for (int i = 0; i < pts.Length; i++) { + miny = Mathf.Min(miny, pts[i].y); + maxy = Mathf.Max(maxy, pts[i].y); + } + var extraHeight = Mathf.Max(minBoundsHeight - (maxy - miny), 0) * 0.5f; + miny -= extraHeight; + maxy += extraHeight; + + using (Draw.WithColor(new Color(1, 1, 1, 0.2f))) { + for (int i = 0; i < pts.Length; i++) { + var next = (i+1) % pts.Length; + var p1 = pts[i] + Vector3.up*(miny - pts[i].y); + var p2 = pts[i] + Vector3.up*(maxy - pts[i].y); + var p1n = pts[next] + Vector3.up*(miny - pts[next].y); + var p2n = pts[next] + Vector3.up*(maxy - pts[next].y); + Draw.Line(p1, p2); + Draw.Line(p1, p1n); + Draw.Line(p2, p2n); + } + } + } + } + } + + /// <summary> + /// Disables legacy mode if it is enabled. + /// Legacy mode is automatically enabled for components when upgrading from an earlier version than 3.8.6. + /// </summary> + public void DisableLegacyMode () { + if (legacyMode) { + legacyMode = false; + if (legacyUseWorldSpace) { + legacyUseWorldSpace = false; + for (int i = 0; i < points.Length; i++) { + points[i] = transform.InverseTransformPoint(points[i]); + } + RecalcConvex(); + } + } + } + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + if (migrations.TryMigrateFromLegacyFormat(out var legacyVersion)) { + if (legacyVersion == 0) { + // Use the old behavior if some points are already set + if (points != null && points.Length > 0) legacyMode = true; + } + if (setTagCompatibility != -1) { + setTag = (uint)setTagCompatibility; + setTagCompatibility = -1; + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateScene.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateScene.cs.meta new file mode 100644 index 0000000..fae937e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateScene.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: efee954c69f0d421086729bb8df1137f +timeCreated: 1490044676 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: -221 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateShape.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateShape.cs new file mode 100644 index 0000000..ed3f13d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateShape.cs @@ -0,0 +1,198 @@ +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace Pathfinding { + /// <summary> + /// Defines a shape for a Pathfinding.GraphUpdateObject. + /// The shape consists of a number of points which it can either calculate the convex hull of or use as a polygon directly. + /// + /// A shape is essentially a 2D shape however it can be rotated arbitrarily. + /// When a matrix and a list of points is specified in the constructor the matrix decides what direction + /// is the 'up' direction. When checking if a point is contained in the shape, the point will be projected down + /// on a plane where the 'up' direction is the normal and then it will check if the shape contains the point. + /// + /// See: Pathfinding.GraphUpdateObject.shape + /// </summary> + public class GraphUpdateShape { + Vector3[] _points; + Vector3[] _convexPoints; + bool _convex; + Vector3 right = Vector3.right; + Vector3 forward = Vector3.forward; + Vector3 up = Vector3.up; + Vector3 origin; + public float minimumHeight; + + /// <summary>Shape optimized for burst</summary> + public struct BurstShape { + [DeallocateOnJobCompletion] + NativeArray<Vector3> points; + float3 origin, right, forward; + bool containsEverything; + + public BurstShape(GraphUpdateShape scene, Allocator allocator) { + var pts = scene.convex ? scene._convexPoints : scene._points; + + if (pts == null) points = new NativeArray<Vector3>(0, allocator); + else points = new NativeArray<Vector3>(pts, allocator); + + origin = scene.origin; + right = scene.right; + forward = scene.forward; + + // Speeds up calculations below + var mgn = scene.right.sqrMagnitude; + if (mgn > 0) right /= mgn; + mgn = scene.forward.sqrMagnitude; + if (mgn > 0) forward /= mgn; + containsEverything = false; + } + + /// <summary>Shape that contains everything</summary> + public static BurstShape Everything => new BurstShape { + points = new NativeArray<Vector3>(0, Allocator.Persistent), + origin = float3.zero, + right = float3.zero, + forward = float3.zero, + containsEverything = true, + }; + + public bool Contains (float3 point) { + if (containsEverything) return true; + // Transform to local space (shape in the XZ plane) + point -= origin; + // Point in local space + var p = new float3(math.dot(point, right), 0, math.dot(point, forward)); + + int j = points.Length-1; + bool inside = false; + + for (int i = 0; i < points.Length; j = i++) { + if (((points[i].z <= p.z && p.z < points[j].z) || (points[j].z <= p.z && p.z < points[i].z)) && + (p.x < (points[j].x - points[i].x) * (p.z - points[i].z) / (points[j].z - points[i].z) + points[i].x)) + inside = !inside; + } + return inside; + } + } + + /// <summary> + /// Gets or sets the points of the polygon in the shape. + /// These points should be specified in clockwise order. + /// Will automatically calculate the convex hull if <see cref="convex"/> is set to true + /// </summary> + public Vector3[] points { + get { + return _points; + } + set { + _points = value; + if (convex) CalculateConvexHull(); + } + } + + /// <summary> + /// Sets if the convex hull of the points should be calculated. + /// Convex hulls are faster but non-convex hulls can be used to specify more complicated shapes. + /// </summary> + public bool convex { + get { + return _convex; + } + set { + if (_convex != value && value) { + CalculateConvexHull(); + } + _convex = value; + } + } + + public GraphUpdateShape () { + } + + /// <summary> + /// Construct a shape. + /// See: <see cref="convex"/> + /// </summary> + /// <param name="points">Contour of the shape in local space with respect to the matrix (i.e the shape should be in the XZ plane, the Y coordinate will only affect the bounds)</param> + /// <param name="convex">If true, the convex hull of the points will be calculated.</param> + /// <param name="matrix">local to world space matrix for the points. The matrix determines the up direction of the shape.</param> + /// <param name="minimumHeight">If the points would be in the XZ plane only, the shape would not have a height and then it might not + /// include any points inside it (as testing for inclusion is done in 3D space when updating graphs). This ensures + /// that the shape has at least the minimum height (in the up direction that the matrix specifies).</param> + public GraphUpdateShape (Vector3[] points, bool convex, Matrix4x4 matrix, float minimumHeight) { + this.convex = convex; + this.points = points; + origin = matrix.MultiplyPoint3x4(Vector3.zero); + right = matrix.MultiplyPoint3x4(Vector3.right) - origin; + up = matrix.MultiplyPoint3x4(Vector3.up) - origin; + forward = matrix.MultiplyPoint3x4(Vector3.forward) - origin; + this.minimumHeight = minimumHeight; + } + + void CalculateConvexHull () { + _convexPoints = points != null? Polygon.ConvexHullXZ(points) : null; + } + + /// <summary>World space bounding box of this shape</summary> + public Bounds GetBounds () { + return GetBounds(convex ? _convexPoints : points, right, up, forward, origin, minimumHeight); + } + + public static Bounds GetBounds (Vector3[] points, Matrix4x4 matrix, float minimumHeight) { + var origin = matrix.MultiplyPoint3x4(Vector3.zero); + var right = matrix.MultiplyPoint3x4(Vector3.right) - origin; + var up = matrix.MultiplyPoint3x4(Vector3.up) - origin; + var forward = matrix.MultiplyPoint3x4(Vector3.forward) - origin; + + return GetBounds(points, right, up, forward, origin, minimumHeight); + } + + static Bounds GetBounds (Vector3[] points, Vector3 right, Vector3 up, Vector3 forward, Vector3 origin, float minimumHeight) { + if (points == null || points.Length == 0) return new Bounds(); + float miny = points[0].y, maxy = points[0].y; + for (int i = 0; i < points.Length; i++) { + miny = Mathf.Min(miny, points[i].y); + maxy = Mathf.Max(maxy, points[i].y); + } + var extraHeight = Mathf.Max(minimumHeight - (maxy - miny), 0) * 0.5f; + miny -= extraHeight; + maxy += extraHeight; + + Vector3 min = right * points[0].x + up * points[0].y + forward * points[0].z; + Vector3 max = min; + for (int i = 0; i < points.Length; i++) { + var p = right * points[i].x + forward * points[i].z; + var p1 = p + up * miny; + var p2 = p + up * maxy; + min = Vector3.Min(min, p1); + min = Vector3.Min(min, p2); + max = Vector3.Max(max, p1); + max = Vector3.Max(max, p2); + } + return new Bounds((min+max)*0.5F + origin, max-min); + } + + public bool Contains (GraphNode node) { + return Contains((Vector3)node.position); + } + + public bool Contains (Vector3 point) { + // Transform to local space (shape in the XZ plane) + point -= origin; + var localSpacePoint = new Vector3(Vector3.Dot(point, right)/right.sqrMagnitude, 0, Vector3.Dot(point, forward)/forward.sqrMagnitude); + + if (convex) { + if (_convexPoints == null) return false; + + for (int i = 0, j = _convexPoints.Length-1; i < _convexPoints.Length; j = i, i++) { + if (VectorMath.RightOrColinearXZ(_convexPoints[i], _convexPoints[j], localSpacePoint)) return false; + } + return true; + } else { + return _points != null && Polygon.ContainsPointXZ(_points, localSpacePoint); + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateShape.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateShape.cs.meta new file mode 100644 index 0000000..7df21dd --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/GraphUpdateShape.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1c31d3b0be14344e98aa458dc66c3a94 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc.meta new file mode 100644 index 0000000..77e0364 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dfc976d61106d46b6a18ace94ffaea8d 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: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes.meta new file mode 100644 index 0000000..76b5eb8 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b41391edefb794969b06b1e03993abf4 diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes/GraphNode.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes/GraphNode.cs new file mode 100644 index 0000000..51c6758 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes/GraphNode.cs @@ -0,0 +1,928 @@ +using UnityEngine; +using System.Collections.Generic; +using Pathfinding.Serialization; + +namespace Pathfinding { + using Pathfinding.Util; + + /// <summary>Represents a connection to another node</summary> + public struct Connection { + /// <summary>Node which this connection goes to</summary> + public GraphNode node; + + /// <summary> + /// Cost of moving along this connection. + /// A cost of 1000 corresponds approximately to the cost of moving one world unit. + /// </summary> + public uint cost; + + /// <summary> + /// Various metadata about the connection, such as the side of the node shape which this connection uses. + /// + /// - Bits 0..1 represent <see cref="shapeEdge"/>. + /// - Bits 2..3 represent <see cref="adjacentShapeEdge"/>. + /// - Bit 4 represents <see cref="isIncoming"/>. + /// - Bit 5 represents <see cref="isOutgoing"/>. + /// - Bit 6 represents <see cref="edgesAreIdentical"/>. + /// + /// Note: Due to alignment, the <see cref="node"/> and <see cref="cost"/> fields use 12 bytes which will be padded + /// to 16 bytes when used in an array even if this field would be removed. + /// So this field does not contribute to increased memory usage. We could even expand it to 32-bits if we need to in the future. + /// </summary> + public byte shapeEdgeInfo; + + /// <summary> + /// The edge of the shape which this connection uses. + /// This is an index into the shape's vertices. + /// + /// A value of 0 corresponds to using the side for vertex 0 and vertex 1 on the node. 1 corresponds to vertex 1 and 2, etc. + /// A value of 3 is invalid, and this will be the value if <see cref="isEdgeShared"/> is false. + /// + /// See: <see cref="TriangleMeshNode"/> + /// See: <see cref="MeshNode.AddPartialConnection"/> + /// </summary> + public int shapeEdge => shapeEdgeInfo & 0b11; + + /// <summary> + /// The edge of the shape in the other node, which this connection represents. + /// + /// See: <see cref="shapeEdge"/> + /// </summary> + public int adjacentShapeEdge => (shapeEdgeInfo >> 2) & 0b11; + + /// <summary> + /// True if the two nodes share an identical edge. + /// + /// This is only true if the connection is between two triangle mesh nodes and the nodes share the edge which this connection represents. + /// + /// In contrast to <see cref="isEdgeShared"/>, this is true only if the triangle edge is identical (but reversed) in the other node. + /// </summary> + public bool edgesAreIdentical => (shapeEdgeInfo & IdenticalEdge) != 0; + + /// <summary> + /// True if the two nodes share an edge. + /// + /// This is only true if the connection is between two triangle mesh nodes and the nodes share the edge which this connection represents. + /// Note that the edge does not need to be perfectly identical for this to be true, it is enough if the edge is very similar. + /// </summary> + public bool isEdgeShared => (shapeEdgeInfo & NoSharedEdge) != NoSharedEdge; + + /// <summary> + /// True if the connection allows movement from this node to the other node. + /// + /// A connection can be either outgoing, incoming, or both. Most connections are two-way, so both incoming and outgoing. + /// </summary> + public bool isOutgoing => (shapeEdgeInfo & OutgoingConnection) != 0; + + /// <summary> + /// True if the connection allows movement from the other node to this node. + /// + /// A connection can be either outgoing, incoming, or both. Most connections are two-way, so both incoming and outgoing. + /// </summary> + public bool isIncoming => (shapeEdgeInfo & IncomingConnection) != 0; + + + public const byte NoSharedEdge = 0b1111; + public const byte IncomingConnection = 1 << 4; + public const byte OutgoingConnection = 1 << 5; + public const byte IdenticalEdge = 1 << 6; + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public Connection (GraphNode node, uint cost, bool isOutgoing, bool isIncoming) { + this.node = node; + this.cost = cost; + this.shapeEdgeInfo = PackShapeEdgeInfo(isOutgoing, isIncoming); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static byte PackShapeEdgeInfo(bool isOutgoing, bool isIncoming) => (byte)(NoSharedEdge | (isIncoming ? IncomingConnection : 0) | (isOutgoing ? OutgoingConnection : 0)); + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static byte PackShapeEdgeInfo (byte shapeEdge, byte adjacentShapeEdge, bool areEdgesIdentical, bool isOutgoing, bool isIncoming) { +#if UNITY_EDITOR + if (shapeEdge > 3) throw new System.ArgumentException("shapeEdge must be at most 3"); + if (adjacentShapeEdge > 3) throw new System.ArgumentException("adjacentShapeEdge must be at most 3"); +#endif + return (byte)(shapeEdge | (adjacentShapeEdge << 2) | (areEdgesIdentical ? IdenticalEdge : 0) | (isOutgoing ? OutgoingConnection : 0) | (isIncoming ? IncomingConnection : 0)); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public Connection (GraphNode node, uint cost, byte shapeEdgeInfo) { + this.node = node; + this.cost = cost; + this.shapeEdgeInfo = shapeEdgeInfo; + } + + public override int GetHashCode () { + return node.GetHashCode() ^ (int)cost; + } + + public override bool Equals (object obj) { + if (!(obj is Connection)) return false; + var conn = (Connection)obj; + return conn.node == node && conn.cost == cost && conn.shapeEdgeInfo == shapeEdgeInfo; + } + } + + /// <summary>Base class for all nodes</summary> + public abstract class GraphNode { + /// <summary>Internal unique index. Also stores some bitpacked values such as <see cref="TemporaryFlag1"/> and <see cref="TemporaryFlag2"/>.</summary> + private int nodeIndex; + + /// <summary> + /// Bitpacked field holding several pieces of data. + /// See: Walkable + /// See: Area + /// See: GraphIndex + /// See: Tag + /// </summary> + protected uint flags; + +#if !ASTAR_NO_PENALTY + /// <summary> + /// Penalty cost for walking on this node. + /// This can be used to make it harder/slower to walk over certain nodes. + /// + /// A penalty of 1000 (Int3.Precision) corresponds to the cost of walking one world unit. + /// + /// See: graph-updates (view in online documentation for working links) + /// </summary> + private uint penalty; +#endif + + /// <summary> + /// Graph which this node belongs to. + /// + /// If you know the node belongs to a particular graph type, you can cast it to that type: + /// <code> + /// GraphNode node = ...; + /// GridGraph graph = node.Graph as GridGraph; + /// </code> + /// + /// Will return null if the node has been destroyed. + /// </summary> + public NavGraph Graph => AstarData.GetGraph(this); + + /// <summary> + /// Destroys the node. + /// Cleans up any temporary pathfinding data used for this node. + /// The graph is responsible for calling this method on nodes when they are destroyed, including when the whole graph is destoyed. + /// Otherwise memory leaks might present themselves. + /// + /// Once called the <see cref="Destroyed"/> property will return true and subsequent calls to this method will not do anything. + /// + /// Note: Assumes the current active AstarPath instance is the same one that created this node. + /// + /// Warning: Should only be called by graph classes on their own nodes + /// </summary> + public void Destroy () { + if (Destroyed) return; + + ClearConnections(true); + + if (AstarPath.active != null) { + AstarPath.active.DestroyNode(this); + } + NodeIndex = DestroyedNodeIndex; + } + + public bool Destroyed => NodeIndex == DestroyedNodeIndex; + + // If anyone creates more than about 200 million nodes then things will not go so well, however at that point one will certainly have more pressing problems, such as having run out of RAM + const uint NodeIndexMask = 0xFFFFFFF; + public const uint DestroyedNodeIndex = NodeIndexMask - 1; + public const int InvalidNodeIndex = 0; + const int TemporaryFlag1Mask = 0x10000000; + const int TemporaryFlag2Mask = 0x20000000; + + /// <summary> + /// Internal unique index. + /// Every node will get a unique index. + /// This index is not necessarily correlated with e.g the position of the node in the graph. + /// </summary> + public uint NodeIndex { get { return (uint)nodeIndex & NodeIndexMask; } internal set { nodeIndex = (int)(((uint)nodeIndex & ~NodeIndexMask) | value); } } + + /// <summary> + /// How many path node variants should be created for each node. + /// + /// This should be a constant for each node type. + /// + /// Typically this is 1, but for example the triangle mesh node type has 3 variants, one for each edge. + /// + /// See: <see cref="Pathfinding.PathNode"/> + /// </summary> + internal virtual int PathNodeVariants => 1; + + /// <summary> + /// Temporary flag for internal purposes. + /// May only be used in the Unity thread. Must be reset to false after every use. + /// </summary> + internal bool TemporaryFlag1 { get { return (nodeIndex & TemporaryFlag1Mask) != 0; } set { nodeIndex = (nodeIndex & ~TemporaryFlag1Mask) | (value ? TemporaryFlag1Mask : 0); } } + + /// <summary> + /// Temporary flag for internal purposes. + /// May only be used in the Unity thread. Must be reset to false after every use. + /// </summary> + internal bool TemporaryFlag2 { get { return (nodeIndex & TemporaryFlag2Mask) != 0; } set { nodeIndex = (nodeIndex & ~TemporaryFlag2Mask) | (value ? TemporaryFlag2Mask : 0); } } + + /// <summary> + /// Position of the node in world space. + /// Note: The position is stored as an Int3, not a Vector3. + /// You can convert an Int3 to a Vector3 using an explicit conversion. + /// <code> var v3 = (Vector3)node.position; </code> + /// </summary> + public Int3 position; + + #region Constants + /// <summary>Position of the walkable bit. See: <see cref="Walkable"/></summary> + const int FlagsWalkableOffset = 0; + /// <summary>Mask of the walkable bit. See: <see cref="Walkable"/></summary> + const uint FlagsWalkableMask = 1 << FlagsWalkableOffset; + + /// <summary>Start of hierarchical node index bits. See: <see cref="HierarchicalNodeIndex"/></summary> + const int FlagsHierarchicalIndexOffset = 1; + /// <summary>Mask of hierarchical node index bits. See: <see cref="HierarchicalNodeIndex"/></summary> + const uint HierarchicalIndexMask = (131072-1) << FlagsHierarchicalIndexOffset; + + /// <summary>Start of <see cref="IsHierarchicalNodeDirty"/> bits. See: <see cref="IsHierarchicalNodeDirty"/></summary> + const int HierarchicalDirtyOffset = 18; + + /// <summary>Mask of the <see cref="IsHierarchicalNodeDirty"/> bit. See: <see cref="IsHierarchicalNodeDirty"/></summary> + const uint HierarchicalDirtyMask = 1 << HierarchicalDirtyOffset; + + /// <summary>Start of graph index bits. See: <see cref="GraphIndex"/></summary> + const int FlagsGraphOffset = 24; + /// <summary>Mask of graph index bits. See: <see cref="GraphIndex"/></summary> + const uint FlagsGraphMask = (256u-1) << FlagsGraphOffset; + + public const uint MaxHierarchicalNodeIndex = HierarchicalIndexMask >> FlagsHierarchicalIndexOffset; + + /// <summary>Max number of graphs-1</summary> + public const uint MaxGraphIndex = (FlagsGraphMask-1) >> FlagsGraphOffset; + public const uint InvalidGraphIndex = (FlagsGraphMask) >> FlagsGraphOffset; + + /// <summary>Start of tag bits. See: <see cref="Tag"/></summary> + const int FlagsTagOffset = 19; + /// <summary>Max number of tags - 1. Always a power of 2 minus one</summary> + public const int MaxTagIndex = 32 - 1; + /// <summary>Mask of tag bits. See: <see cref="Tag"/></summary> + const uint FlagsTagMask = MaxTagIndex << FlagsTagOffset; + + #endregion + + #region Properties + + /// <summary> + /// Holds various bitpacked variables. + /// + /// Bit 0: <see cref="Walkable"/> + /// Bits 1 through 17: <see cref="HierarchicalNodeIndex"/> + /// Bit 18: <see cref="IsHierarchicalNodeDirty"/> + /// Bits 19 through 23: <see cref="Tag"/> + /// Bits 24 through 31: <see cref="GraphIndex"/> + /// + /// Warning: You should pretty much never modify this property directly. Use the other properties instead. + /// </summary> + public uint Flags { + get => flags; + set => flags = value; + } + + /// <summary> + /// Penalty cost for walking on this node. + /// This can be used to make it harder/slower to walk over specific nodes. + /// A cost of 1000 (<see cref="Pathfinding.Int3.Precision"/>) corresponds to the cost of moving 1 world unit. + /// + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public uint Penalty { +#if !ASTAR_NO_PENALTY + get => penalty; + set { + if (value > 0xFFFFFF) + Debug.LogWarning("Very high penalty applied. Are you sure negative values haven't underflowed?\n" + + "Penalty values this high could with long paths cause overflows and in some cases infinity loops because of that.\n" + + "Penalty value applied: "+value); + penalty = value; + } +#else + get => 0U; + set {} +#endif + } + + /// <summary> + /// True if the node is traversable. + /// + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public bool Walkable { + get => (flags & FlagsWalkableMask) != 0; + set { + flags = flags & ~FlagsWalkableMask | (value ? 1U : 0U) << FlagsWalkableOffset; + AstarPath.active.hierarchicalGraph.AddDirtyNode(this); + } + } + + /// <summary> + /// Hierarchical Node that contains this node. + /// The graph is divided into clusters of small hierarchical nodes in which there is a path from every node to every other node. + /// This structure is used to speed up connected component calculations which is used to quickly determine if a node is reachable from another node. + /// + /// See: <see cref="Pathfinding.HierarchicalGraph"/> + /// + /// Warning: This is an internal property and you should most likely not change it. + /// + /// Warning: This is only guaranteed to be valid outside of graph updates, and only for walkable nodes. + /// </summary> + internal int HierarchicalNodeIndex { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get => (int)((flags & HierarchicalIndexMask) >> FlagsHierarchicalIndexOffset); + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + set => flags = (flags & ~HierarchicalIndexMask) | (uint)(value << FlagsHierarchicalIndexOffset); + } + + /// <summary>Some internal bookkeeping</summary> + internal bool IsHierarchicalNodeDirty { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get => (flags & HierarchicalDirtyMask) != 0; + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + set => flags = flags & ~HierarchicalDirtyMask | (value ? 1U : 0U) << HierarchicalDirtyOffset; + } + + /// <summary> + /// Connected component that contains the node. + /// This is visualized in the scene view as differently colored nodes (if the graph coloring mode is set to 'Areas'). + /// Each area represents a set of nodes such that there is no valid path between nodes of different colors. + /// + /// See: https://en.wikipedia.org/wiki/Connected_component_(graph_theory) + /// See: <see cref="Pathfinding.HierarchicalGraph"/> + /// </summary> + public uint Area => AstarPath.active.hierarchicalGraph.GetConnectedComponent(HierarchicalNodeIndex); + + /// <summary> + /// Graph which contains this node. + /// See: <see cref="Pathfinding.AstarData.graphs"/> + /// See: <see cref="Graph"/> + /// </summary> + public uint GraphIndex { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get => (flags & FlagsGraphMask) >> FlagsGraphOffset; + set => flags = flags & ~FlagsGraphMask | value << FlagsGraphOffset; + } + + /// <summary> + /// Node tag. + /// See: tags (view in online documentation for working links) + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public uint Tag { + get => (flags & FlagsTagMask) >> FlagsTagOffset; + set => flags = flags & ~FlagsTagMask | ((value << FlagsTagOffset) & FlagsTagMask); + } + + #endregion + + /// <summary> + /// Inform the system that the node's connectivity has changed. + /// This is used for recalculating the connected components of the graph. + /// + /// See: <see cref="Pathfinding.HierarchicalGraph"/> + /// + /// You must call this method if you change the connectivity or walkability of the node without going through the high level methods + /// such as the <see cref="Walkable"/> property or the <see cref="Connect"/> method. For example if your manually change the <see cref="Pathfinding.MeshNode.connections"/> array you need to call this method. + /// </summary> + public void SetConnectivityDirty () { + AstarPath.active.hierarchicalGraph.AddDirtyNode(this); + } + + /// <summary> + /// Calls the delegate with all connections from this node. + /// + /// * + /// <code> + /// node.GetConnections(connectedTo => { + /// Debug.DrawLine((Vector3)node.position, (Vector3)connectedTo.position, Color.red); + /// }); + /// </code> + /// + /// You can add all connected nodes to a list like this + /// <code> + /// var connections = new List<GraphNode>(); + /// node.GetConnections(connections.Add); + /// </code> + /// </summary> + /// <param name="action">The delegate which will be called once for every connection.</param> + /// <param name="connectionFilter">A bitmask of which connection types will be included. You may pass any combination of \reflink{Connection.OutgoingConnection} and \reflink{Connection.IncomingConnection}. + /// Defaults to only outgoing connections. Unless one-way links are added to a graph, all connections will typically be bidirectional.</param> + public virtual void GetConnections (System.Action<GraphNode> action, int connectionFilter = Connection.OutgoingConnection) { + GetConnections((GraphNode node, ref System.Action<GraphNode> action) => action(node), ref action, connectionFilter); + } + + /// <summary> + /// Calls the delegate with all connections from or to this node, and passes a custom data value to the delegate. + /// + /// <code> + /// node.GetConnections(connectedTo => { + /// Debug.DrawLine((Vector3)node.position, (Vector3)connectedTo.position, Color.red); + /// }); + /// </code> + /// + /// You can add all connected nodes to a list like this + /// <code> + /// var connections = new List<GraphNode>(); + /// node.GetConnections(connections.Add); + /// </code> + /// </summary> + /// <param name="action">The delegate which will be called once for every connection. + /// The first parameter to the delegate is the connection and the second parameter is the custom data passed to this method.</param> + /// <param name="data">Custom data which will be passed to the delegate.</param> + /// <param name="connectionFilter">A bitmask of which connection types will be included. A connection can either be incoming, outgoing, or both (bidirectional). You may pass any combination of \reflink{Connection.OutgoingConnection} and \reflink{Connection.IncomingConnection}. + /// Defaults to only outgoing connections. Unless one-way links are added to a graph, all connections will typically be bidirectional.</param> + public abstract void GetConnections<T>(GetConnectionsWithData<T> action, ref T data, int connectionFilter = Connection.OutgoingConnection); + + public delegate void GetConnectionsWithData<T>(GraphNode node, ref T data); + + /// <summary> + /// Adds a connection between two nodes. + /// + /// If the nodes already have a connection to each other, that connection will be updated with the new cost. + /// + /// Note that some graphs have a special representation for some connections which is more efficient. + /// For example grid graphs can represent connections to its 8 neighbours more efficiently. + /// But to use that efficient representation you'll need to call <see cref="GridNode.SetConnectionInternal"/> instead of this method. + /// + /// This is different from an off-mesh link. An off-mesh link contains more metadata about the connection and is in many cases preferable to use instead of this method. + /// This is a much lower-level method which is used by off-mesh links internally. + /// + /// Movement scripts such as the <see cref="RichAI"/> or <see cref="FollowerEntity"/> may get confused if they try to follow a connection made using this method + /// as it does not contain any information about how to traverse the connection. + /// + /// Internally, both nodes keep track of the connection to the other node, even for a one-way connection. + /// This is done to make sure the connection can always be removed later on, if for example one of the nodes is destroyed. + /// + /// <code> + /// // Connect two nodes + /// var node1 = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node; + /// var node2 = AstarPath.active.GetNearest(transform.position + Vector3.right, NNConstraint.None).node; + /// var cost = (uint)(node2.position - node1.position).costMagnitude; + /// + /// GraphNode.Connect(node1, node2, cost, OffMeshLinks.Directionality.TwoWay); + /// </code> + /// + /// See: <see cref="OffMeshLinks"/> + /// See: <see cref="AddPartialConnection"/> which is a lower level method. But if you use it, you need to handle invariants yourself. + /// </summary> + /// <param name="lhs">First node to connect.</param> + /// <param name="rhs">Second node to connect</param> + /// <param name="cost">Cost of the connection. A cost of 1000 corresponds approximately to the cost of moving one world unit. See \reflink{Int3.Precision}.</param> + /// <param name="directionality">Determines if both lhs->rhs and rhs->lhs connections will be created, or if only a connection from lhs->rhs should be created.</param> + public static void Connect (GraphNode lhs, GraphNode rhs, uint cost, OffMeshLinks.Directionality directionality = OffMeshLinks.Directionality.TwoWay) { + lhs.AddPartialConnection(rhs, cost, true, directionality == OffMeshLinks.Directionality.TwoWay); + rhs.AddPartialConnection(lhs, cost, directionality == OffMeshLinks.Directionality.TwoWay, true); + } + + /// <summary> + /// Removes the connection between two nodes. + /// + /// If no connection exists between the nodes, nothing will be done. + /// + /// This will also handle special connections representations that some node types use. For example grid graphs represent + /// the connections to their 8 grid neighbours differently from other connections. + /// + /// See: <see cref="GraphNode.Connect"/> + /// </summary> + public static void Disconnect (GraphNode lhs, GraphNode rhs) { + lhs.RemovePartialConnection(rhs); + rhs.RemovePartialConnection(lhs); + } + + /// <summary> + /// Adds a connection to the given node. + /// + /// Deprecated: Use the static <see cref="Connect"/> method instead, or <see cref="AddPartialConnection"/> if you really need to. + /// </summary> + [System.Obsolete("Use the static Connect method instead, or AddPartialConnection if you really need to")] + public void AddConnection(GraphNode node, uint cost) => AddPartialConnection(node, cost, true, true); + + /// <summary> + /// Removes a connection to the given node. + /// + /// Deprecated: Use the static <see cref="Disconnect"/> method instead, or <see cref="RemovePartialConnection"/> if you really need to. + /// </summary> + + [System.Obsolete("Use the static Disconnect method instead, or RemovePartialConnection if you really need to")] + public void RemoveConnection(GraphNode node) => RemovePartialConnection(node); + + /// <summary> + /// Add a connection from this node to the specified node. + /// If the connection already exists, the cost will simply be updated and + /// no extra connection added. + /// + /// <code> + /// AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => { + /// // Connect two nodes + /// var node1 = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node; + /// var node2 = AstarPath.active.GetNearest(transform.position + Vector3.right, NNConstraint.None).node; + /// var cost = (uint)(node2.position - node1.position).costMagnitude; + /// node1.AddPartialConnection(node2, cost, true, true); + /// node2.AddPartialConnection(node1, cost, true, true); + /// + /// node1.ContainsOutgoingConnection(node2); // True + /// + /// node1.RemovePartialConnection(node2); + /// node2.RemovePartialConnection(node1); + /// })); + /// </code> + /// + /// Warning: In almost all cases, you should be using the <see cref="Connect"/> method instead. If you use this method, you must ensure that you preserve the required invariants of connections. + /// Notably: If a connection exists from A to B, then there must also exist a connection from B to A. And their outgoing and incoming connection flags must be set symmetrically. + /// + /// Some graphs have a special representation for some connections which is more efficient. + /// For example grid graphs can represent connections to its 8 neighbours more efficiently. + /// But to use that efficient representation you'll need to call <see cref="GridNode.SetConnectionInternal"/> instead of this method. + /// </summary> + public abstract void AddPartialConnection(GraphNode node, uint cost, bool isOutgoing, bool isIncoming); + + /// <summary> + /// Removes any connection from this node to the specified node. + /// If no such connection exists, nothing will be done. + /// + /// Warning: In almost all cases, you should be using the <see cref="Disconnect"/> method instead. If you use this method, you must ensure that you preserve the required invariants of connections. + /// Notably: If a connection exists from A to B, then there must also exist a connection from B to A. And their outgoing and incoming connection flags must be set symmetrically. + /// Graphs sometimes use this method directly to improve performance in some situations. + /// + /// <code> + /// AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => { + /// // Connect two nodes + /// var node1 = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node; + /// var node2 = AstarPath.active.GetNearest(transform.position + Vector3.right, NNConstraint.None).node; + /// var cost = (uint)(node2.position - node1.position).costMagnitude; + /// node1.AddPartialConnection(node2, cost, true, true); + /// node2.AddPartialConnection(node1, cost, true, true); + /// + /// node1.ContainsOutgoingConnection(node2); // True + /// + /// node1.RemovePartialConnection(node2); + /// node2.RemovePartialConnection(node1); + /// })); + /// </code> + /// </summary> + public abstract void RemovePartialConnection(GraphNode node); + + /// <summary> + /// Remove all connections between this node and other nodes. + /// + /// Warning: If you pass false to the alsoReverse parameter, you must ensure that you preserve the required invariants of connections. See <see cref="RemovePartialConnection"/>. + /// </summary> + /// <param name="alsoReverse">if true, neighbours will be requested to remove connections to this node.</param> + public abstract void ClearConnections(bool alsoReverse = true); + + /// <summary> + /// True if this node contains a connection to the given node. + /// + /// Deprecated: Use <see cref="ContainsOutgoingConnection"/> instead + /// </summary> + [System.Obsolete("Use ContainsOutgoingConnection instead")] + public bool ContainsConnection(GraphNode node) => ContainsOutgoingConnection(node); + + /// <summary> + /// True if this node contains a connection to the given node. + /// + /// This will not return true if another node has a one-way connection to this node. + /// + /// <code> + /// AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => { + /// // Connect two nodes + /// var node1 = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node; + /// var node2 = AstarPath.active.GetNearest(transform.position + Vector3.right, NNConstraint.None).node; + /// var cost = (uint)(node2.position - node1.position).costMagnitude; + /// node1.AddPartialConnection(node2, cost, true, true); + /// node2.AddPartialConnection(node1, cost, true, true); + /// + /// node1.ContainsOutgoingConnection(node2); // True + /// + /// node1.RemovePartialConnection(node2); + /// node2.RemovePartialConnection(node1); + /// })); + /// </code> + /// </summary> + public virtual bool ContainsOutgoingConnection (GraphNode node) { + // Simple but slow default implementation + bool contains = false; + + GetConnections((GraphNode neighbour, ref bool contains) => { + contains |= neighbour == node; + }, ref contains); + return contains; + } + + /// <summary> + /// Add a portal from this node to the specified node. + /// This function should add a portal to the left and right lists which is connecting the two nodes (this and other). + /// + /// Returns: True if the call was deemed successful. False if some unknown case was encountered and no portal could be added. + /// If both calls to node1.GetPortal (node2,...) and node2.GetPortal (node1,...) return false, the funnel modifier will fall back to adding to the path + /// the positions of the node. + /// + /// The default implementation simply returns false. + /// + /// This function may add more than one portal if necessary. + /// + /// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html + /// + /// Deprecated: Use GetPortal(GraphNode, out Vector3, out Vector3) instead + /// </summary> + /// <param name="other">The node which is on the other side of the portal (strictly speaking it does not actually have to be on the other side of the portal though).</param> + /// <param name="left">List of portal points on the left side of the funnel</param> + /// <param name="right">List of portal points on the right side of the funnel</param> + /// <param name="backwards">If this is true, the call was made on a node with the other node as the node before this one in the path. + /// In this case you may choose to do nothing since a similar call will be made to the other node with this node referenced as other (but then with backwards = true). + /// You do not have to care about switching the left and right lists, that is done for you already.</param> + [System.Obsolete("Use GetPortal(GraphNode, out Vector3, out Vector3) instead")] + public bool GetPortal (GraphNode other, List<Vector3> left, List<Vector3> right, bool backwards) { + if (!backwards && GetPortal(other, out var lp, out var rp)) { + if (left != null) { + left.Add(lp); + right.Add(rp); + } + return true; + } else { + return false; + } + } + + /// <summary> + /// Add a portal from this node to the specified node. + /// This function returns a portal which connects this node to the given adjacenet node. + /// + /// Returns: True if the call was deemed successful. False if some unknown case was encountered and no portal could be added. + /// If both calls to node1.GetPortal (node2,...) and node2.GetPortal (node1,...) return false, the funnel modifier will fall back to adding to the path + /// the positions of the node. + /// + /// The default implementation simply returns false. + /// + /// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html + /// </summary> + /// <param name="other">The node which is on the other side of the portal.</param> + /// <param name="left">Output left side of the portal.</param> + /// <param name="right">Output right side of the portal.</param> + public virtual bool GetPortal (GraphNode other, out Vector3 left, out Vector3 right) { + left = Vector3.zero; + right = Vector3.zero; + return false; + } + + /// <summary> + /// Open the node. + /// Used internally by the A* algorithm. + /// </summary> + public abstract void Open(Path path, uint pathNodeIndex, uint gScore); + + /// <summary> + /// Open the node at a specific point. + /// + /// Used internally by the A* algorithm. + /// + /// Used when a path starts inside a node, or when an off-mesh link is used to move to a point inside this node. + /// </summary> + public abstract void OpenAtPoint(Path path, uint pathNodeIndex, Int3 position, uint gScore); + + /// <summary> + /// The position of the path node during the search. + /// + /// When an A* search on triangle nodes is carried out, each edge of the node is a separate path node variant. + /// The search will additionally decide where on that edge the path node is located. + /// This is encoded by the fractionAlongEdge variable. + /// This function decodes the position of the path node. + /// + /// Note: Most node types only have a single path node variant and does not use the fractionAlongEdge field. + /// In those cases this function only returns the node <see cref="position"/> unchanged. + /// </summary> + public virtual Int3 DecodeVariantPosition(uint pathNodeIndex, uint fractionAlongEdge) => position; + + /// <summary>The surface area of the node in square world units</summary> + public virtual float SurfaceArea() => 0; + + /// <summary> + /// A random point on the surface of the node. + /// For point nodes and other nodes which do not have a surface, this will always return the position of the node. + /// </summary> + public virtual Vector3 RandomPointOnSurface () { + return (Vector3)position; + } + + /// <summary>Closest point on the surface of this node to the point p</summary> + public abstract Vector3 ClosestPointOnNode(Vector3 p); + + /// <summary>Checks if point is inside the node when seen from above</summary> + public virtual bool ContainsPoint (Int3 point) { + return ContainsPoint((Vector3)point); + } + + /// <summary>Checks if point is inside the node when seen from above.</summary> + public abstract bool ContainsPoint(Vector3 point); + + /// <summary>Checks if point is inside the node in graph space</summary> + public abstract bool ContainsPointInGraphSpace(Int3 point); + + /// <summary> + /// Hash code used for checking if the gizmos need to be updated. + /// Will change when the gizmos for the node might change. + /// </summary> + public virtual int GetGizmoHashCode () { + // Some hashing, the constants are just some arbitrary prime numbers. #flags contains the info for #Tag and #Walkable + return position.GetHashCode() ^ (19 * (int)Penalty) ^ (41 * (int)(flags & ~(HierarchicalIndexMask | HierarchicalDirtyMask))); + } + + /// <summary>Serialized the node data to a byte array</summary> + public virtual void SerializeNode (GraphSerializationContext ctx) { + // Write basic node data. + ctx.writer.Write(Penalty); + // Save all flags except the hierarchical node index and the dirty bit + ctx.writer.Write(Flags & ~(HierarchicalIndexMask | HierarchicalDirtyMask)); + } + + /// <summary>Deserializes the node data from a byte array</summary> + public virtual void DeserializeNode (GraphSerializationContext ctx) { + Penalty = ctx.reader.ReadUInt32(); + // Load all flags except the hierarchical node index and the dirty bit (they aren't saved in newer versions and older data should just be cleared) + // Note that the dirty bit needs to be preserved here because it may already be set (due to the node being created) + Flags = (ctx.reader.ReadUInt32() & ~(HierarchicalIndexMask | HierarchicalDirtyMask)) | (Flags & (HierarchicalIndexMask | HierarchicalDirtyMask)); + + // Set the correct graph index (which might have changed, e.g if loading additively) + GraphIndex = ctx.graphIndex; + } + + /// <summary> + /// Used to serialize references to other nodes e.g connections. + /// Use the GraphSerializationContext.GetNodeIdentifier and + /// GraphSerializationContext.GetNodeFromIdentifier methods + /// for serialization and deserialization respectively. + /// + /// Nodes must override this method and serialize their connections. + /// Graph generators do not need to call this method, it will be called automatically on all + /// nodes at the correct time by the serializer. + /// </summary> + public virtual void SerializeReferences (GraphSerializationContext ctx) { + } + + /// <summary> + /// Used to deserialize references to other nodes e.g connections. + /// Use the GraphSerializationContext.GetNodeIdentifier and + /// GraphSerializationContext.GetNodeFromIdentifier methods + /// for serialization and deserialization respectively. + /// + /// Nodes must override this method and serialize their connections. + /// Graph generators do not need to call this method, it will be called automatically on all + /// nodes at the correct time by the serializer. + /// </summary> + public virtual void DeserializeReferences (GraphSerializationContext ctx) { + } + } + + public abstract class MeshNode : GraphNode { + /// <summary> + /// All connections from this node. + /// See: <see cref="Connect"/> + /// See: <see cref="Disconnect"/> + /// + /// Note: If you modify this array or the contents of it you must call <see cref="SetConnectivityDirty"/>. + /// + /// May be null if the node has no connections. + /// </summary> + public Connection[] connections; + + /// <summary>Get a vertex of this node.</summary> + /// <param name="i">vertex index. Must be between 0 and #GetVertexCount (exclusive).</param> + public abstract Int3 GetVertex(int i); + + /// <summary> + /// Number of corner vertices that this node has. + /// For example for a triangle node this will return 3. + /// </summary> + public abstract int GetVertexCount(); + + /// <summary> + /// Closest point on the surface of this node when seen from above. + /// This is usually very similar to <see cref="ClosestPointOnNode"/> but when the node is in a slope this can be significantly different. + /// [Open online documentation to see images] + /// When the blue point in the above image is used as an argument this method call will return the green point while the <see cref="ClosestPointOnNode"/> method will return the red point. + /// </summary> + public abstract Vector3 ClosestPointOnNodeXZ(Vector3 p); + + public override void ClearConnections (bool alsoReverse = true) { + // Remove all connections to this node from our neighbours + if (alsoReverse && connections != null) { + for (int i = 0; i < connections.Length; i++) { + connections[i].node.RemovePartialConnection(this); + } + } + + ArrayPool<Connection>.Release(ref connections, true); + AstarPath.active.hierarchicalGraph.AddDirtyNode(this); + } + + public override void GetConnections<T>(GetConnectionsWithData<T> action, ref T data, int connectionFilter = Connection.OutgoingConnection) { + if (connections == null) return; + for (int i = 0; i < connections.Length; i++) if ((connections[i].shapeEdgeInfo & connectionFilter) != 0) action(connections[i].node, ref data); + } + + public override bool ContainsOutgoingConnection (GraphNode node) { + if (connections != null) for (int i = 0; i < connections.Length; i++) if (connections[i].node == node && connections[i].isOutgoing) return true; + return false; + } + + public override void AddPartialConnection (GraphNode node, uint cost, bool isOutgoing, bool isIncoming) { + AddPartialConnection(node, cost, Connection.PackShapeEdgeInfo(isOutgoing, isIncoming)); + } + + /// <summary> + /// Add a connection from this node to the specified node. + /// + /// If the connection already exists, the cost will simply be updated and + /// no extra connection added. + /// + /// Warning: In almost all cases, you should be using the <see cref="Connect"/> method instead. If you use this method, you must ensure that you preserve the required invariants of connections. + /// Notably: If a connection exists from A to B, then there must also exist a connection from B to A. And their outgoing and incoming connection flags must be set symmetrically. + /// </summary> + /// <param name="node">Node to add a connection to</param> + /// <param name="cost">Cost of traversing the connection. A cost of 1000 corresponds approximately to the cost of moving 1 world unit.</param> + /// <param name="shapeEdgeInfo">Info about how the edge is which edge on the shape of this node to use or \reflink{Connection.NoSharedEdge} if no edge is used. See \reflink{Connection.PackShapeEdgeInfo(byte,byte,bool,bool,bool)}.</param> + public void AddPartialConnection (GraphNode node, uint cost, byte shapeEdgeInfo) { + if (node == null) throw new System.ArgumentNullException(); + + // Check if we already have a connection to the node + if (connections != null) { + for (int i = 0; i < connections.Length; i++) { + if (connections[i].node == node) { + // Just update the cost for the existing connection + connections[i].cost = cost; + connections[i].shapeEdgeInfo = shapeEdgeInfo; + return; + } + } + } + + // Create new arrays which include the new connection + int connLength = connections != null ? connections.Length : 0; + + var newconns = ArrayPool<Connection>.ClaimWithExactLength(connLength+1); + for (int i = 0; i < connLength; i++) { + newconns[i] = connections[i]; + } + + newconns[connLength] = new Connection(node, cost, shapeEdgeInfo); + + if (connections != null) { + ArrayPool<Connection>.Release(ref connections, true); + } + + connections = newconns; + AstarPath.active.hierarchicalGraph.AddDirtyNode(this); + } + + public override void RemovePartialConnection (GraphNode node) { + if (connections == null) return; + + // Iterate through all connections and check if there are any to the node + for (int i = 0; i < connections.Length; i++) { + if (connections[i].node == node) { + // Create new arrays which have the specified node removed + int connLength = connections.Length; + + var newconns = ArrayPool<Connection>.ClaimWithExactLength(connLength-1); + for (int j = 0; j < i; j++) { + newconns[j] = connections[j]; + } + for (int j = i+1; j < connLength; j++) { + newconns[j-1] = connections[j]; + } + + if (connections != null) { + ArrayPool<Connection>.Release(ref connections, true); + } + + connections = newconns; + AstarPath.active.hierarchicalGraph.AddDirtyNode(this); + return; + } + } + } + + public override int GetGizmoHashCode () { + var hash = base.GetGizmoHashCode(); + + if (connections != null) { + for (int i = 0; i < connections.Length; i++) { + hash ^= 17 * connections[i].GetHashCode(); + } + } + return hash; + } + + public override void SerializeReferences(GraphSerializationContext ctx) => ctx.SerializeConnections(connections, true); + + public override void DeserializeReferences(GraphSerializationContext ctx) => connections = ctx.DeserializeConnections(true); + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes/GraphNode.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes/GraphNode.cs.meta new file mode 100644 index 0000000..0ecafaf --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Nodes/GraphNode.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: a660e0d66fb3b49d19d438de51ac6744 +timeCreated: 1486987492 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding.meta new file mode 100644 index 0000000..ddf8b4f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fbaf930b256c7634db0b555f62098d79 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/BlockableChannel.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/BlockableChannel.cs new file mode 100644 index 0000000..e3fce03 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/BlockableChannel.cs @@ -0,0 +1,212 @@ +using System.Threading; +using Pathfinding.Util; +using UnityEngine.Assertions; + +namespace Pathfinding { + /// <summary> + /// Multi-producer-multi-consumer (MPMC) channel. + /// + /// This is a channel that can be used to send data between threads. + /// It is thread safe and can be used by multiple threads at the same time. + /// + /// Additionally, the channel can be put into a blocking mode, which will cause all calls to Receive to block until the channel is unblocked. + /// </summary> + internal class BlockableChannel<T> where T : class { + public enum PopState { + Ok, + Wait, + Closed, + } + + readonly System.Object lockObj = new System.Object(); + + CircularBuffer<T> queue = new CircularBuffer<T>(16); + public int numReceivers { get; private set; } + + // Marked as volatile such that the compiler will not try to optimize the allReceiversBlocked property too much (this is more of a theoretical concern than a practical issue). + volatile int waitingReceivers; +#if !UNITY_WEBGL + ManualResetEvent starving = new ManualResetEvent(false); +#endif + bool blocked; + + /// <summary>True if <see cref="Close"/> has been called</summary> + public bool isClosed { get; private set; } + + /// <summary>True if the queue is empty</summary> + public bool isEmpty { + get { + lock (lockObj) { + return queue.Length == 0; + } + } + } + + /// <summary>True if blocking and all receivers are waiting for unblocking</summary> + // Note: This is designed to be lock-free for performance. But it will only generate a useful value if called from the same thread that is blocking/unblocking the queue, otherwise the return value could become invalid at any time. + public bool allReceiversBlocked => blocked && waitingReceivers == numReceivers; + + /// <summary>If true, all calls to Receive will block until this property is set to false</summary> + public bool isBlocked { + get => blocked; + set { + lock (lockObj) { + blocked = value; + if (isClosed) return; + isStarving = value || queue.Length == 0; + } + } + } + + /// <summary>All calls to Receive and ReceiveNoBlock will now return PopState.Closed</summary> + public void Close () { + lock (lockObj) { + isClosed = true; + isStarving = false; + } + } + + bool isStarving { + get { +#if UNITY_WEBGL + // In WebGL, semaphores are not supported. + // They will compile, but they don't work properly. + // So instead we directly use what the starving semaphore should indicate. + return (blocked || queue.Length == 0) && !isClosed; +#else + return !starving.WaitOne(0); +#endif + } + set { +#if !UNITY_WEBGL + if (value) starving.Reset(); + else starving.Set(); +#endif + } + } + + /// <summary> + /// Resets a closed channel so that it can be used again. + /// + /// The existing queue is preserved. + /// + /// This will throw an exception if there are any receivers still active. + /// </summary> + public void Reopen () { + lock (lockObj) { + if (numReceivers != 0) throw new System.InvalidOperationException("Can only reopen a channel after Close has been called on all receivers"); + Assert.AreEqual(waitingReceivers, 0); + isClosed = false; + isBlocked = false; + } + } + + public Receiver AddReceiver () { + lock (lockObj) { + if (isClosed) throw new System.InvalidOperationException("Channel is closed"); + this.numReceivers++; + return new Receiver(this); + } + } + + /// <summary>Push a path to the front of the queue</summary> + public void PushFront (T path) { + lock (lockObj) { + if (isClosed) throw new System.InvalidOperationException("Channel is closed"); + queue.PushStart(path); + if (!blocked) isStarving = false; + } + } + + /// <summary>Push a path to the end of the queue</summary> + public void Push (T path) { + lock (lockObj) { + if (isClosed) throw new System.InvalidOperationException("Channel is closed"); + queue.PushEnd(path); + if (!blocked) isStarving = false; + } + } + + /// <summary>Allows receiving items from a channel</summary> + public struct Receiver { + BlockableChannel<T> channel; + + public Receiver(BlockableChannel<T> channel) { + this.channel = channel; + } + + /// <summary> + /// Call when a receiver was terminated. + /// + /// After this call, this receiver cannot be used for anything. + /// </summary> + public void Close () { + lock (channel.lockObj) { + Assert.IsTrue(channel.numReceivers > 0); + Assert.IsTrue(channel.waitingReceivers < channel.numReceivers); + channel.numReceivers--; + } + channel = null; + } + + /// <summary> + /// Receives the next item from the channel. + /// This call will block if there are no items in the channel or if the channel is currently blocked. + /// + /// Returns: PopState.Ok and a non-null item in the normal case. Returns PopState.Closed if the channel has been closed. + /// </summary> + public PopState Receive (out T item) { +#if UNITY_WEBGL + throw new System.Exception("Cannot block in WebGL. Use ReceiveNoBlock instead."); +#else + Interlocked.Increment(ref channel.waitingReceivers); + while (true) { + channel.starving.WaitOne(); + // Note that right here, the channel could become blocked, closed or anything really. We have to check conditions again after locking. + lock (channel.lockObj) { + if (channel.isClosed) { + Interlocked.Decrement(ref channel.waitingReceivers); + item = null; + return PopState.Closed; + } + if (channel.queue.Length == 0) channel.isStarving = true; + if (channel.isStarving) continue; + Assert.IsFalse(channel.blocked); + Interlocked.Decrement(ref channel.waitingReceivers); + item = channel.queue.PopStart(); + return PopState.Ok; + } + } +#endif + } + + /// <summary> + /// Receives the next item from the channel, this call will not block. + /// To ensure a consistent state, the caller must follow this pattern. + /// 1. Call ReceiveNoBlock(false), if PopState.Wait is returned, wait for a bit (e.g yield return null in a Unity coroutine) + /// 2. try again with PopNoBlock(true), if PopState.Wait, wait for a bit + /// 3. Repeat from step 2. + /// </summary> + public PopState ReceiveNoBlock (bool blockedBefore, out T item) { + item = null; + if (!blockedBefore) Interlocked.Increment(ref channel.waitingReceivers); + while (true) { + if (channel.isStarving) return PopState.Wait; + // Note that right here, the channel could become blocked, closed or anything really. We have to check conditions again after locking. + lock (channel.lockObj) { + if (channel.isClosed) { + Interlocked.Decrement(ref channel.waitingReceivers); + return PopState.Closed; + } + if (channel.queue.Length == 0) channel.isStarving = true; + if (channel.isStarving) continue; + Assert.IsFalse(channel.blocked); + Interlocked.Decrement(ref channel.waitingReceivers); + item = channel.queue.PopStart(); + return PopState.Ok; + } + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/BlockableChannel.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/BlockableChannel.cs.meta new file mode 100644 index 0000000..cad1e5f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/BlockableChannel.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e6a8344fd0d1c453cbaf5c5eb8f55ca5 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GlobalNodeStorage.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GlobalNodeStorage.cs new file mode 100644 index 0000000..ec8b530 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GlobalNodeStorage.cs @@ -0,0 +1,299 @@ +using Unity.Collections; +using UnityEngine; +using Unity.Mathematics; +using Unity.Jobs; +using UnityEngine.Profiling; + +namespace Pathfinding { + using Pathfinding.Util; + using Pathfinding.Jobs; + + internal class GlobalNodeStorage { + readonly AstarPath astar; + Unity.Jobs.JobHandle lastAllocationJob; + + /// <summary> + /// Holds the next node index which has not been used by any previous node. + /// See: <see cref="nodeIndexPools"/> + /// </summary> + public uint nextNodeIndex = 1; + + /// <summary> + /// The number of nodes for which path node data has been reserved. + /// Will be at least as high as <see cref="nextNodeIndex"/> + /// </summary> + uint reservedPathNodeData = 0; + + /// <summary>Number of nodes that have been destroyed in total</summary> + public uint destroyedNodesVersion { get; private set; } + +#if ASTAR_MORE_MULTI_TARGET_PATH_TARGETS + public const int MaxTemporaryNodes = 4096; +#else + public const int MaxTemporaryNodes = 256; +#endif + + /// <summary> + /// Holds indices for nodes that have been destroyed. + /// To avoid trashing a lot of memory structures when nodes are + /// frequently deleted and created, node indices are reused. + /// + /// There's one pool for each possible number of node variants (1, 2 and 3). + /// </summary> + readonly IndexedStack<uint>[] nodeIndexPools = new [] { + new IndexedStack<uint>(), + new IndexedStack<uint>(), + new IndexedStack<uint>(), + }; + + public PathfindingThreadData[] pathfindingThreadData = new PathfindingThreadData[0]; + + /// <summary>Maps from NodeIndex to node</summary> + GraphNode[] nodes = new GraphNode[0]; + + public GlobalNodeStorage (AstarPath astar) { + this.astar = astar; + } + + public GraphNode GetNode(uint nodeIndex) => nodes[nodeIndex]; + +#if UNITY_EDITOR + public struct DebugPathNode { + public uint g; + public uint h; + public uint parentIndex; + public ushort pathID; + public byte fractionAlongEdge; + } +#endif + + public struct PathfindingThreadData { + public UnsafeSpan<PathNode> pathNodes; +#if UNITY_EDITOR + public UnsafeSpan<DebugPathNode> debugPathNodes; +#endif + } + + class IndexedStack<T> { + T[] buffer = new T[4]; + + public int Count { get; private set; } + + public void Push (T v) { + if (Count == buffer.Length) { + Util.Memory.Realloc(ref buffer, buffer.Length * 2); + } + + buffer[Count] = v; + Count++; + } + + public void Clear () { + Count = 0; + } + + public T Pop () { + Count--; + return buffer[Count]; + } + + /// <summary>Pop the last N elements and store them in the buffer. The items will be in insertion order.</summary> + public void PopMany (T[] resultBuffer, int popCount) { + if (popCount > Count) throw new System.IndexOutOfRangeException(); + System.Array.Copy(buffer, Count - popCount, resultBuffer, 0, popCount); + Count -= popCount; + } + } + + void DisposeThreadData () { + if (pathfindingThreadData.Length > 0) { + for (int i = 0; i < pathfindingThreadData.Length; i++) { + unsafe { + pathfindingThreadData[i].pathNodes.Free(Allocator.Persistent); +#if UNITY_EDITOR + pathfindingThreadData[i].debugPathNodes.Free(Allocator.Persistent); +#endif + } + } + pathfindingThreadData = new PathfindingThreadData[0]; + } + } + + public void SetThreadCount (int threadCount) { + if (pathfindingThreadData.Length != threadCount) { + DisposeThreadData(); + pathfindingThreadData = new PathfindingThreadData[threadCount]; + + for (int i = 0; i < pathfindingThreadData.Length; i++) { + // Allocate per-thread data. + // We allocate using UnsafeSpans because this code may run inside jobs, and Unity does not allow us to allocate NativeArray memory + // using the persistent allocator inside jobs. + pathfindingThreadData[i].pathNodes = new UnsafeSpan<PathNode>(Allocator.Persistent, (int)reservedPathNodeData + MaxTemporaryNodes); +#if UNITY_EDITOR + pathfindingThreadData[i].debugPathNodes = new UnsafeSpan<DebugPathNode>(Allocator.Persistent, (int)reservedPathNodeData); + pathfindingThreadData[i].debugPathNodes.FillZeros(); +#endif + var pnodes = pathfindingThreadData[i].pathNodes; + pnodes.Fill(PathNode.Default); + } + } + } + + /// <summary> + /// Initializes temporary path data for a node. + /// Warning: This method should not be called directly. + /// + /// See: <see cref="AstarPath.InitializeNode"/> + /// </summary> + public void InitializeNode (GraphNode node) { + var variants = node.PathNodeVariants; + + // Graphs may initialize nodes from different threads, + // so we need to lock. + // Luckily, uncontested locks are really really cheap in C# + lock (this) { + if (nodeIndexPools[variants-1].Count > 0) { + node.NodeIndex = nodeIndexPools[variants-1].Pop(); + } else { + // Highest node index in the new list of nodes + node.NodeIndex = nextNodeIndex; + nextNodeIndex += (uint)variants; + ReserveNodeIndices(nextNodeIndex); + } + + for (int i = 0; i < variants; i++) { + nodes[node.NodeIndex + i] = node; + } + + astar.hierarchicalGraph.OnCreatedNode(node); + } + } + + /// <summary> + /// Reserves space for global node data. + /// + /// Warning: Must be called only when a lock is held on this object. + /// </summary> + void ReserveNodeIndices (uint nextNodeIndex) { + if (nextNodeIndex <= reservedPathNodeData) return; + + reservedPathNodeData = math.ceilpow2(nextNodeIndex); + + // Allocate more internal pathfinding data for the new nodes + astar.hierarchicalGraph.ReserveNodeIndices(reservedPathNodeData); + var threadCount = pathfindingThreadData.Length; + DisposeThreadData(); + SetThreadCount(threadCount); + Memory.Realloc(ref nodes, (int)reservedPathNodeData); + } + + /// <summary> + /// Destroyes the given node. + /// This is to be called after the node has been disconnected from the graph so that it cannot be reached from any other nodes. + /// It should only be called during graph updates, that is when the pathfinding threads are either not running or paused. + /// + /// Warning: This method should not be called by user code. It is used internally by the system. + /// </summary> + public void DestroyNode (GraphNode node) { + var nodeIndex = node.NodeIndex; + if (nodeIndex == GraphNode.DestroyedNodeIndex) return; + + destroyedNodesVersion++; + int variants = node.PathNodeVariants; + nodeIndexPools[variants - 1].Push(nodeIndex); + for (int i = 0; i < variants; i++) { + nodes[nodeIndex + i] = null; + } + + for (int t = 0; t < pathfindingThreadData.Length; t++) { + var threadData = pathfindingThreadData[t]; + for (uint i = 0; i < variants; i++) { + // This is not required for pathfinding, but not clearing it may confuse gizmo drawing for a fraction of a second. + // Especially when 'Show Search Tree' is enabled + threadData.pathNodes[nodeIndex + i].pathID = 0; +#if UNITY_EDITOR + threadData.debugPathNodes[nodeIndex + i].pathID = 0; +#endif + } + } + + astar.hierarchicalGraph.OnDestroyedNode(node); + } + + public void OnDisable () { + lastAllocationJob.Complete(); + nextNodeIndex = 1; + reservedPathNodeData = 0; + for (int i = 0; i < nodeIndexPools.Length; i++) nodeIndexPools[i].Clear(); + nodes = new GraphNode[0]; + DisposeThreadData(); + } + + struct JobAllocateNodes<T> : IJob where T : GraphNode { + public T[] result; + public int count; + public GlobalNodeStorage nodeStorage; + public uint variantsPerNode; + public System.Func<T> createNode; + + public bool allowBoundsChecks => false; + + public void Execute () { + Profiler.BeginSample("Allocating nodes"); + var hierarchicalGraph = nodeStorage.astar.hierarchicalGraph; + + lock (nodeStorage) { + var pool = nodeStorage.nodeIndexPools[variantsPerNode-1]; + uint nextNodeIndex = nodeStorage.nextNodeIndex; + + // Allocate the actual nodes + for (uint i = 0; i < count; i++) { + var node = result[i] = createNode(); + + // Get a new node index. Re-use one from a previously destroyed node if possible + if (pool.Count > 0) { + node.NodeIndex = pool.Pop(); + } else { + node.NodeIndex = nextNodeIndex; + nextNodeIndex += variantsPerNode; + } + } + + // Allocate more internal pathfinding data for the new nodes + nodeStorage.ReserveNodeIndices(nextNodeIndex); + + // Mark the node indices as used + nodeStorage.nextNodeIndex = nextNodeIndex; + + for (int i = 0; i < count; i++) { + var node = result[i]; + hierarchicalGraph.AddDirtyNode(node); + nodeStorage.nodes[node.NodeIndex] = node; + } + } + Profiler.EndSample(); + } + } + + public Unity.Jobs.JobHandle AllocateNodesJob<T>(T[] result, int count, System.Func<T> createNode, uint variantsPerNode) where T : GraphNode { + // Get all node indices that we are going to recycle and store them in a new buffer. + // It's best to store them in a new buffer to avoid multithreading issues. + UnityEngine.Assertions.Assert.IsTrue(variantsPerNode > 0 && variantsPerNode <= 3); + + // It may be tempting to use a parallel job for this + // but it seems like allocation (new) in C# uses some kind of locking. + // Therefore it is not faster (it may even be slower) to try to allocate the nodes in multiple threads in parallel. + // The job will use locking internally for safety, but it's still nice to set appropriate dependencies, to avoid lots of worker threads + // just stalling because they are waiting for a lock, in case this method is called multiple times in parallel. + lastAllocationJob = new JobAllocateNodes<T> { + result = result, + count = count, + nodeStorage = this, + variantsPerNode = variantsPerNode, + createNode = createNode, + }.ScheduleManaged(lastAllocationJob); + + return lastAllocationJob; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GlobalNodeStorage.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GlobalNodeStorage.cs.meta new file mode 100644 index 0000000..c348e27 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GlobalNodeStorage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a659ff4e5955406479ba3f895ac8fca5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GraphUpdateProcessor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GraphUpdateProcessor.cs new file mode 100644 index 0000000..16d5290 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GraphUpdateProcessor.cs @@ -0,0 +1,274 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Pathfinding { + using Pathfinding.Jobs; + using Pathfinding.Util; + using Unity.Jobs; + using Unity.Profiling; + using UnityEngine.Assertions; + using UnityEngine.Profiling; + + /// <summary> + /// Promise representing a graph update. + /// + /// This is used internally by the system to represent a graph update. + /// Generally you shouldn't need to care about it, unless you are implementing your own graph type. + /// </summary> + public interface IGraphUpdatePromise { + /// <summary> + /// Returns the progress of the update. + /// + /// This should be a value between 0 and 1. + /// </summary> + float Progress => 0.0f; + + /// <summary> + /// Coroutine to prepare an update asynchronously. + /// + /// If a JobHandle is returned, it will be awaited before the coroutine is ticked again, and before the <see cref="Apply"/> method is called. + /// + /// After this coroutine has finished, the <see cref="Apply"/> method will be called. + /// + /// Note: No changes must be made to the graph in this method. Those should only be done in the <see cref="Apply"/> method. + /// + /// May return null if no async work is required. + /// </summary> + IEnumerator<JobHandle> Prepare() => null; + + /// <summary> + /// Applies the update in a single atomic update. + /// + /// It is done as a single atomic update (from the main thread's perspective) to ensure + /// that even if one does an async scan or update, the graph will always be in a valid state. + /// This guarantees that things like GetNearest will still work during an async scan. + /// + /// Warning: Must only be called after the <see cref="Prepare"/> method has finished. + /// </summary> + // TODO: Pass in a JobHandle and allow returning a JobHandle? + void Apply(IGraphUpdateContext context); + } + + /// <summary> + /// Helper functions for graph updates. + /// + /// A context is passed to graphs when they are updated, and to work items when they are executed. + /// The <see cref="IWorkItemContext"/> interface inherits from this interface. + /// </summary> + public interface IGraphUpdateContext { + /// <summary> + /// Mark a particular region of the world as having been changed. + /// + /// This should be used whenever graphs are changed. + /// + /// This is used to recalculate off-mesh links that touch these bounds, and it will also ensure <see cref="GraphModifier"/> events are callled. + /// + /// The bounding box should cover the surface of all nodes that have been updated. + /// It is fine to use a larger bounding box than necessary (even an infinite one), though this may be slower, since more off-mesh links need to be recalculated. + /// You can even use an infinitely large bounding box if you don't want to bother calculating a more accurate one. + /// You can also call this multiple times to dirty multiple bounding boxes. + /// </summary> + void DirtyBounds(Bounds bounds); + } + + class GraphUpdateProcessor { + /// <summary>Holds graphs that can be updated</summary> + readonly AstarPath astar; + + /// <summary>Used for IsAnyGraphUpdateInProgress</summary> + bool anyGraphUpdateInProgress; + + /// <summary> + /// Queue containing all waiting graph update queries. Add to this queue by using \link AddToQueue \endlink. + /// See: AddToQueue + /// </summary> + readonly Queue<GraphUpdateObject> graphUpdateQueue = new Queue<GraphUpdateObject>(); + readonly List<(IGraphUpdatePromise, IEnumerator<JobHandle>)> pendingPromises = new List<(IGraphUpdatePromise, IEnumerator<JobHandle>)>(); + readonly List<GraphUpdateObject> pendingGraphUpdates = new List<GraphUpdateObject>(); + + /// <summary>Returns if any graph updates are waiting to be applied</summary> + public bool IsAnyGraphUpdateQueued { get { return graphUpdateQueue.Count > 0; } } + + /// <summary>Returns if any graph updates are in progress</summary> + public bool IsAnyGraphUpdateInProgress { get { return anyGraphUpdateInProgress; } } + + public GraphUpdateProcessor (AstarPath astar) { + this.astar = astar; + } + + /// <summary>Work item which can be used to apply all queued updates</summary> + public AstarWorkItem GetWorkItem () { + return new AstarWorkItem(QueueGraphUpdatesInternal, ProcessGraphUpdates); + } + + /// <summary> + /// Update all graphs using the GraphUpdateObject. + /// This can be used to, e.g make all nodes in an area unwalkable, or set them to a higher penalty. + /// The graphs will be updated as soon as possible (with respect to AstarPath.batchGraphUpdates) + /// + /// See: FlushGraphUpdates + /// </summary> + public void AddToQueue (GraphUpdateObject ob) { + // Put the GUO in the queue + graphUpdateQueue.Enqueue(ob); + } + + /// <summary> + /// Discards all queued graph updates. + /// + /// Graph updates that are already in progress will not be discarded. + /// </summary> + public void DiscardQueued () { + while (graphUpdateQueue.Count > 0) { + graphUpdateQueue.Dequeue().internalStage = GraphUpdateObject.STAGE_ABORTED; + } + } + + /// <summary>Schedules graph updates internally</summary> + void QueueGraphUpdatesInternal (IWorkItemContext context) { + Assert.IsTrue(pendingGraphUpdates.Count == 0); + while (graphUpdateQueue.Count > 0) { + var ob = graphUpdateQueue.Dequeue(); + pendingGraphUpdates.Add(ob); + if (ob.internalStage != GraphUpdateObject.STAGE_PENDING) { + Debug.LogError("Expected remaining graph update to be pending"); + continue; + } + } + + foreach (IUpdatableGraph g in astar.data.GetUpdateableGraphs()) { + NavGraph gr = g as NavGraph; + var updates = ListPool<GraphUpdateObject>.Claim(); + for (int i = 0; i < pendingGraphUpdates.Count; i++) { + GraphUpdateObject ob = pendingGraphUpdates[i]; + if (ob.nnConstraint == null || ob.nnConstraint.SuitableGraph((int)gr.graphIndex, gr)) { + updates.Add(ob); + } + } + if (updates.Count > 0) { + var promise = g.ScheduleGraphUpdates(updates); + if (promise != null) { + var it = promise.Prepare(); + pendingPromises.Add((promise, it)); + } else { + ListPool<GraphUpdateObject>.Release(ref updates); + } + } else { + ListPool<GraphUpdateObject>.Release(ref updates); + } + } + + context.PreUpdate(); + anyGraphUpdateInProgress = true; + } + + static readonly ProfilerMarker MarkerSleep = new ProfilerMarker(ProfilerCategory.Loading, "Sleep"); + static readonly ProfilerMarker MarkerCalculate = new ProfilerMarker("Calculating Graph Update"); + static readonly ProfilerMarker MarkerApply = new ProfilerMarker("Applying Graph Update"); + + /// <summary> + /// Updates graphs. + /// Will do some graph updates, possibly signal another thread to do them. + /// Will only process graph updates added by QueueGraphUpdatesInternal + /// + /// Returns: True if all graph updates have been done and pathfinding (or other tasks) may resume. + /// False if there are still graph updates being processed or waiting in the queue. + /// </summary> + /// <param name="context">Helper methods for the work items.</param> + /// <param name="force">If true, all graph updates will be processed before this function returns. The return value + /// will be True.</param> + bool ProcessGraphUpdates (IWorkItemContext context, bool force) { + Assert.IsTrue(anyGraphUpdateInProgress); + + if (pendingPromises.Count > 0) { + if (!ProcessGraphUpdatePromises(pendingPromises, context, force)) { + return false; + } + + pendingPromises.Clear(); + } + + anyGraphUpdateInProgress = false; + for (int i = 0; i < pendingGraphUpdates.Count; i++) { + pendingGraphUpdates[i].internalStage = GraphUpdateObject.STAGE_APPLIED; + } + pendingGraphUpdates.Clear(); + return true; + } + + public static bool ProcessGraphUpdatePromises (List<(IGraphUpdatePromise, IEnumerator<JobHandle>)> promises, IGraphUpdateContext context, bool force = false) { + TimeSlice timeSlice = TimeSlice.MillisFromNow(2f); + TimeSlice idleTimeSlice = default; + + while (true) { + int firstNonFinished = -1; + bool anyMainThreadProgress = false; + for (int i = 0; i < promises.Count; i++) { + var(promise, it) = promises[i]; + if (it == null) continue; + if (force) { + it.Current.Complete(); + } else { + if (it.Current.IsCompleted) { + it.Current.Complete(); + } else { + if (firstNonFinished == -1) firstNonFinished = i; + continue; + } + } + + anyMainThreadProgress = true; + MarkerCalculate.Begin(); + try { + if (it.MoveNext()) { + if (firstNonFinished == -1) firstNonFinished = i; + } else promises[i] = (promise, null); + } catch (System.Exception e) { + Debug.LogError(new System.Exception("Error while updating graphs.", e)); + promises[i] = (null, null); + } + MarkerCalculate.End(); + } + + if (firstNonFinished == -1) { + break; + } else if (!force) { + if (timeSlice.expired) { + return false; + } else if (anyMainThreadProgress) { + // Reset the idle time slice if we got something done on the main thread. + // This allows us to wait on more very short jobs. + idleTimeSlice = TimeSlice.MillisFromNow(0.1f); + } else if (idleTimeSlice.expired) { + return false; + } else { + // Allow waiting for a short amount of time to allow very short running + // jobs in graph updates to complete without having to wait until the next frame. + // While waiting we release our thread's time slice to make sure other threads get priority. + if (!anyMainThreadProgress) { + MarkerSleep.Begin(); + System.Threading.Thread.Yield(); + MarkerSleep.End(); + } + } + } + } + + for (int i = 0; i < promises.Count; i++) { + var(promise, it) = promises[i]; + Assert.IsNull(it); + if (promise != null) { + MarkerApply.Begin(); + try { + promise.Apply(context); + } catch (System.Exception e) { + Debug.LogError(new System.Exception("Error while updating graphs.", e)); + } + MarkerApply.End(); + } + } + + return true; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GraphUpdateProcessor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GraphUpdateProcessor.cs.meta new file mode 100644 index 0000000..bd24da6 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/GraphUpdateProcessor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: b1798d8e7c7d54972ae8522558cbd27c +timeCreated: 1443114816 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HeuristicObjective.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HeuristicObjective.cs new file mode 100644 index 0000000..2a2cea4 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HeuristicObjective.cs @@ -0,0 +1,99 @@ +using Unity.Mathematics; +using Unity.Burst; +using Pathfinding.Util; +using Pathfinding.Graphs.Util; + +namespace Pathfinding { + /// <summary> + /// Calculates an estimated cost from the specified point to the target. + /// + /// See: https://en.wikipedia.org/wiki/A*_search_algorithm + /// </summary> + [BurstCompile] + public readonly struct HeuristicObjective { + readonly int3 mn; + readonly int3 mx; + readonly Heuristic heuristic; + readonly float heuristicScale; + readonly UnsafeSpan<uint> euclideanEmbeddingCosts; + readonly uint euclideanEmbeddingPivots; + readonly uint targetNodeIndex; + + public HeuristicObjective (int3 point, Heuristic heuristic, float heuristicScale) { + this.mn = this.mx = point; + this.heuristic = heuristic; + this.heuristicScale = heuristicScale; + this.euclideanEmbeddingCosts = default; + this.euclideanEmbeddingPivots = 0; + this.targetNodeIndex = 0; + } + + public HeuristicObjective (int3 point, Heuristic heuristic, float heuristicScale, uint targetNodeIndex, EuclideanEmbedding euclideanEmbedding) { + this.mn = this.mx = point; + this.heuristic = heuristic; + this.heuristicScale = heuristicScale; + // The euclidean embedding costs are guaranteed to be valid for the duration of the pathfinding request. + // We cannot perform checks here, because we may be running in another thread, and Unity does not like that. + this.euclideanEmbeddingCosts = euclideanEmbedding != null? euclideanEmbedding.costs.AsUnsafeSpanNoChecks() : default; + this.euclideanEmbeddingPivots = euclideanEmbedding != null ? (uint)euclideanEmbedding.pivotCount : 0; + this.targetNodeIndex = targetNodeIndex; + } + + public HeuristicObjective (int3 mn, int3 mx, Heuristic heuristic, float heuristicScale, uint targetNodeIndex, EuclideanEmbedding euclideanEmbedding) { + this.mn = mn; + this.mx = mx; + this.heuristic = heuristic; + this.heuristicScale = heuristicScale; + // The euclidean embedding costs are guaranteed to be valid for the duration of the pathfinding request. + // We cannot perform checks here, because we may be running in another thread, and Unity does not like that. + this.euclideanEmbeddingCosts = euclideanEmbedding != null? euclideanEmbedding.costs.AsUnsafeSpanNoChecks() : default; + this.euclideanEmbeddingPivots = euclideanEmbedding != null ? (uint)euclideanEmbedding.pivotCount : 0; + this.targetNodeIndex = targetNodeIndex; + } + + public int Calculate (int3 point, uint nodeIndex) { + return Calculate(in this, ref point, nodeIndex); + } + + [BurstCompile] + public static int Calculate (in HeuristicObjective objective, ref int3 point, uint nodeIndex) { + var closest = math.clamp(point, objective.mn, objective.mx); + var diff = point - closest; + + int h; + switch (objective.heuristic) { + case Heuristic.Euclidean: + h = (int)(math.length((float3)diff) * objective.heuristicScale); + break; + case Heuristic.Manhattan: + h = (int)(math.csum(math.abs(diff)) * objective.heuristicScale); + break; + case Heuristic.DiagonalManhattan: + // Octile distance extended to 3D + diff = math.abs(diff); + var a = diff.x; + var b = diff.y; + var c = diff.z; + // Sort the values so that a <= b <= c + if (a > b) Memory.Swap(ref a, ref b); + if (b > c) Memory.Swap(ref b, ref c); + if (a > b) Memory.Swap(ref a, ref b); + + // This is the same as the Manhattan distance, but with a different weight for the diagonal moves. + const float SQRT_3 = 1.7321f; + const float SQRT_2 = 1.4142f; + h = (int)(objective.heuristicScale * (SQRT_3 * a + SQRT_2 * (b-a) + (c-b-a))); + break; + case Heuristic.None: + default: + h = 0; + break; + } + + if (objective.euclideanEmbeddingPivots > 0) { + h = math.max(h, (int)EuclideanEmbedding.GetHeuristic(objective.euclideanEmbeddingCosts, objective.euclideanEmbeddingPivots, nodeIndex, objective.targetNodeIndex)); + } + return h; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HeuristicObjective.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HeuristicObjective.cs.meta new file mode 100644 index 0000000..df7cd07 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HeuristicObjective.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7f83de4e1bae824c801f70be70d1d5d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HierarchicalGraph.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HierarchicalGraph.cs new file mode 100644 index 0000000..73a580d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HierarchicalGraph.cs @@ -0,0 +1,604 @@ +// #define CHECK_INVARIANTS +using System.Collections.Generic; +using Pathfinding.Util; +using UnityEngine; +using Unity.Jobs; +using UnityEngine.Profiling; +using UnityEngine.Assertions; + +namespace Pathfinding { + using System.Runtime.InteropServices; + using Pathfinding.Drawing; + using Pathfinding.Jobs; + using Unity.Collections; + using Unity.Collections.LowLevel.Unsafe; + using Unity.Mathematics; + + /// <summary> + /// Holds a hierarchical graph to speed up certain pathfinding queries. + /// + /// A common type of query that needs to be very fast is on the form 'is this node reachable from this other node'. + /// This is for example used when picking the end node of a path. The end node is determined as the closest node to the end point + /// that can be reached from the start node. + /// + /// This data structure's primary purpose is to keep track of which connected component each node is contained in, in order to make such queries fast. + /// + /// See: https://en.wikipedia.org/wiki/Connected_component_(graph_theory) + /// + /// A connected component is a set of nodes such that there is a valid path between every pair of nodes in that set. + /// Thus the query above can simply be answered by checking if they are in the same connected component. + /// The connected component is exposed on nodes as the <see cref="Pathfinding.GraphNode.Area"/> property and on this class using the <see cref="GetConnectedComponent"/> method. + /// + /// Note: This class does not calculate strictly connected components. In case of one-way connections, it will still consider the nodes to be in the same connected component. + /// + /// In the image below (showing a 200x200 grid graph) each connected component is colored using a separate color. + /// The actual color doesn't signify anything in particular however, only that they are different. + /// [Open online documentation to see images] + /// + /// Prior to version 4.2 the connected components were just a number stored on each node, and when a graph was updated + /// the connected components were completely recalculated. This can be done relatively efficiently using a flood filling + /// algorithm (see https://en.wikipedia.org/wiki/Flood_fill) however it still requires a pass through every single node + /// which can be quite costly on larger graphs. + /// + /// This class instead builds a much smaller graph that still respects the same connectivity as the original graph. + /// Each node in this hierarchical graph represents a larger number of real nodes that are one single connected component. + /// Take a look at the image below for an example. In the image each color is a separate hierarchical node, and the black connections go between the center of each hierarchical node. + /// + /// [Open online documentation to see images] + /// + /// With the hierarchical graph, the connected components can be calculated by flood filling the hierarchical graph instead of the real graph. + /// Then when we need to know which connected component a node belongs to, we look up the connected component of the hierarchical node the node belongs to. + /// + /// The benefit is not immediately obvious. The above is just a bit more complicated way to accomplish the same thing. However the real benefit comes when updating the graph. + /// When the graph is updated, all hierarchical nodes which contain any node that was affected by the update is removed completely and then once all have been removed new hierarchical nodes are recalculated in their place. + /// Once this is done the connected components of the whole graph can be updated by flood filling only the hierarchical graph. Since the hierarchical graph is vastly smaller than the real graph, this is significantly faster. + /// + /// [Open online documentation to see videos] + /// + /// So finally using all of this, the connected components of the graph can be recalculated very quickly as the graph is updated. + /// The effect of this grows larger the larger the graph is, and the smaller the graph update is. Making a small update to a 1000x1000 grid graph is on the order of 40 times faster with these optimizations. + /// When scanning a graph or making updates to the whole graph at the same time there is however no speed boost. In fact due to the extra complexity it is a bit slower, however after profiling the extra time seems to be mostly insignificant compared to the rest of the cost of scanning the graph. + /// + /// [Open online documentation to see videos] + /// + /// See: <see cref="Pathfinding.PathUtilities.IsPathPossible"/> + /// See: <see cref="Pathfinding.NNConstraint"/> + /// See: <see cref="Pathfinding.GraphNode.Area"/> + /// </summary> + public class HierarchicalGraph { + const int Tiling = 16; + const int MaxChildrenPerNode = Tiling * Tiling; + const int MinChildrenPerNode = MaxChildrenPerNode/2; + + GlobalNodeStorage nodeStorage; + internal List<GraphNode>[] children; + internal NativeList<int> connectionAllocations; + internal SlabAllocator<int> connectionAllocator; + NativeList<int> dirtiedHierarchicalNodes; + int[] areas; + byte[] dirty; + int[] versions; + internal NativeList<Bounds> bounds; + /// <summary>Holds areas.Length as a burst-accessible reference</summary> + NativeReference<int> numHierarchicalNodes; + internal GCHandle gcHandle; + + public int version { get; private set; } + public NavmeshEdges navmeshEdges; + + Queue<GraphNode> temporaryQueue = new Queue<GraphNode>(); + List<int> currentConnections = new List<int>(); + Stack<int> temporaryStack = new Stack<int>(); + + HierarchicalBitset dirtyNodes; + + CircularBuffer<int> freeNodeIndices; + + int gizmoVersion = 0; + + RWLock rwLock = new RWLock(); + + /// <summary> + /// Disposes of all unmanaged data and clears managed data. + /// + /// If you want to use this instance again, you must call <see cref="OnEnable"/>. + /// </summary> + internal void OnDisable () { + rwLock.WriteSync().Unlock(); + navmeshEdges.Dispose(); + if (gcHandle.IsAllocated) gcHandle.Free(); + if (connectionAllocator.IsCreated) { + numHierarchicalNodes.Dispose(); + connectionAllocator.Dispose(); + connectionAllocations.Dispose(); + bounds.Dispose(); + dirtiedHierarchicalNodes.Dispose(); + dirtyNodes.Dispose(); + + children = null; + areas = null; + dirty = null; + versions = null; + freeNodeIndices.Clear(); + } + } + + // Make methods internal + public int GetHierarchicalNodeVersion (int index) { + return (index * 71237) ^ versions[index]; + } + + /// <summary>Burst-accessible data about the hierarhical nodes</summary> + public struct HierarhicalNodeData { + [Unity.Collections.ReadOnly] + public SlabAllocator<int> connectionAllocator; + [Unity.Collections.ReadOnly] + public NativeList<int> connectionAllocations; + [Unity.Collections.ReadOnly] + public NativeList<Bounds> bounds; + } + + /// <summary> + /// Data about the hierarhical nodes. + /// + /// Can be accessed in burst jobs. + /// </summary> + public HierarhicalNodeData GetHierarhicalNodeData (out RWLock.ReadLockAsync readLock) { + readLock = rwLock.Read(); + return new HierarhicalNodeData { + connectionAllocator = connectionAllocator, + connectionAllocations = connectionAllocations, + bounds = bounds, + }; + } + + internal HierarchicalGraph (GlobalNodeStorage nodeStorage) { + this.nodeStorage = nodeStorage; + navmeshEdges = new NavmeshEdges(); + navmeshEdges.hierarchicalGraph = this; + } + + /// <summary> + /// Initializes the HierarchicalGraph data. + /// It is safe to call this multiple times even if it has already been enabled before. + /// </summary> + public void OnEnable () { + if (!connectionAllocator.IsCreated) { + gcHandle = GCHandle.Alloc(this); + + connectionAllocator = new SlabAllocator<int>(1024, Allocator.Persistent); + connectionAllocations = new NativeList<int>(0, Allocator.Persistent); + bounds = new NativeList<Bounds>(0, Allocator.Persistent); + numHierarchicalNodes = new NativeReference<int>(0, Allocator.Persistent); + dirtiedHierarchicalNodes = new NativeList<int>(0, Allocator.Persistent); + dirtyNodes = new HierarchicalBitset(1024, Allocator.Persistent); + + children = new List<GraphNode>[1] { new List<GraphNode>() }; + areas = new int[1]; + dirty = new byte[1]; + versions = new int[1]; + freeNodeIndices.Clear(); + } + } + + internal void OnCreatedNode (GraphNode node) { + AddDirtyNode(node); + } + + internal void OnDestroyedNode (GraphNode node) { + dirty[node.HierarchicalNodeIndex] = 1; + dirtyNodes.Reset((int)node.NodeIndex); + node.IsHierarchicalNodeDirty = false; + } + + /// <summary> + /// Marks this node as dirty because it's connectivity or walkability has changed. + /// This must be called by node classes after any connectivity/walkability changes have been made to them. + /// + /// See: <see cref="GraphNode.SetConnectivityDirty"/> + /// </summary> + public void AddDirtyNode (GraphNode node) { + if (!node.IsHierarchicalNodeDirty) { + // We may be calling this when shutting down + if (!dirtyNodes.IsCreated || node.Destroyed) return; + + dirtyNodes.Set((int)node.NodeIndex); + + // Mark the associated hierarchical node as dirty to ensure it is recalculated or removed later. + // Nodes that have been unwalkable since the last update, will have HierarchicalNodeIndex=0, which is a dummy hierarchical node that is never used. + dirty[node.HierarchicalNodeIndex] = 1; + node.IsHierarchicalNodeDirty = true; + } + } + + public void ReserveNodeIndices (uint nodeIndexCount) { + dirtyNodes.Capacity = Mathf.Max(dirtyNodes.Capacity, (int)nodeIndexCount); + } + + public int NumConnectedComponents { get; private set; } + + /// <summary>Get the connected component index of a hierarchical node</summary> + public uint GetConnectedComponent (int hierarchicalNodeIndex) { + return (uint)areas[hierarchicalNodeIndex]; + } + + struct JobRecalculateComponents : IJob { + public System.Runtime.InteropServices.GCHandle hGraphGC; + public NativeList<int> connectionAllocations; + public NativeList<Bounds> bounds; + public NativeList<int> dirtiedHierarchicalNodes; + public NativeReference<int> numHierarchicalNodes; + + void Grow (HierarchicalGraph graph) { + var newChildren = new List<GraphNode>[System.Math.Max(64, graph.children.Length*2)]; + var newAreas = new int[newChildren.Length]; + var newDirty = new byte[newChildren.Length]; + var newVersions = new int[newChildren.Length]; + numHierarchicalNodes.Value = newChildren.Length; + + graph.children.CopyTo(newChildren, 0); + graph.areas.CopyTo(newAreas, 0); + graph.dirty.CopyTo(newDirty, 0); + graph.versions.CopyTo(newVersions, 0); + bounds.Resize(newChildren.Length, NativeArrayOptions.UninitializedMemory); + connectionAllocations.Resize(newChildren.Length, NativeArrayOptions.ClearMemory); + + // Create all necessary lists for the new nodes + // Iterate in reverse so that when popping from the freeNodeIndices + // stack we get numbers from smallest to largest (this is not + // necessary, but it makes the starting colors be a bit nicer when + // visualized in the scene view). + for (int i = newChildren.Length - 1; i >= graph.children.Length; i--) { + newChildren[i] = ListPool<GraphNode>.Claim(MaxChildrenPerNode); + connectionAllocations[i] = SlabAllocator<int>.InvalidAllocation; + if (i > 0) graph.freeNodeIndices.PushEnd(i); + } + connectionAllocations[0] = SlabAllocator<int>.InvalidAllocation; + + graph.children = newChildren; + graph.areas = newAreas; + graph.dirty = newDirty; + graph.versions = newVersions; + } + + int GetHierarchicalNodeIndex (HierarchicalGraph graph) { + if (graph.freeNodeIndices.Length == 0) Grow(graph); + return graph.freeNodeIndices.PopEnd(); + } + + void RemoveHierarchicalNode (HierarchicalGraph hGraph, int hierarchicalNode, bool removeAdjacentSmallNodes) { + Assert.AreNotEqual(hierarchicalNode, 0); + hGraph.freeNodeIndices.PushEnd(hierarchicalNode); + hGraph.versions[hierarchicalNode]++; + var connAllocation = connectionAllocations[hierarchicalNode]; + var conns = hGraph.connectionAllocator.GetSpan(connAllocation); + + for (int i = 0; i < conns.Length; i++) { + var adjacentHierarchicalNode = conns[i]; + // If dirty, this hierarchical node will be removed later anyway, so don't bother doing anything with it. + if (hGraph.dirty[adjacentHierarchicalNode] != 0) continue; + + if (removeAdjacentSmallNodes && hGraph.children[adjacentHierarchicalNode].Count < MinChildrenPerNode) { + hGraph.dirty[adjacentHierarchicalNode] = 2; + RemoveHierarchicalNode(hGraph, adjacentHierarchicalNode, false); + + // The connection list may have been reallocated, so we need to get it again + conns = hGraph.connectionAllocator.GetSpan(connAllocation); + } else { + // Remove the connection from the other node to this node as we are removing this node. + var otherConnections = hGraph.connectionAllocator.GetList(connectionAllocations[adjacentHierarchicalNode]); + otherConnections.Remove(hierarchicalNode); + // Update the allocation index of the list, in case it was reallocated + connectionAllocations[adjacentHierarchicalNode] = otherConnections.allocationIndex; + } + } + Assert.AreEqual(connectionAllocations[hierarchicalNode], connAllocation); + + hGraph.connectionAllocator.Free(connAllocation); + connectionAllocations[hierarchicalNode] = SlabAllocator<int>.InvalidAllocation; + + var nodeChildren = hGraph.children[hierarchicalNode]; + + // Ensure all children of dirty hierarchical nodes are included in the recalculation + var preDirty = hGraph.dirty[hierarchicalNode]; + for (int i = 0; i < nodeChildren.Count; i++) { + if (!nodeChildren[i].Destroyed) hGraph.AddDirtyNode(nodeChildren[i]); + } + // Put the dirty flag back to what it was before, as it might have been set to 1 by the AddDirtyNode call + hGraph.dirty[hierarchicalNode] = preDirty; + + nodeChildren.ClearFast(); + } + + [System.Diagnostics.Conditional("CHECK_INVARIANTS")] + void CheckConnectionInvariants () { + var hGraph = (HierarchicalGraph)hGraphGC.Target; + if (connectionAllocations.Length > 0) Assert.AreEqual(connectionAllocations[0], SlabAllocator<int>.InvalidAllocation); + for (int i = 0; i < connectionAllocations.Length; i++) { + if (connectionAllocations[i] != SlabAllocator<int>.InvalidAllocation) { + var conns = hGraph.connectionAllocator.GetSpan(connectionAllocations[i]); + for (int j = 0; j < conns.Length; j++) { + Assert.IsFalse(connectionAllocations[conns[j]] == SlabAllocator<int>.InvalidAllocation, "Invalid connection allocation"); + var otherConns = hGraph.connectionAllocator.GetSpan(connectionAllocations[conns[j]]); + if (!otherConns.Contains(i)) { + throw new System.Exception("Connections are not bidirectional"); + } + } + } + } + } + + [System.Diagnostics.Conditional("CHECK_INVARIANTS")] + void CheckPreUpdateInvariants () { + var hGraph = (HierarchicalGraph)hGraphGC.Target; + + if (connectionAllocations.Length > 0) Assert.AreEqual(connectionAllocations[0], SlabAllocator<int>.InvalidAllocation); + for (int i = 0; i < connectionAllocations.Length; i++) { + if (connectionAllocations[i] != SlabAllocator<int>.InvalidAllocation) { + var children = hGraph.children[i]; + for (int j = 0; j < children.Count; j++) { + if (!children[j].Destroyed) { + Assert.AreEqual(children[j].HierarchicalNodeIndex, i); + } + } + } + } + } + + [System.Diagnostics.Conditional("CHECK_INVARIANTS")] + void CheckChildInvariants () { + var hGraph = (HierarchicalGraph)hGraphGC.Target; + + if (connectionAllocations.Length > 0) Assert.AreEqual(connectionAllocations[0], SlabAllocator<int>.InvalidAllocation); + for (int i = 0; i < connectionAllocations.Length; i++) { + if (connectionAllocations[i] != SlabAllocator<int>.InvalidAllocation) { + var children = hGraph.children[i]; + for (int j = 0; j < children.Count; j++) { + Assert.IsFalse(children[j].Destroyed); + Assert.AreEqual(children[j].HierarchicalNodeIndex, i); + } + } + } + } + + struct Context { + public List<GraphNode> children; + public int hierarchicalNodeIndex; + public List<int> connections; + public uint graphindex; + public Queue<GraphNode> queue; + } + + + /// <summary>Run a BFS out from a start node and assign up to MaxChildrenPerNode nodes to the specified hierarchical node which are not already assigned to another hierarchical node</summary> + void FindHierarchicalNodeChildren (HierarchicalGraph hGraph, int hierarchicalNode, GraphNode startNode) { + Assert.AreNotEqual(hierarchicalNode, 0); + Assert.AreEqual(hGraph.children[hierarchicalNode].Count, 0); + hGraph.versions[hierarchicalNode]++; + + // We create a context and pass that by reference to the GetConnections method. + // This allows us to pass a non-capturing delegate, which does not require a heap allocation. + var queue = hGraph.temporaryQueue; + var context = new Context { + children = hGraph.children[hierarchicalNode], + hierarchicalNodeIndex = hierarchicalNode, + connections = hGraph.currentConnections, + graphindex = startNode.GraphIndex, + queue = queue, + }; + context.connections.Clear(); + context.children.Add(startNode); + context.queue.Enqueue(startNode); + startNode.HierarchicalNodeIndex = hierarchicalNode; + + GraphNode.GetConnectionsWithData<Context> visitConnection = (GraphNode neighbour, ref Context context) => { + if (neighbour.Destroyed) { + throw new System.InvalidOperationException("A node in a " + AstarPath.active.graphs[context.graphindex].GetType().Name + " contained a connection to a destroyed " + neighbour.GetType().Name + "."); + } + var hIndex = neighbour.HierarchicalNodeIndex; + if (hIndex == 0) { + if (context.children.Count < MaxChildrenPerNode && neighbour.Walkable && neighbour.GraphIndex == context.graphindex /* && (((GridNode)currentChildren[0]).XCoordinateInGrid/Tiling == ((GridNode)neighbour).XCoordinateInGrid/Tiling) && (((GridNode)currentChildren[0]).ZCoordinateInGrid/Tiling == ((GridNode)neighbour).ZCoordinateInGrid/Tiling)*/) { + neighbour.HierarchicalNodeIndex = context.hierarchicalNodeIndex; + context.queue.Enqueue(neighbour); + context.children.Add(neighbour); + } + } else if (hIndex != context.hierarchicalNodeIndex && !context.connections.Contains(hIndex)) { + // The Contains call can in theory be very slow as an hierarchical node may be adjacent to an arbitrary number of nodes. + // However in practice due to how the hierarchical nodes are constructed they will only be adjacent to a smallish (≈4-6) number of other nodes. + // So a Contains call will be much faster than say a Set lookup. + context.connections.Add(hIndex); + } + }; + + while (queue.Count > 0) queue.Dequeue().GetConnections(visitConnection, ref context, Connection.IncomingConnection | Connection.OutgoingConnection); + + for (int i = 0; i < hGraph.currentConnections.Count; i++) { + var otherHierarchicalNode = hGraph.currentConnections[i]; + Assert.AreNotEqual(otherHierarchicalNode, 0); + var otherAllocationIndex = connectionAllocations[otherHierarchicalNode]; + Assert.AreNotEqual(otherAllocationIndex, SlabAllocator<int>.InvalidAllocation); + var otherConnections = hGraph.connectionAllocator.GetList(otherAllocationIndex); + otherConnections.Add(hierarchicalNode); + // Update the allocation index in case the list was reallocated + connectionAllocations[otherHierarchicalNode] = otherConnections.allocationIndex; + } + + connectionAllocations[hierarchicalNode] = hGraph.connectionAllocator.Allocate(hGraph.currentConnections); + queue.Clear(); + } + + /// <summary>Flood fills the graph of hierarchical nodes and assigns the same area ID to all hierarchical nodes that are in the same connected component</summary> + void FloodFill (HierarchicalGraph hGraph) { + var areas = hGraph.areas; + for (int i = 0; i < areas.Length; i++) areas[i] = 0; + + Stack<int> stack = hGraph.temporaryStack; + int currentArea = 0; + for (int i = 1; i < areas.Length; i++) { + // Already taken care of, or does not exist + if (areas[i] != 0 || connectionAllocations[i] == SlabAllocator<int>.InvalidAllocation) continue; + + currentArea++; + areas[i] = currentArea; + stack.Push(i); + while (stack.Count > 0) { + int node = stack.Pop(); + var conns = hGraph.connectionAllocator.GetSpan(connectionAllocations[node]); + for (int j = conns.Length - 1; j >= 0; j--) { + var otherNode = conns[j]; + // Note: slightly important that this is != currentArea and not != 0 in case there are some connected, but not stongly connected components in the graph (this will happen in only veeery few types of games) + if (areas[otherNode] != currentArea) { + areas[otherNode] = currentArea; + stack.Push(otherNode); + } + } + } + } + + hGraph.NumConnectedComponents = System.Math.Max(1, currentArea + 1); + hGraph.version++; + } + + public void Execute () { + var hGraph = hGraphGC.Target as HierarchicalGraph; + CheckPreUpdateInvariants(); + Profiler.BeginSample("Recalculate Connected Components"); + var dirty = hGraph.dirty; + CheckConnectionInvariants(); + + Profiler.BeginSample("Remove"); + // Remove all hierarchical nodes and then build new hierarchical nodes in their place + // which take into account the new graph data. + var initialFreeLength = hGraph.freeNodeIndices.Length; + for (int i = 1; i < dirty.Length; i++) { + if (dirty[i] == 1) RemoveHierarchicalNode(hGraph, i, true); + } + + // Reset the dirty flag on all hierarchical nodes + for (int i = 1; i < dirty.Length; i++) dirty[i] = 0; + + // Reset the dirty flag on all nodes, and make sure they don't refer to their new destroyed hierarchical nodes + var buffer = new NativeArray<int>(512, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + var nodeStorage = hGraph.nodeStorage; + foreach (var span in hGraph.dirtyNodes.GetIterator(buffer.AsUnsafeSpan())) { + for (int i = 0; i < span.Length; i++) { + var node = nodeStorage.GetNode((uint)span[i]); + node.IsHierarchicalNodeDirty = false; + node.HierarchicalNodeIndex = 0; + } + } + + Profiler.EndSample(); + CheckConnectionInvariants(); + + Profiler.BeginSample("Find"); + dirtiedHierarchicalNodes.Clear(); + foreach (var span in hGraph.dirtyNodes.GetIterator(buffer.AsUnsafeSpan())) { + for (int i = 0; i < span.Length; i++) { + var node = nodeStorage.GetNode((uint)span[i]); + + Assert.IsFalse(node.Destroyed); + if (!node.Destroyed && node.HierarchicalNodeIndex == 0 && node.Walkable) { + var hNode = GetHierarchicalNodeIndex(hGraph); + Profiler.BeginSample("FindChildren"); + FindHierarchicalNodeChildren(hGraph, hNode, node); + Profiler.EndSample(); + dirtiedHierarchicalNodes.Add(hNode); + } + } + } + + // These are hierarchical node indices that were pushed to the free id stack, and we did not immediately reuse them. + // This means that they have been destroyed, and we should notify the NavmeshEdges class about this. + for (int i = initialFreeLength; i < hGraph.freeNodeIndices.Length; i++) { + dirtiedHierarchicalNodes.Add(hGraph.freeNodeIndices[i]); + } + + hGraph.dirtyNodes.Clear(); + + // Recalculate the connected components of the hierarchical nodes + // This is usually very quick compared to the code above + FloodFill(hGraph); + Profiler.EndSample(); + hGraph.gizmoVersion++; + CheckConnectionInvariants(); + CheckChildInvariants(); + } + } + + /// <summary>Recalculate the hierarchical graph and the connected components if any nodes have been marked as dirty</summary> + public void RecalculateIfNecessary () { + // We need to complete both jobs here, because after this method + // the graph may change in arbitrary ways. The RecalculateObstacles job reads from the graph. + JobRecalculateIfNecessary().Complete(); + } + + /// <summary> + /// Schedule a job to recalculate the hierarchical graph and the connected components if any nodes have been marked as dirty. + /// Returns dependsOn if nothing has to be done. + /// + /// Note: Assumes the graph is unchanged until the returned dependency is completed. + /// </summary> + public JobHandle JobRecalculateIfNecessary (JobHandle dependsOn = default) { + OnEnable(); + if (!dirtyNodes.IsEmpty) { + var writeLock = rwLock.Write(); + var lastJob = new JobRecalculateComponents { + hGraphGC = gcHandle, + connectionAllocations = connectionAllocations, + bounds = bounds, + dirtiedHierarchicalNodes = dirtiedHierarchicalNodes, + numHierarchicalNodes = numHierarchicalNodes, + }.Schedule(JobHandle.CombineDependencies(writeLock.dependency, dependsOn)); + // We need to output both jobs as dependencies. + // Firstly they use some internal data (e.g. dirtiedHierarchicalNodes), so we need to set lastJob. + // Secondly, they read from the graph. And the graph data is only read-only until this returned dependency is completed. + lastJob = navmeshEdges.RecalculateObstacles(dirtiedHierarchicalNodes, numHierarchicalNodes, lastJob); + writeLock.UnlockAfter(lastJob); + return lastJob; + } else { + return dependsOn; + } + } + + /// <summary> + /// Recalculate everything from scratch. + /// This is primarily to be used for legacy code for compatibility reasons, not for any new code. + /// + /// See: <see cref="RecalculateIfNecessary"/> + /// </summary> + public void RecalculateAll () { + var writeLock = rwLock.WriteSync(); + AstarPath.active.data.GetNodes(AddDirtyNode); + writeLock.Unlock(); + RecalculateIfNecessary(); + } + + public void OnDrawGizmos (DrawingData gizmos, RedrawScope redrawScope) { + var hasher = new NodeHasher(AstarPath.active); + + hasher.Add(gizmoVersion); + + if (!gizmos.Draw(hasher, redrawScope)) { + var readLock = rwLock.ReadSync(); + try { + using (var builder = gizmos.GetBuilder(hasher, redrawScope)) { + for (int i = 0; i < areas.Length; i++) { + if (children[i].Count > 0) { + builder.WireBox(bounds[i].center, bounds[i].size); + var conns = connectionAllocator.GetSpan(connectionAllocations[i]); + for (int j = 0; j < conns.Length; j++) { + if (conns[j] > i) { + builder.Line(bounds[i].center, bounds[conns[j]].center, Color.black); + } + } + } + } + } + } finally { + readLock.Unlock(); + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HierarchicalGraph.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HierarchicalGraph.cs.meta new file mode 100644 index 0000000..e8db5b3 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/HierarchicalGraph.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: aa5d6e4a6adb04d4ca9b7e735addcbd5 +timeCreated: 1535050640 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/ITraversalProvider.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/ITraversalProvider.cs new file mode 100644 index 0000000..249077b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/ITraversalProvider.cs @@ -0,0 +1,105 @@ +namespace Pathfinding { + /// <summary> + /// Provides additional traversal information to a path request. + /// + /// Example implementation: + /// <code> + /// public class MyCustomTraversalProvider : ITraversalProvider { + /// public bool CanTraverse (Path path, GraphNode node) { + /// // Make sure that the node is walkable and that the 'enabledTags' bitmask + /// // includes the node's tag. + /// return node.Walkable && (path.enabledTags >> (int)node.Tag & 0x1) != 0; + /// // alternatively: + /// // return DefaultITraversalProvider.CanTraverse(path, node); + /// } + /// + /// /** [CanTraverseDefault] */ + /// public bool CanTraverse (Path path, GraphNode from, GraphNode to) { + /// return CanTraverse(path, to); + /// } + /// /** [CanTraverseDefault] */ + /// + /// public uint GetTraversalCost (Path path, GraphNode node) { + /// // The traversal cost is the sum of the penalty of the node's tag and the node's penalty + /// return path.GetTagPenalty((int)node.Tag) + node.Penalty; + /// // alternatively: + /// // return DefaultITraversalProvider.GetTraversalCost(path, node); + /// } + /// + /// // This can be omitted in Unity 2021.3 and newer because a default implementation (returning true) can be used there. + /// public bool filterDiagonalGridConnections { + /// get { + /// return true; + /// } + /// } + /// } + /// </code> + /// + /// See: traversal_provider (view in online documentation for working links) + /// </summary> + public interface ITraversalProvider { + /// <summary> + /// Filter diagonal connections using <see cref="GridGraph.cutCorners"/> for effects applied by this ITraversalProvider. + /// This includes tags and other effects that this ITraversalProvider controls. + /// + /// This only has an effect if <see cref="GridGraph.cutCorners"/> is set to false and your grid has <see cref="GridGraph.neighbours"/> set to Eight. + /// + /// Take this example, the grid is completely walkable, but an ITraversalProvider is used to make the nodes marked with '#' + /// as unwalkable. The agent 'S' is in the middle. + /// + /// <code> + /// .......... + /// ....#..... + /// ...<see cref="S"/>#.... + /// ....#..... + /// .......... + /// </code> + /// + /// If filterDiagonalGridConnections is false the agent will be free to use the diagonal connections to move away from that spot. + /// However, if filterDiagonalGridConnections is true (the default) then the diagonal connections will be disabled and the agent will be stuck. + /// + /// Typically, there are a few common use cases: + /// - If your ITraversalProvider makes walls and obstacles and you want it to behave identically to obstacles included in the original grid graph scan, then this should be true. + /// - If your ITraversalProvider is used for agent to agent avoidance and you want them to be able to move around each other more freely, then this should be false. + /// + /// See: <see cref="GridNode"/> + /// </summary> + bool filterDiagonalGridConnections => true; + + /// <summary>True if node should be able to be traversed by the path.</summary> + bool CanTraverse(Path path, GraphNode node) => DefaultITraversalProvider.CanTraverse(path, node); + + /// <summary> + /// True if the path can traverse a link between from and to and if to can be traversed itself. + /// If this method returns true then a call to CanTraverse(path,to) must also return true. + /// Thus this method is a more flexible version of <see cref="CanTraverse(Path,GraphNode)"/>. + /// + /// If you only need the functionality for <see cref="CanTraverse(Path,GraphNode)"/> then you may implement this method by just forwarding it to <see cref="CanTraverse(Path,GraphNode)"/> + /// + /// <code> + /// public bool CanTraverse (Path path, GraphNode from, GraphNode to) { + /// return CanTraverse(path, to); + /// } + /// </code> + /// </summary> + bool CanTraverse(Path path, GraphNode from, GraphNode to) => CanTraverse(path, to); + + /// <summary> + /// Cost of traversing a given node. + /// Should return the additional cost for traversing this node. By default if no tags or penalties + /// are used then the traversal cost is zero. A cost of 1000 corresponds roughly to the cost of moving 1 world unit. + /// </summary> + uint GetTraversalCost(Path path, GraphNode node) => DefaultITraversalProvider.GetTraversalCost(path, node); + } + + /// <summary>Convenience class to access the default implementation of the ITraversalProvider</summary> + public static class DefaultITraversalProvider { + public static bool CanTraverse (Path path, GraphNode node) { + return node.Walkable && (path == null || (path.enabledTags >> (int)node.Tag & 0x1) != 0); + } + + public static uint GetTraversalCost (Path path, GraphNode node) { + return node.Penalty + (path != null ? path.GetTagPenalty((int)node.Tag) : 0); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/ITraversalProvider.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/ITraversalProvider.cs.meta new file mode 100644 index 0000000..c85b200 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/ITraversalProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb11e2e7634f01646a8d62ad94c75291 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs new file mode 100644 index 0000000..51f5975 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs @@ -0,0 +1,1095 @@ +//#define ASTAR_POOL_DEBUG //@SHOWINEDITOR Enables debugging of path pooling. Will log warnings and info messages about paths not beeing pooled correctly. + +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using Unity.Mathematics; + +namespace Pathfinding { + /// <summary>Base class for all path types</summary> + [Unity.Burst.BurstCompile] + public abstract class Path : IPathInternals { +#if ASTAR_POOL_DEBUG + private string pathTraceInfo = ""; + private List<string> claimInfo = new List<string>(); + ~Path() { + Debug.Log("Destroying " + GetType().Name + " instance"); + if (claimed.Count > 0) { + Debug.LogWarning("Pool Is Leaking. See list of claims:\n" + + "Each message below will list what objects are currently claiming the path." + + " These objects have removed their reference to the path object but has not called .Release on it (which is bad).\n" + pathTraceInfo+"\n"); + for (int i = 0; i < claimed.Count; i++) { + Debug.LogWarning("- Claim "+ (i+1) + " is by a " + claimed[i].GetType().Name + "\n"+claimInfo[i]); + } + } else { + Debug.Log("Some scripts are not using pooling.\n" + pathTraceInfo + "\n"); + } + } +#endif + + /// <summary>Data for the thread calculating this path</summary> + protected PathHandler pathHandler; + + /// <summary> + /// Callback to call when the path is complete. + /// This is usually sent to the Seeker component which post processes the path and then calls a callback to the script which requested the path + /// </summary> + public OnPathDelegate callback; + + /// <summary> + /// Immediate callback to call when the path is complete. + /// Warning: This may be called from a separate thread. Usually you do not want to use this one. + /// + /// See: callback + /// </summary> + public OnPathDelegate immediateCallback; + + /// <summary>Returns the state of the path in the pathfinding pipeline</summary> + public PathState PipelineState { get; private set; } + + /// <summary> + /// Provides additional traversal information to a path request. + /// See: traversal_provider (view in online documentation for working links) + /// </summary> + public ITraversalProvider traversalProvider; + + + /// <summary>Backing field for <see cref="CompleteState"/></summary> + protected PathCompleteState completeState; + + /// <summary> + /// Current state of the path. + /// Bug: This may currently be set to Complete before the path has actually been fully calculated. In particular the vectorPath and path lists may not have been fully constructed. + /// This can lead to race conditions when using multithreading. Try to avoid using this method to check for if the path is calculated right now, use <see cref="IsDone"/> instead. + /// </summary> + public PathCompleteState CompleteState { + get { return completeState; } + protected set { + // Locking is used to avoid multithreading race conditions in which, for example, + // the error state is set on the main thread to cancel the path, + // and then a pathfinding thread marks the path as completed, + // which would replace the error state (if a lock and check would not have been used). + // We lock on the path object itself. Users should rarely have to use the path object + // themselves for anything before the path is calculated, much less take a lock on it. + lock (this) { + // Once the path is put in the error state, it cannot be set to any other state + if (completeState != PathCompleteState.Error) completeState = value; + } + } + } + + /// <summary> + /// If the path failed, this is true. + /// See: <see cref="errorLog"/> + /// See: This is equivalent to checking path.CompleteState == PathCompleteState.Error + /// </summary> + public bool error { get { return CompleteState == PathCompleteState.Error; } } + + /// <summary> + /// Additional info on why a path failed. + /// See: <see cref="AstarPath.logPathResults"/> + /// </summary> + public string errorLog { get; private set; } + + /// <summary> + /// Holds the path as a <see cref="GraphNode"/> list. + /// + /// These are all nodes that the path traversed, as calculated by the pathfinding algorithm. + /// This may not be the same nodes as the post processed path traverses. + /// + /// See: <see cref="vectorPath"/> + /// </summary> + public List<GraphNode> path; + + /// <summary> + /// Holds the (possibly post-processed) path as a Vector3 list. + /// + /// This list may be modified by path modifiers to be smoother or simpler compared to the raw path generated by the pathfinding algorithm. + /// + /// See: modifiers (view in online documentation for working links) + /// See: <see cref="path"/> + /// </summary> + public List<Vector3> vectorPath; + + /// <summary>How long it took to calculate this path in milliseconds</summary> + public float duration; + + /// <summary>Number of nodes this path has searched</summary> + public int searchedNodes { get; protected set; } + + /// <summary> + /// True if the path is currently pooled. + /// Do not set this value. Only read. It is used internally. + /// + /// See: <see cref="PathPool"/> + /// </summary> + bool IPathInternals.Pooled { get; set; } + + /// <summary> + /// True if the Reset function has been called. + /// Used to alert users when they are doing something wrong. + /// </summary> + protected bool hasBeenReset; + + /// <summary>Constraint for how to search for nodes</summary> + public NNConstraint nnConstraint = PathNNConstraint.Walkable; + + /// <summary>Determines which heuristic to use</summary> + public Heuristic heuristic; + + /// <summary> + /// Scale of the heuristic values. + /// See: AstarPath.heuristicScale + /// </summary> + public float heuristicScale = 1F; + + /// <summary>ID of this path. Used to distinguish between different paths</summary> + public ushort pathID { get; private set; } + + /// <summary>Target to use for H score calculation.</summary> + protected GraphNode hTargetNode; + + /// <summary> + /// Target to use for H score calculations. + /// See: https://en.wikipedia.org/wiki/Admissible_heuristic + /// </summary> + protected HeuristicObjective heuristicObjective; + + internal ref HeuristicObjective heuristicObjectiveInternal => ref heuristicObjective; + + /// <summary> + /// Which graph tags are traversable. + /// This is a bitmask so -1 = all bits set = all tags traversable. + /// For example, to set bit 5 to true, you would do + /// <code> myPath.enabledTags |= 1 << 5; </code> + /// To set it to false, you would do + /// <code> myPath.enabledTags &= ~(1 << 5); </code> + /// + /// The Seeker has a popup field where you can set which tags to use. + /// Note: If you are using a Seeker. The Seeker will set this value to what is set in the inspector field on StartPath. + /// So you need to change the Seeker value via script, not set this value if you want to change it via script. + /// + /// See: <see cref="CanTraverse"/> + /// See: bitmasks (view in online documentation for working links) + /// </summary> + public int enabledTags = -1; + + /// <summary>List of zeroes to use as default tag penalties</summary> + internal static readonly int[] ZeroTagPenalties = new int[32]; + + /// <summary> + /// The tag penalties that are actually used. + /// See: <see cref="tagPenalties"/> + /// </summary> + protected int[] internalTagPenalties; + + /// <summary> + /// Penalties for each tag. + /// Tag 0 which is the default tag, will get a penalty of tagPenalties[0]. + /// These should only be non-negative values since the A* algorithm cannot handle negative penalties. + /// + /// When assigning an array to this property it must have a length of 32. + /// + /// Note: Setting this to null will make all tag penalties be treated as if they are zero. + /// + /// Note: If you are using a Seeker. The Seeker will set this value to what is set in the inspector field when you call seeker.StartPath. + /// So you need to change the Seeker's value via script, not set this value. + /// + /// See: <see cref="Seeker.tagPenalties"/> + /// </summary> + public int[] tagPenalties { + get { + return internalTagPenalties == ZeroTagPenalties ? null : internalTagPenalties; + } + set { + if (value == null) { + internalTagPenalties = ZeroTagPenalties; + } else { + if (value.Length != 32) throw new System.ArgumentException("tagPenalties must have a length of 32"); + + internalTagPenalties = value; + } + } + } + + /// <summary>Copies the given settings into this path</summary> + public void UseSettings (PathRequestSettings settings) { + nnConstraint.graphMask = settings.graphMask; + traversalProvider = settings.traversalProvider; + enabledTags = settings.traversableTags; + tagPenalties = settings.tagPenalties; + } + + /// <summary> + /// Total Length of the path. + /// Calculates the total length of the <see cref="vectorPath"/>. + /// Cache this rather than call this function every time since it will calculate the length every time, not just return a cached value. + /// Returns: Total length of <see cref="vectorPath"/>, if <see cref="vectorPath"/> is null positive infinity is returned. + /// </summary> + public float GetTotalLength () { + if (vectorPath == null) return float.PositiveInfinity; + float tot = 0; + for (int i = 0; i < vectorPath.Count-1; i++) tot += Vector3.Distance(vectorPath[i], vectorPath[i+1]); + return tot; + } + + /// <summary> + /// Waits until this path has been calculated and returned. + /// Allows for very easy scripting. + /// + /// <code> + /// IEnumerator Start () { + /// // Get the seeker component attached to this GameObject + /// var seeker = GetComponent<Seeker>(); + /// + /// var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, null); + /// // Wait... This may take a frame or two depending on how complex the path is + /// // The rest of the game will continue to run while we wait + /// yield return StartCoroutine(path.WaitForPath()); + /// // The path is calculated now + /// + /// // Draw the path in the scene view for 10 seconds + /// for (int i = 0; i < path.vectorPath.Count - 1; i++) { + /// Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10); + /// } + /// } + /// </code> + /// + /// Note: Do not confuse this with AstarPath.BlockUntilCalculated. This one will wait using yield until it has been calculated + /// while AstarPath.BlockUntilCalculated will halt all operations until the path has been calculated. + /// + /// Throws: System.InvalidOperationException if the path is not started. Send the path to <see cref="Seeker.StartPath(Path)"/> or <see cref="AstarPath.StartPath"/> before calling this function. + /// + /// See: <see cref="BlockUntilCalculated"/> + /// See: https://docs.unity3d.com/Manual/Coroutines.html + /// </summary> + public IEnumerator WaitForPath () { + if (PipelineState == PathState.Created) throw new System.InvalidOperationException("This path has not been started yet"); + + while (PipelineState != PathState.Returned) yield return null; + } + + /// <summary> + /// Blocks until this path has been calculated and returned. + /// Normally it takes a few frames for a path to be calculated and returned. + /// This function will ensure that the path will be calculated when this function returns + /// and that the callback for that path has been called. + /// + /// Use this function only if you really need to. + /// There is a point to spreading path calculations out over several frames. + /// It smoothes out the framerate and makes sure requesting a large + /// number of paths at the same time does not cause lag. + /// + /// Note: Graph updates and other callbacks might get called during the execution of this function. + /// + /// <code> + /// var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete); + /// path.BlockUntilCalculated(); + /// + /// // The path is calculated now, and the OnPathComplete callback has been called + /// </code> + /// + /// See: This is equivalent to calling <see cref="AstarPath.BlockUntilCalculated(Path)"/> + /// See: <see cref="WaitForPath"/> + /// </summary> + public void BlockUntilCalculated () { + AstarPath.BlockUntilCalculated(this); + } + + /// <summary> + /// True if this path node might be worth exploring. + /// + /// This is used during a search to filter out nodes which have already been fully searched. + /// </summary> + public bool ShouldConsiderPathNode (uint pathNodeIndex) { + var node = pathHandler.pathNodes[pathNodeIndex]; + return node.pathID != pathID || node.heapIndex != BinaryHeap.NotInHeap; + } + + public static readonly Unity.Profiling.ProfilerMarker MarkerOpenCandidateConnectionsToEnd = new Unity.Profiling.ProfilerMarker("OpenCandidateConnectionsToEnd"); + public static readonly Unity.Profiling.ProfilerMarker MarkerTrace = new Unity.Profiling.ProfilerMarker("Trace"); + + /// <summary> + /// Open a connection to the temporary end node if necessary. + /// + /// The start and end nodes are temporary nodes and are not included in the graph itself. + /// This means that we need to handle connections to and from those nodes as a special case. + /// This function will open a connection from the given node to the end node, if such a connection exists. + /// + /// It is called from the <see cref="GraphNode.Open"/> function. + /// </summary> + /// <param name="position">Position of the path node that is being opened. This may be different from the node's position if \reflink{PathNode.fractionAlongEdge} is being used.</param> + /// <param name="parentPathNode">Index of the path node that is being opened. This is often the same as parentNodeIndex, but may be different if the node has multiple path node variants.</param> + /// <param name="parentNodeIndex">Index of the node that is being opened.</param> + /// <param name="parentG">G score of the parent node. The cost to reach the parent node from the start of the path.</param> + public void OpenCandidateConnectionsToEndNode (Int3 position, uint parentPathNode, uint parentNodeIndex, uint parentG) { + // True iff this node has a connection to one or more temporary nodes + if (pathHandler.pathNodes[parentNodeIndex].flag1) { + MarkerOpenCandidateConnectionsToEnd.Begin(); + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End && node.associatedNode == parentNodeIndex) { + var cost = (uint)(position - node.position).costMagnitude; + OpenCandidateConnection(parentPathNode, nodeIndex, parentG, cost, 0, node.position); + } + } + MarkerOpenCandidateConnectionsToEnd.End(); + } + } + + /// <summary> + /// Opens a connection between two nodes during the A* search. + /// + /// When a node is "opened" (i.e. searched by the A* algorithm), it will open connections to all its neighbours. + /// This function checks those connections to see if passing through the node to its neighbour is the best way to reach the neighbour that we have seen so far, + /// and if so, it will push the neighbour onto the search heap. + /// </summary> + /// <param name="parentPathNode">The node that is being opened.</param> + /// <param name="targetPathNode">A neighbour of the parent that is being considered.</param> + /// <param name="parentG">The G value of the parent node. This is the cost to reach the parent node from the start of the path.</param> + /// <param name="connectionCost">The cost of moving from the parent node to the target node.</param> + /// <param name="fractionAlongEdge">Internal value used by the TriangleMeshNode to store where on the shared edge between the nodes we say we cross over.</param> + /// <param name="targetNodePosition">The position of the target node. This is used by the heuristic to estimate the cost to reach the end node.</param> + public void OpenCandidateConnection (uint parentPathNode, uint targetPathNode, uint parentG, uint connectionCost, uint fractionAlongEdge, Int3 targetNodePosition) { + if (!ShouldConsiderPathNode(targetPathNode)) { + // We have seen this node before, but it is not in the heap. + // This means we have already processed it and it must have had a better F score than this node (or the heuristic was not admissable). + // We can safely discard this connection. + return; + } + + uint candidateEnteringCost; + uint targetNodeIndex; + if (pathHandler.IsTemporaryNode(targetPathNode)) { + candidateEnteringCost = 0; + targetNodeIndex = 0; + } else { + var targetNode = pathHandler.GetNode(targetPathNode); + candidateEnteringCost = GetTraversalCost(targetNode); + targetNodeIndex = targetNode.NodeIndex; + } + var candidateG = parentG + connectionCost + candidateEnteringCost; + var pars = new OpenCandidateParams { + pathID = pathID, + parentPathNode = parentPathNode, + targetPathNode = targetPathNode, + targetNodeIndex = targetNodeIndex, + candidateG = candidateG, + fractionAlongEdge = fractionAlongEdge, + targetNodePosition = (int3)targetNodePosition, + pathNodes = pathHandler.pathNodes, + }; + OpenCandidateConnectionBurst(ref pars, + ref pathHandler.heap, ref heuristicObjective + ); + } + + /// <summary> + /// Parameters to OpenCandidateConnectionBurst. + /// Using a struct instead of passing the parameters as separate arguments is significantly faster. + /// </summary> + public struct OpenCandidateParams { + public Util.UnsafeSpan<PathNode> pathNodes; + public uint parentPathNode; + public uint targetPathNode; + public uint targetNodeIndex; + public uint candidateG; + public uint fractionAlongEdge; + public int3 targetNodePosition; + public ushort pathID; + } + + /// <summary> + /// Burst-compiled internal implementation of OpenCandidateConnection. + /// Compiling it using burst provides a decent 25% speedup. + /// The function itself is much faster, but the overhead of calling it from C# is quite significant. + /// </summary> + [Unity.Burst.BurstCompile] + public static void OpenCandidateConnectionBurst (ref OpenCandidateParams pars, ref BinaryHeap heap, ref HeuristicObjective heuristicObjective) { + var pathID = pars.pathID; + var parentPathNode = pars.parentPathNode; + var targetPathNode = pars.targetPathNode; + var candidateG = pars.candidateG; + var fractionAlongEdge = pars.fractionAlongEdge; + var targetNodePosition = pars.targetNodePosition; + var pathNodes = pars.pathNodes; + ref var target = ref pathNodes[targetPathNode]; + if (target.pathID != pathID) { + // This is the first time we have seen this node. This connection must be optimal. + target.fractionAlongEdge = fractionAlongEdge; + target.pathID = pathID; + target.parentIndex = parentPathNode; + var candidateH = (uint)heuristicObjective.Calculate(targetNodePosition, pars.targetNodeIndex); + var candidateF = candidateG + candidateH; + heap.Add(pathNodes, targetPathNode, candidateG, candidateF); + } else { + // Note: Before this method is called, a check is done for the case target.pathID==pathID && heapIndex == NotInHeap, + // so we know target.heapIndex != NotInHeap here. + + // We have seen this node before and it is in the heap. + // Now we check if this path to the target node is better than the previous one. + + var targetG = heap.GetG(target.heapIndex); + // The previous F score of the node + var targetF = heap.GetF(target.heapIndex); + var targetH = targetF - targetG; + uint candidateH; + + if (target.fractionAlongEdge != fractionAlongEdge) { + // Different fractionAlongEdge, this means that targetNodePosition may have changed + // and therefore the heuristic may also have changed. + candidateH = (uint)heuristicObjective.Calculate(targetNodePosition, pars.targetNodeIndex); + } else { + // If fractionAlongEdge has not changed, then we assume the heuristic is also the same. + // This saves us from having to calculate it again. + candidateH = targetH; + } + + var candidateF = candidateG + candidateH; + if (candidateF < targetF) { + // This connection is better than the previous one. + target.fractionAlongEdge = fractionAlongEdge; + target.parentIndex = parentPathNode; + heap.Add(pathNodes, targetPathNode, candidateG, candidateF); + } else { + // This connection is not better than the previous one. + // We can safely discard this connection. + } + } + } + + /// <summary>Returns penalty for the given tag.</summary> + /// <param name="tag">A value between 0 (inclusive) and 32 (exclusive).</param> + public uint GetTagPenalty (int tag) { + return (uint)internalTagPenalties[tag]; + } + + /// <summary> + /// Returns if the node can be traversed. + /// This by default equals to if the node is walkable and if the node's tag is included in <see cref="enabledTags"/>. + /// + /// See: <see cref="traversalProvider"/> + /// </summary> + public bool CanTraverse (GraphNode node) { + // Use traversal provider if set, otherwise fall back on default behaviour + // This method is hot, but this branch is extremely well predicted so it + // doesn't affect performance much (profiling indicates it is just above + // the noise level, somewhere around 0%-0.3%) + if (traversalProvider != null) + return traversalProvider.CanTraverse(this, node); + + // Manually inlined code from DefaultITraversalProvider + unchecked { return node.Walkable && (enabledTags >> (int)node.Tag & 0x1) != 0; } + } + + + /// <summary> + /// Returns if the path can traverse a link between from and to and if to can be traversed itself. + /// This by default equals to if the to is walkable and if the to's tag is included in <see cref="enabledTags"/>. + /// + /// See: <see cref="traversalProvider"/> + /// </summary> + public bool CanTraverse (GraphNode from, GraphNode to) { + // Use traversal provider if set, otherwise fall back on default behaviour + // This method is hot, but this branch is extremely well predicted so it + // doesn't affect performance much (profiling indicates it is just above + // the noise level, somewhere around 0%-0.3%) + if (traversalProvider != null) + return traversalProvider.CanTraverse(this, from, to); + + // Manually inlined code from DefaultITraversalProvider + unchecked { return to.Walkable && (enabledTags >> (int)to.Tag & 0x1) != 0; } + } + + /// <summary>Returns the cost of traversing the given node</summary> + public uint GetTraversalCost (GraphNode node) { +#if ASTAR_NO_TRAVERSAL_COST + return 0; +#else + // Use traversal provider if set, otherwise fall back on default behaviour + if (traversalProvider != null) + return traversalProvider.GetTraversalCost(this, node); + + unchecked { return GetTagPenalty((int)node.Tag) + node.Penalty; } +#endif + } + + /// <summary> + /// True if this path is done calculating. + /// + /// Note: The callback for the path might not have been called yet. + /// + /// See: <see cref="Seeker.IsDone"/> which also takes into account if the path callback has been called and had modifiers applied. + /// </summary> + public bool IsDone () { + return PipelineState > PathState.Processing; + } + + /// <summary>Threadsafe increment of the state</summary> + void IPathInternals.AdvanceState (PathState s) { + lock (this) { + PipelineState = (PathState)System.Math.Max((int)PipelineState, (int)s); + } + } + + /// <summary>Causes the path to fail and sets <see cref="errorLog"/> to msg</summary> + public void FailWithError (string msg) { + Error(); + if (errorLog != "") errorLog += "\n" + msg; + else errorLog = msg; + } + + /// <summary> + /// Aborts the path because of an error. + /// Sets <see cref="error"/> to true. + /// This function is called when an error has occurred (e.g a valid path could not be found). + /// See: <see cref="FailWithError"/> + /// </summary> + public void Error () { + CompleteState = PathCompleteState.Error; + } + + /// <summary> + /// Performs some error checking. + /// Makes sure the user isn't using old code paths and that no major errors have been made. + /// + /// Causes the path to fail if any errors are found. + /// </summary> + private void ErrorCheck () { + if (!hasBeenReset) FailWithError("Please use the static Construct function for creating paths, do not use the normal constructors."); + if (((IPathInternals)this).Pooled) FailWithError("The path is currently in a path pool. Are you sending the path for calculation twice?"); + if (pathHandler == null) FailWithError("Field pathHandler is not set. Please report this bug."); + if (PipelineState > PathState.Processing) FailWithError("This path has already been processed. Do not request a path with the same path object twice."); + } + + /// <summary> + /// Called when the path enters the pool. + /// This method should release e.g pooled lists and other pooled resources + /// The base version of this method releases vectorPath and path lists. + /// Reset() will be called after this function, not before. + /// Warning: Do not call this function manually. + /// </summary> + protected virtual void OnEnterPool () { + if (vectorPath != null) Pathfinding.Util.ListPool<Vector3>.Release(ref vectorPath); + if (path != null) Pathfinding.Util.ListPool<GraphNode>.Release(ref path); + // Clear the callback to remove a potential memory leak + // while the path is in the pool (which it could be for a long time). + callback = null; + immediateCallback = null; + traversalProvider = null; + pathHandler = null; + } + + /// <summary> + /// Reset all values to their default values. + /// + /// Note: All inheriting path types (e.g ConstantPath, RandomPath, etc.) which declare their own variables need to + /// override this function, resetting ALL their variables to enable pooling of paths. + /// If this is not done, trying to use that path type for pooling could result in weird behaviour. + /// The best way is to reset to default values the variables declared in the extended path type and then + /// call the base function in inheriting types with base.Reset(). + /// </summary> + protected virtual void Reset () { +#if ASTAR_POOL_DEBUG + pathTraceInfo = "This path was got from the pool or created from here (stacktrace):\n"; + pathTraceInfo += System.Environment.StackTrace; +#endif + + if (System.Object.ReferenceEquals(AstarPath.active, null)) + throw new System.NullReferenceException("No AstarPath object found in the scene. " + + "Make sure there is one or do not create paths in Awake"); + + hasBeenReset = true; + PipelineState = (int)PathState.Created; + releasedNotSilent = false; + + pathHandler = null; + callback = null; + immediateCallback = null; + errorLog = ""; + completeState = PathCompleteState.NotCalculated; + + path = Pathfinding.Util.ListPool<GraphNode>.Claim(); + vectorPath = Pathfinding.Util.ListPool<Vector3>.Claim(); + + duration = 0; + searchedNodes = 0; + + nnConstraint = PathNNConstraint.Walkable; + + heuristic = AstarPath.active.heuristic; + heuristicScale = AstarPath.active.heuristicScale; + + enabledTags = -1; + tagPenalties = null; + + pathID = AstarPath.active.GetNextPathID(); + + hTargetNode = null; + + traversalProvider = null; + } + + /// <summary>List of claims on this path with reference objects</summary> + private List<System.Object> claimed = new List<System.Object>(); + + /// <summary> + /// True if the path has been released with a non-silent call yet. + /// + /// See: Release + /// See: Claim + /// </summary> + private bool releasedNotSilent; + + /// <summary> + /// Increase the reference count on this path by 1 (for pooling). + /// A claim on a path will ensure that it is not pooled. + /// If you are using a path, you will want to claim it when you first get it and then release it when you will not + /// use it anymore. When there are no claims on the path, it will be reset and put in a pool. + /// + /// This is essentially just reference counting. + /// + /// The object passed to this method is merely used as a way to more easily detect when pooling is not done correctly. + /// It can be any object, when used from a movement script you can just pass "this". This class will throw an exception + /// if you try to call Claim on the same path twice with the same object (which is usually not what you want) or + /// if you try to call Release with an object that has not been used in a Claim call for that path. + /// The object passed to the Claim method needs to be the same as the one you pass to this method. + /// + /// See: Release + /// See: Pool + /// See: pooling (view in online documentation for working links) + /// See: https://en.wikipedia.org/wiki/Reference_counting + /// </summary> + public void Claim (System.Object o) { + if (System.Object.ReferenceEquals(o, null)) throw new System.ArgumentNullException("o"); + + for (int i = 0; i < claimed.Count; i++) { + // Need to use ReferenceEquals because it might be called from another thread + if (System.Object.ReferenceEquals(claimed[i], o)) + throw new System.ArgumentException("You have already claimed the path with that object ("+o+"). Are you claiming the path with the same object twice?"); + } + + claimed.Add(o); +#if ASTAR_POOL_DEBUG + claimInfo.Add(o.ToString() + "\n\nClaimed from:\n" + System.Environment.StackTrace); +#endif + } + + /// <summary> + /// Reduces the reference count on the path by 1 (pooling). + /// Removes the claim on the path by the specified object. + /// When the reference count reaches zero, the path will be pooled, all variables will be cleared and the path will be put in a pool to be used again. + /// This is great for performance since fewer allocations are made. + /// + /// If the silent parameter is true, this method will remove the claim by the specified object + /// but the path will not be pooled if the claim count reches zero unless a non-silent Release call has been made earlier. + /// This is used by the internal pathfinding components such as Seeker and AstarPath so that they will not cause paths to be pooled. + /// This enables users to skip the claim/release calls if they want without the path being pooled by the Seeker or AstarPath and + /// thus causing strange bugs. + /// + /// See: Claim + /// See: PathPool + /// </summary> + public void Release (System.Object o, bool silent = false) { + if (o == null) throw new System.ArgumentNullException("o"); + + for (int i = 0; i < claimed.Count; i++) { + // Need to use ReferenceEquals because it might be called from another thread + if (System.Object.ReferenceEquals(claimed[i], o)) { + claimed.RemoveAt(i); +#if ASTAR_POOL_DEBUG + claimInfo.RemoveAt(i); +#endif + if (!silent) { + releasedNotSilent = true; + } + + if (claimed.Count == 0 && releasedNotSilent) { + PathPool.Pool(this); + } + return; + } + } + if (claimed.Count == 0) { + throw new System.ArgumentException("You are releasing a path which is not claimed at all (most likely it has been pooled already). " + + "Are you releasing the path with the same object ("+o+") twice?" + + "\nCheck out the documentation on path pooling for help."); + } + throw new System.ArgumentException("You are releasing a path which has not been claimed with this object ("+o+"). " + + "Are you releasing the path with the same object twice?\n" + + "Check out the documentation on path pooling for help."); + } + + /// <summary> + /// Traces the calculated path from the end node to the start. + /// This will build an array (<see cref="path)"/> of the nodes this path will pass through and also set the <see cref="vectorPath"/> array to the <see cref="path"/> arrays positions. + /// Assumes the <see cref="vectorPath"/> and <see cref="path"/> are empty and not null (which will be the case for a correctly initialized path). + /// </summary> + protected virtual void Trace (uint fromPathNodeIndex) { + MarkerTrace.Begin(); + // Current node we are processing + var c = fromPathNodeIndex; + int count = 0; + var pathNodes = pathHandler.pathNodes; + + while (c != 0) { + c = pathNodes[c].parentIndex; + count++; + if (count > 16384) { + Debug.LogWarning("Infinite loop? >16384 node path. Remove this message if you really have that long paths (Path.cs, Trace method)"); + break; + } + } + + // Ensure the lists have enough capacity + if (path.Capacity < count) path.Capacity = count; + UnityEngine.Assertions.Assert.AreEqual(0, path.Count); + + c = fromPathNodeIndex; + + GraphNode lastNode = null; + for (int i = 0; i < count; i++) { + GraphNode node; + if (pathHandler.IsTemporaryNode(c)) { + node = pathHandler.GetNode(pathHandler.GetTemporaryNode(c).associatedNode); + } else { + node = pathHandler.GetNode(c); + } + // If a node has multiple variants (like the triangle mesh node), then we may visit + // the same node multiple times in a sequence (but different variants of it). + // In the final path we don't want the duplicates. + if (node != lastNode) { + path.Add(node); + lastNode = node; + } + c = pathNodes[c].parentIndex; + } + + // Reverse + count = path.Count; + int half = count/2; + for (int i = 0; i < half; i++) { + var tmp = path[i]; + path[i] = path[count-i-1]; + path[count - i - 1] = tmp; + } + + if (vectorPath.Capacity < count) vectorPath.Capacity = count; + for (int i = 0; i < count; i++) { + vectorPath.Add((Vector3)path[i].position); + } + MarkerTrace.End(); + } + + /// <summary> + /// Writes text shared for all overrides of DebugString to the string builder. + /// See: DebugString + /// </summary> + protected void DebugStringPrefix (PathLog logMode, System.Text.StringBuilder text) { + text.Append(error ? "Path Failed : " : "Path Completed : "); + text.Append("Computation Time "); + text.Append(duration.ToString(logMode == PathLog.Heavy ? "0.000 ms " : "0.00 ms ")); + + text.Append("Searched Nodes ").Append(searchedNodes); + + if (!error) { + text.Append(" Path Length "); + text.Append(path == null ? "Null" : path.Count.ToString()); + } + } + + /// <summary> + /// Writes text shared for all overrides of DebugString to the string builder. + /// See: DebugString + /// </summary> + protected void DebugStringSuffix (PathLog logMode, System.Text.StringBuilder text) { + if (error) { + text.Append("\nError: ").Append(errorLog); + } + + // Can only print this from the Unity thread + // since otherwise an exception might be thrown + if (logMode == PathLog.Heavy && !AstarPath.active.IsUsingMultithreading) { + text.Append("\nCallback references "); + if (callback != null) text.Append(callback.Target.GetType().FullName).AppendLine(); + else text.AppendLine("NULL"); + } + + text.Append("\nPath Number ").Append(pathID).Append(" (unique id)"); + } + + /// <summary> + /// Returns a string with information about it. + /// More information is emitted when logMode == Heavy. + /// An empty string is returned if logMode == None + /// or logMode == OnlyErrors and this path did not fail. + /// </summary> + protected virtual string DebugString (PathLog logMode) { + if (logMode == PathLog.None || (!error && logMode == PathLog.OnlyErrors)) { + return ""; + } + + // Get a cached string builder for this thread + System.Text.StringBuilder text = pathHandler.DebugStringBuilder; + text.Length = 0; + + DebugStringPrefix(logMode, text); + DebugStringSuffix(logMode, text); + + return text.ToString(); + } + + /// <summary>Calls callback to return the calculated path. See: <see cref="callback"/></summary> + protected virtual void ReturnPath () { + if (callback != null) { + callback(this); + } + } + + /// <summary> + /// Prepares low level path variables for calculation. + /// Called before a path search will take place. + /// Always called before the Prepare, Initialize and CalculateStep functions + /// </summary> + protected void PrepareBase (PathHandler pathHandler) { + //Make sure the path has a reference to the pathHandler + this.pathHandler = pathHandler; + //Assign relevant path data to the pathHandler + pathHandler.InitializeForPath(this); + + // Make sure that internalTagPenalties is an array which has the length 32 + if (internalTagPenalties == null || internalTagPenalties.Length != 32) + internalTagPenalties = ZeroTagPenalties; + + try { + ErrorCheck(); + } catch (System.Exception e) { + FailWithError(e.Message); + } + } + + /// <summary> + /// Called before the path is started. + /// Called right before Initialize + /// </summary> + protected abstract void Prepare(); + + /// <summary> + /// Always called after the path has been calculated. + /// Guaranteed to be called before other paths have been calculated on + /// the same thread. + /// Use for cleaning up things like node tagging and similar. + /// </summary> + protected virtual void Cleanup () { + // Cleanup any flags set by temporary nodes + var pathNodes = pathHandler.pathNodes; + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + + var associatedNode = pathHandler.GetNode(node.associatedNode); + for (uint v = 0; v < associatedNode.PathNodeVariants; v++) { + pathNodes[node.associatedNode + v].flag1 = false; + pathNodes[node.associatedNode + v].flag2 = false; + } + } + } + + protected int3 FirstTemporaryEndNode () { + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End) { + return (int3)node.position; + } + } + throw new System.InvalidOperationException("There are no end nodes in the path"); + } + + + protected void TemporaryEndNodesBoundingBox (out int3 mn, out int3 mx) { + // These represent a bounding box containing all valid end points. + // Typically there's only one end point, but in some cases there can be more. + mn = (int3)int.MaxValue; + mx = (int3)int.MinValue; + + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End) { + mn = math.min(mn, (int3)node.position); + mx = math.max(mx, (int3)node.position); + } + } + } + + protected void MarkNodesAdjacentToTemporaryEndNodes () { + var pathNodes = pathHandler.pathNodes; + + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.End) { + // Mark node with flag1 to mark it as a node connected to an end node + var associatedNode = pathHandler.GetNode(node.associatedNode); + for (uint v = 0; v < associatedNode.PathNodeVariants; v++) { + pathNodes[node.associatedNode + v].flag1 = true; + } + } + } + } + + protected void AddStartNodesToHeap () { + var pathNodes = pathHandler.pathNodes; + for (uint i = 0; i < pathHandler.numTemporaryNodes; i++) { + var nodeIndex = pathHandler.temporaryNodeStartIndex + i; + ref var node = ref pathHandler.GetTemporaryNode(nodeIndex); + if (node.type == TemporaryNodeType.Start) { + // Note: Setting F score to 0 is technically incorrect, but it doesn't + // matter since we will open the start nodes first anyway. + pathHandler.heap.Add(pathNodes, nodeIndex, 0, 0); + } + } + } + + /// <summary> + /// Called when there are no more nodes to search. + /// + /// This may be used to calculate a partial path as a fallback. + /// </summary> + protected abstract void OnHeapExhausted(); + + /// <summary> + /// Called when a valid node has been found for the end of the path. + /// + /// This function should trace the path back to the start node, and set CompleteState to Complete. + /// If CompleteState is unchanged, the search will continue. + /// </summary> + protected abstract void OnFoundEndNode(uint pathNode, uint hScore, uint gScore); + + /// <summary> + /// Called for every node that the path visits. + /// + /// This is used by path types to check if the target node has been reached, to log debug data, etc. + /// </summary> + public virtual void OnVisitNode (uint pathNode, uint hScore, uint gScore) {} + + /// <summary> + /// Calculates the path until completed or until the time has passed targetTick. + /// Usually a check is only done every 500 nodes if the time has passed targetTick. + /// Time/Ticks are got from System.DateTime.UtcNow.Ticks. + /// + /// Basic outline of what the function does for the standard path (Pathfinding.ABPath). + /// <code> + /// while the end has not been found and no error has occurred + /// pop the next node of the heap and set it as current + /// check if we have reached the end + /// if so, exit and return the path + /// + /// open the current node, i.e loop through its neighbours, mark them as visited and put them on a heap + /// + /// check if there are still nodes left to process (or have we searched the whole graph) + /// if there are none, flag error and exit + /// + /// check if the function has exceeded the time limit + /// if so, return and wait for the function to get called again + /// </code> + /// </summary> + protected virtual void CalculateStep (long targetTick) { + int counter = 0; + var pathNodes = pathHandler.pathNodes; + var temporaryNodeStartIndex = pathHandler.temporaryNodeStartIndex; + + // Continue to search as long as we haven't encountered an error and we haven't found the target + while (CompleteState == PathCompleteState.NotCalculated) { + searchedNodes++; + + // Any nodes left to search? + if (pathHandler.heap.isEmpty) { + OnHeapExhausted(); + return; + } + + // Select the node with the lowest F score and remove it from the open list + var currentPathNodeIndex = pathHandler.heap.Remove(pathNodes, out uint currentNodeG, out uint currentNodeF); + var currentNodeH = currentNodeF - currentNodeG; + + if (currentPathNodeIndex >= temporaryNodeStartIndex) { + // This is a special node + var node = pathHandler.GetTemporaryNode(currentPathNodeIndex); + if (node.type == TemporaryNodeType.Start) { + // A start node. We should open the associated node at this point + pathHandler.GetNode(node.associatedNode).OpenAtPoint(this, currentPathNodeIndex, node.position, currentNodeG); + } else if (node.type == TemporaryNodeType.End) { + // An end node. Yay! We found the path we wanted. + // Now we can just trace the path back to the start and return that. + // However, some path types may choose to continue the search to find more end points (e.g. the multi target path). + { + // Make sure we visit the node associated with the end node. + // This is usually redundant, but it can matter in some cases. + // In particular, triangle mesh nodes can be opened in such a way that the temporary end node + // gets a lower F score than the individual sides of the triangle. This means that the temporary end + // node will be searched before the triangle sides are searched and that might complete the path. + // This would lead to us never actually calling LogVisitedNode for the triangle node, if we didn't have this code. + pathHandler.LogVisitedNode(node.associatedNode, currentNodeH, currentNodeG); + } + OnFoundEndNode(currentPathNodeIndex, currentNodeH, currentNodeG); + if (CompleteState == PathCompleteState.Complete) { + return; + } + } + } else { + pathHandler.LogVisitedNode(currentPathNodeIndex, currentNodeH, currentNodeG); + + OnVisitNode(currentPathNodeIndex, currentNodeH, currentNodeG); + + // Loop through all walkable neighbours of the node and add them to the open list. + var node = pathHandler.GetNode(currentPathNodeIndex); + node.Open(this, currentPathNodeIndex, currentNodeG); + } + + // Check for time every 500 nodes, roughly every 0.5 ms usually + if (counter > 500) { + // Have we exceded the maxFrameTime, if so we should wait one frame before continuing the search since we don't want the game to lag + if (System.DateTime.UtcNow.Ticks >= targetTick) { + return; + } + counter = 0; + + // Mostly for development + if (searchedNodes > 1000000) { + throw new System.Exception("Probable infinite loop. Over 1,000,000 nodes searched"); + } + } + + counter++; + } + } + + PathHandler IPathInternals.PathHandler { get { return pathHandler; } } + void IPathInternals.OnEnterPool () { OnEnterPool(); } + void IPathInternals.Reset () { Reset(); } + void IPathInternals.ReturnPath () { ReturnPath(); } + void IPathInternals.PrepareBase (PathHandler handler) { PrepareBase(handler); } + void IPathInternals.Prepare () { Prepare(); } + void IPathInternals.Cleanup () { Cleanup(); } + void IPathInternals.CalculateStep (long targetTick) { CalculateStep(targetTick); } + string IPathInternals.DebugString (PathLog logMode) { return DebugString(logMode); } + } + + /// <summary>Used for hiding internal methods of the Path class</summary> + internal interface IPathInternals { + PathHandler PathHandler { get; } + bool Pooled { get; set; } + void AdvanceState(PathState s); + void OnEnterPool(); + void Reset(); + void ReturnPath(); + void PrepareBase(PathHandler handler); + void Prepare(); + void Cleanup(); + void CalculateStep(long targetTick); + string DebugString(PathLog logMode); + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs.meta new file mode 100644 index 0000000..17cffcf --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/Path.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b179046d83ec84f0c91efc5335f78a30 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathHandler.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathHandler.cs new file mode 100644 index 0000000..debafc1 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathHandler.cs @@ -0,0 +1,249 @@ +using System.Collections.Generic; +using Unity.Collections; +using UnityEngine; + +namespace Pathfinding { + using Pathfinding.Util; + + /// <summary> + /// Stores temporary node data for a single pathfinding request. + /// Every node has one PathNode per thread used. + /// It stores e.g G score, H score and other temporary variables needed + /// for path calculation, but which are not part of the graph structure. + /// + /// See: Pathfinding.PathHandler + /// See: https://en.wikipedia.org/wiki/A*_search_algorithm + /// </summary> + public struct PathNode { + /// <summary>The path request (in this thread, if multithreading is used) which last used this node</summary> + public ushort pathID; + + /// <summary> + /// Index of the node in the binary heap. + /// The open list in the A* algorithm is backed by a binary heap. + /// To support fast 'decrease key' operations, the index of the node + /// is saved here. + /// </summary> + public ushort heapIndex; + + /// <summary>Bitpacked variable which stores several fields</summary> + private uint flags; + + public static readonly PathNode Default = new PathNode { pathID = 0, heapIndex = BinaryHeap.NotInHeap, flags = 0 }; + + /// <summary>Parent index uses the first 26 bits</summary> + private const uint ParentIndexMask = (1U << 26) - 1U; + + private const int FractionAlongEdgeOffset = 26; + private const uint FractionAlongEdgeMask = ((1U << 30) - 1U) & ~ParentIndexMask; + public const int FractionAlongEdgeQuantization = 1 << (30 - 26); + + public static uint ReverseFractionAlongEdge(uint v) => (FractionAlongEdgeQuantization - 1) - v; + + public static uint QuantizeFractionAlongEdge (float v) { + v *= FractionAlongEdgeQuantization - 1; + v += 0.5f; + return Unity.Mathematics.math.clamp((uint)v, 0, FractionAlongEdgeQuantization - 1); + } + + public static float UnQuantizeFractionAlongEdge (uint v) { + return (float)v * (1.0f / (FractionAlongEdgeQuantization - 1)); + } + + /// <summary>Flag 1 is at bit 30</summary> + private const int Flag1Offset = 30; + private const uint Flag1Mask = 1U << Flag1Offset; + + /// <summary>Flag 2 is at bit 31</summary> + private const int Flag2Offset = 31; + private const uint Flag2Mask = 1U << Flag2Offset; + + public uint fractionAlongEdge { + get => (flags & FractionAlongEdgeMask) >> FractionAlongEdgeOffset; + set => flags = (flags & ~FractionAlongEdgeMask) | ((value << FractionAlongEdgeOffset) & FractionAlongEdgeMask); + } + + public uint parentIndex { + get => flags & ParentIndexMask; + set => flags = (flags & ~ParentIndexMask) | value; + } + + /// <summary> + /// Use as temporary flag during pathfinding. + /// Path types can use this during pathfinding to mark + /// nodes. When done, this flag should be reverted to its default state (false) to + /// avoid messing up other pathfinding requests. + /// </summary> + public bool flag1 { + get => (flags & Flag1Mask) != 0; + set => flags = (flags & ~Flag1Mask) | (value ? Flag1Mask : 0U); + } + + /// <summary> + /// Use as temporary flag during pathfinding. + /// Path types can use this during pathfinding to mark + /// nodes. When done, this flag should be reverted to its default state (false) to + /// avoid messing up other pathfinding requests. + /// </summary> + public bool flag2 { + get => (flags & Flag2Mask) != 0; + set => flags = (flags & ~Flag2Mask) | (value ? Flag2Mask : 0U); + } + } + + public enum TemporaryNodeType { + Start, + End, + Ignore, + } + + public struct TemporaryNode { + public uint associatedNode; + public Int3 position; + public int targetIndex; + public TemporaryNodeType type; + } + + /// <summary>Handles thread specific path data.</summary> + public class PathHandler { + /// <summary> + /// Current PathID. + /// See: <see cref="PathID"/> + /// </summary> + private ushort pathID; + + public readonly int threadID; + public readonly int totalThreadCount; + internal readonly GlobalNodeStorage nodeStorage; + public int numTemporaryNodes { get; private set; } + + /// <summary> + /// All path nodes with an index greater or equal to this are temporary nodes that only exist for the duration of a single path. + /// + /// This is a copy of NodeStorage.nextNodeIndex. This is used to avoid having to access the NodeStorage while pathfinding as it's an extra indirection. + /// </summary> + public uint temporaryNodeStartIndex { get; private set; } + UnsafeSpan<TemporaryNode> temporaryNodes; + + /// <summary> + /// Reference to the per-node data for this thread. + /// + /// Note: Only guaranteed to point to a valid allocation while the path is being calculated. + /// </summary> + public UnsafeSpan<PathNode> pathNodes; +#if UNITY_EDITOR + UnsafeSpan<GlobalNodeStorage.DebugPathNode> debugPathNodes; +#endif + + /// <summary> + /// Binary heap to keep track of nodes on the "Open list". + /// See: https://en.wikipedia.org/wiki/A*_search_algorithm + /// </summary> + public BinaryHeap heap = new BinaryHeap(128); + + /// <summary>ID for the path currently being calculated or last path that was calculated</summary> + public ushort PathID { get { return pathID; } } + + /// <summary> + /// StringBuilder that paths can use to build debug strings. + /// Better for performance and memory usage to use a single StringBuilder instead of each path creating its own + /// </summary> + public readonly System.Text.StringBuilder DebugStringBuilder = new System.Text.StringBuilder(); + + internal PathHandler (GlobalNodeStorage nodeStorage, int threadID, int totalThreadCount) { + this.threadID = threadID; + this.totalThreadCount = totalThreadCount; + this.nodeStorage = nodeStorage; + temporaryNodes = new UnsafeSpan<TemporaryNode>(Allocator.Persistent, GlobalNodeStorage.MaxTemporaryNodes); + } + + public void InitializeForPath (Path p) { + var lastPathId = pathID; + pathID = p.pathID; + numTemporaryNodes = 0; + temporaryNodeStartIndex = nodeStorage.nextNodeIndex; + // Get the path nodes for this thread (may have been resized since last we calculated a path) + pathNodes = nodeStorage.pathfindingThreadData[threadID].pathNodes; + +#if UNITY_EDITOR + var astar = AstarPath.active; + var shouldLog = astar.showGraphs && (astar.debugMode == GraphDebugMode.F || astar.debugMode == GraphDebugMode.H || astar.debugMode == GraphDebugMode.G || astar.showSearchTree); + debugPathNodes = shouldLog ? nodeStorage.pathfindingThreadData[threadID].debugPathNodes : default; +#endif + + // Path IDs have overflowed 65K, cleanup is needed to avoid bugs where we think + // we have already visited a node when we haven't. + // Since pathIDs are handed out sequentially, we can check if the new path id + // is smaller than the last one. + if (pathID < lastPathId) { + ClearPathIDs(); + } + } + + /// <summary> + /// Returns the PathNode corresponding to the specified node. + /// The PathNode is specific to this PathHandler since multiple PathHandlers + /// are used at the same time if multithreading is enabled. + /// </summary> + public ref PathNode GetPathNode (GraphNode node, uint variant = 0) { + return ref pathNodes[node.NodeIndex + variant]; + } + + public bool IsTemporaryNode(uint pathNodeIndex) => pathNodeIndex >= temporaryNodeStartIndex; + + public uint AddTemporaryNode (TemporaryNode node) { + if (numTemporaryNodes >= GlobalNodeStorage.MaxTemporaryNodes) { + // It would be nice if we could dynamically re-allocate the temporaryNodes array, and the pathNodes array. But this class allows handing out references to path nodes and temporary nodes, + // and we cannot guarantee that those references will not be used after this function is called (which may lead to memory corruption). + // So instead we just have a hard limit, which can be increased by enabling the ASTAR_MORE_MULTI_TARGET_PATH_TARGETS define. + throw new System.InvalidOperationException("Cannot create more than " + GlobalNodeStorage.MaxTemporaryNodes + " temporary nodes. You can enable ASTAR_MORE_MULTI_TARGET_PATH_TARGETS in the A* Inspector optimizations tab to increase this limit."); + } + + var index = temporaryNodeStartIndex + (uint)numTemporaryNodes; + temporaryNodes[numTemporaryNodes] = node; + pathNodes[index] = PathNode.Default; + numTemporaryNodes++; + return index; + } + + public GraphNode GetNode(uint nodeIndex) => nodeStorage.GetNode(nodeIndex); + + public ref TemporaryNode GetTemporaryNode (uint nodeIndex) { + if (nodeIndex < temporaryNodeStartIndex || nodeIndex >= temporaryNodeStartIndex + numTemporaryNodes) + throw new System.ArgumentOutOfRangeException(); + return ref temporaryNodes[(int)(nodeIndex - temporaryNodeStartIndex)]; + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void LogVisitedNode (uint pathNodeIndex, uint h, uint g) { +#if UNITY_EDITOR + if (debugPathNodes.Length > 0 && !IsTemporaryNode(pathNodeIndex)) { + var parent = pathNodes[pathNodeIndex].parentIndex; + debugPathNodes[pathNodeIndex] = new GlobalNodeStorage.DebugPathNode { + h = h, + g = g, + parentIndex = parent >= temporaryNodeStartIndex ? 0 : parent, + pathID = pathID, + fractionAlongEdge = (byte)pathNodes[pathNodeIndex].fractionAlongEdge, + }; + } +#endif + } + + /// <summary> + /// Set all nodes' pathIDs to 0. + /// See: Pathfinding.PathNode.pathID + /// </summary> + public void ClearPathIDs () { + for (int i = 0; i < pathNodes.Length; i++) { + pathNodes[i].pathID = 0; + } + } + + public void Dispose () { + heap.Dispose(); + temporaryNodes.Free(Allocator.Persistent); + pathNodes = default; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathHandler.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathHandler.cs.meta new file mode 100644 index 0000000..b197ba3 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathHandler.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7cc885607b0534cc6a4a34c4caa58d89 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathProcessor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathProcessor.cs new file mode 100644 index 0000000..b41a159 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathProcessor.cs @@ -0,0 +1,520 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using UnityEngine.Profiling; +using UnityEngine.Assertions; + +namespace Pathfinding { +#if NETFX_CORE + using Thread = Pathfinding.WindowsStore.Thread; +#else + using Thread = System.Threading.Thread; +#endif + + public class PathProcessor { + public event System.Action<Path> OnPathPreSearch; + public event System.Action<Path> OnPathPostSearch; + public event System.Action OnQueueUnblocked; + + internal BlockableChannel<Path> queue; + readonly AstarPath astar; + readonly PathReturnQueue returnQueue; + + PathHandler[] pathHandlers; + + /// <summary>References to each of the pathfinding threads</summary> + Thread[] threads; + bool multithreaded; + + /// <summary> + /// When no multithreading is used, the IEnumerator is stored here. + /// When no multithreading is used, a coroutine is used instead. It is not directly called with StartCoroutine + /// but a separate function has just a while loop which increments the main IEnumerator. + /// This is done so other functions can step the thread forward at any time, without having to wait for Unity to update it. + /// See: <see cref="CalculatePaths"/> + /// See: <see cref="CalculatePathsThreaded"/> + /// </summary> + IEnumerator threadCoroutine; + BlockableChannel<Path>.Receiver coroutineReceiver; + + readonly List<int> locks = new List<int>(); + int nextLockID = 0; + + static readonly Unity.Profiling.ProfilerMarker MarkerCalculatePath = new Unity.Profiling.ProfilerMarker("Calculating Path"); + static readonly Unity.Profiling.ProfilerMarker MarkerPreparePath = new Unity.Profiling.ProfilerMarker("Prepare Path"); + + /// <summary> + /// Number of parallel pathfinders. + /// Returns the number of concurrent processes which can calculate paths at once. + /// When using multithreading, this will be the number of threads, if not using multithreading it is always 1 (since only 1 coroutine is used). + /// See: threadInfos + /// See: IsUsingMultithreading + /// </summary> + public int NumThreads { + get { + return pathHandlers.Length; + } + } + + /// <summary>Returns whether or not multithreading is used</summary> + public bool IsUsingMultithreading { + get { + return multithreaded; + } + } + + internal PathProcessor (AstarPath astar, PathReturnQueue returnQueue, int processors, bool multithreaded) { + this.astar = astar; + this.returnQueue = returnQueue; + + // Set up path queue with the specified number of receivers + queue = new BlockableChannel<Path>(); + threads = null; + threadCoroutine = null; + pathHandlers = new PathHandler[0]; + } + + /// <summary> + /// Changes the number of threads used for pathfinding. + /// + /// If multithreading is disabled, processors must be equal to 1. + /// </summary> + public void SetThreadCount (int processors, bool multithreaded) { + if (threads != null || threadCoroutine != null || pathHandlers.Length > 0) throw new System.Exception("Call StopThreads before setting the thread count"); + + if (processors < 1) { + throw new System.ArgumentOutOfRangeException("processors"); + } + + if (!multithreaded && processors != 1) { + throw new System.Exception("Only a single non-multithreaded processor is allowed"); + } + + pathHandlers = new PathHandler[processors]; + this.multithreaded = multithreaded; + + for (int i = 0; i < processors; i++) { + pathHandlers[i] = new PathHandler(astar.nodeStorage, i, processors); + } + astar.nodeStorage.SetThreadCount(processors); + StartThreads(); + } + + void StartThreads () { + if (threads != null || threadCoroutine != null) throw new System.Exception("Call StopThreads before starting threads"); + + queue.Reopen(); + + // Ensure the node storage is up to date. + // Per-thread data may have been cleared if the AstarPath object + // was disabled. + astar.nodeStorage.SetThreadCount(pathHandlers.Length); + + if (multithreaded) { + threads = new Thread[this.pathHandlers.Length]; + + // Start lots of threads + for (int i = 0; i < this.pathHandlers.Length; i++) { + var pathHandler = pathHandlers[i]; + var receiver = queue.AddReceiver(); + threads[i] = new Thread(() => CalculatePathsThreaded(pathHandler, receiver)); +#if !UNITY_SWITCH || UNITY_EDITOR + // Note: Setting the thread name seems to crash when deploying for Switch: https://forum.arongranberg.com/t/path-processor-crashing-nintendo-switch-build/6584 + threads[i].Name = "Pathfinding Thread " + i; +#endif + threads[i].IsBackground = true; + threads[i].Start(); + } + } else { + coroutineReceiver = queue.AddReceiver(); + // Start coroutine if not using multithreading + threadCoroutine = CalculatePaths(pathHandlers[0]); + } + } + + /// <summary>Prevents pathfinding from running while held</summary> + public struct GraphUpdateLock { + PathProcessor pathProcessor; + int id; + + public GraphUpdateLock (PathProcessor pathProcessor, bool block) { + this.pathProcessor = pathProcessor; + Profiler.BeginSample("Pausing pathfinding"); + id = pathProcessor.Lock(block); + Profiler.EndSample(); + } + + /// <summary> + /// True while this lock is preventing the pathfinding threads from processing more paths. + /// Note that the pathfinding threads may not be paused yet (if this lock was obtained using PausePathfinding(false)). + /// </summary> + public bool Held => pathProcessor != null && pathProcessor.locks.Contains(id); + + /// <summary>Allow pathfinding to start running again if no other locks are still held</summary> + public void Release() => pathProcessor.Unlock(id); + } + + int Lock (bool block) { + queue.isBlocked = true; + if (block) { + while (!queue.allReceiversBlocked) { + Assert.IsTrue(threads != null || threadCoroutine != null); + if (IsUsingMultithreading) { + Thread.Sleep(1); + } else { + TickNonMultithreaded(); + } + } + } + + nextLockID++; + locks.Add(nextLockID); + return nextLockID; + } + + void Unlock (int id) { + if (!locks.Remove(id)) { + throw new System.ArgumentException("This lock has already been released"); + } + + // Check if there are no remaining active locks + if (locks.Count == 0) { + if (OnQueueUnblocked != null) OnQueueUnblocked(); + + queue.isBlocked = false; + } + } + + /// <summary> + /// Prevents pathfinding threads from starting to calculate any new paths. + /// + /// Returns: A lock object. You need to call Unlock on that object to allow pathfinding to resume. + /// + /// Note: In most cases this should not be called from user code. + /// </summary> + /// <param name="block">If true, this call will block until all pathfinding threads are paused. + /// otherwise the threads will be paused as soon as they are done with what they are currently doing.</param> + public GraphUpdateLock PausePathfinding (bool block) { + return new GraphUpdateLock(this, block); + } + + /// <summary> + /// Does pathfinding calculations when not using multithreading. + /// + /// This method should be called once per frame if <see cref="IsUsingMultithreading"/> is true. + /// </summary> + public void TickNonMultithreaded () { + // Process paths + if (threadCoroutine == null) throw new System.InvalidOperationException("Cannot tick non-multithreaded pathfinding when no coroutine has been started"); + + try { + if (!threadCoroutine.MoveNext()) { + threadCoroutine = null; + coroutineReceiver.Close(); + } + } catch (System.Exception e) { + Debug.LogException(e); + Debug.LogError("Unhandled exception during pathfinding. Terminating."); + queue.Close(); + + // This will kill pathfinding + threadCoroutine = null; + coroutineReceiver.Close(); + } + } + + /// <summary> + /// Calls 'Join' on each of the threads to block until they have completed. + /// + /// This will also clean up any unmanaged memory used by the threads. + /// </summary> + public void StopThreads () { + // Don't accept any more path calls to this AstarPath instance. + // This will cause all pathfinding threads to exit (if any exist) + queue.Close(); + + if (threads != null) { + for (int i = 0; i < threads.Length; i++) { + if (!threads[i].Join(200)) { + Debug.LogError("Could not terminate pathfinding thread["+i+"] in 200ms, trying Thread.Abort"); + threads[i].Abort(); + } + } + threads = null; + } + if (threadCoroutine != null) { + Assert.IsTrue(queue.numReceivers > 0); + while (queue.numReceivers > 0) TickNonMultithreaded(); + Assert.IsNull(threadCoroutine); + } + + Assert.AreEqual(queue.numReceivers, 0, "Not all receivers were blocked and terminated when stopping threads"); + + // Dispose unmanaged data + for (int i = 0; i < pathHandlers.Length; i++) { + pathHandlers[i].Dispose(); + } + pathHandlers = new PathHandler[0]; + } + + /// <summary> + /// Cleans up all native memory managed by this instance. + /// + /// You may use this instance again by calling SetThreadCount. + /// </summary> + public void Dispose () { + StopThreads(); + } + + /// <summary> + /// Main pathfinding method (multithreaded). + /// This method will calculate the paths in the pathfinding queue when multithreading is enabled. + /// + /// See: CalculatePaths + /// See: <see cref="AstarPath.StartPath"/> + /// </summary> + void CalculatePathsThreaded (PathHandler pathHandler, BlockableChannel<Path>.Receiver receiver) { + UnityEngine.Profiling.Profiler.BeginThreadProfiling("Pathfinding", "Pathfinding thread #" + (pathHandler.threadID+1)); + + try { + // Max number of ticks we are allowed to continue working in one run. + // One tick is 1/10000 of a millisecond. + // We need to check once in a while if the thread should be stopped. + long maxTicks = (long)(10*10000); + long targetTick = System.DateTime.UtcNow.Ticks + maxTicks; + while (true) { + // The path we are currently calculating + if (receiver.Receive(out var path) == BlockableChannel<Path>.PopState.Closed) { + if (astar.logPathResults == PathLog.Heavy) + Debug.LogWarning("Shutting down pathfinding thread #" + pathHandler.threadID); + receiver.Close(); + return; + } + MarkerCalculatePath.Begin(); + // Access the internal implementation methods + IPathInternals ipath = (IPathInternals)path; + + + MarkerPreparePath.Begin(); + ipath.PrepareBase(pathHandler); + + // Now processing the path + // Will advance to Processing + ipath.AdvanceState(PathState.Processing); + + // Call some callbacks + if (OnPathPreSearch != null) { + OnPathPreSearch(path); + } + + // Tick for when the path started, used for calculating how long time the calculation took + long startTicks = System.DateTime.UtcNow.Ticks; + + // Prepare the path + ipath.Prepare(); + MarkerPreparePath.End(); + + + if (path.CompleteState == PathCompleteState.NotCalculated) { + // For visualization purposes, we set the last computed path to p, so we can view debug info on it in the editor (scene view). + astar.debugPathData = ipath.PathHandler; + astar.debugPathID = path.pathID; + + // Loop while the path has not been fully calculated + while (path.CompleteState == PathCompleteState.NotCalculated) { + // Do some work on the path calculation. + // The function will return when it has taken too much time + // or when it has finished calculation + ipath.CalculateStep(targetTick); + + targetTick = System.DateTime.UtcNow.Ticks + maxTicks; + + // Cancel function (and thus the thread) if no more paths should be accepted. + // This is done when the A* object is about to be destroyed + // The path is returned and then this function will be terminated + if (queue.isClosed) { + path.FailWithError("AstarPath object destroyed"); + } + } + + path.duration = (System.DateTime.UtcNow.Ticks - startTicks)*0.0001F; + +#if ProfileAstar + System.Threading.Interlocked.Increment(ref AstarPath.PathsCompleted); + System.Threading.Interlocked.Add(ref AstarPath.TotalSearchTime, System.DateTime.UtcNow.Ticks - startTicks); +#endif + } + + // Cleans up node tagging and other things + ipath.Cleanup(); + pathHandler.heap.Clear(pathHandler.pathNodes); + + + if (path.immediateCallback != null) path.immediateCallback(path); + + if (OnPathPostSearch != null) { + OnPathPostSearch(path); + } + + // Push the path onto the return stack + // It will be detected by the main Unity thread and returned as fast as possible (the next late update hopefully) + returnQueue.Enqueue(path); + + // Will advance to ReturnQueue + ipath.AdvanceState(PathState.ReturnQueue); + + MarkerCalculatePath.End(); + } + } catch (System.Exception e) { +#if !NETFX_CORE + if (e is ThreadAbortException) { + if (astar.logPathResults == PathLog.Heavy) + Debug.LogWarning("Shutting down pathfinding thread #" + pathHandler.threadID); + receiver.Close(); + return; + } +#endif + + Debug.LogException(e); + Debug.LogError("Unhandled exception during pathfinding. Terminating."); + // Unhandled exception, kill pathfinding + queue.Close(); + } finally { + UnityEngine.Profiling.Profiler.EndThreadProfiling(); + } + + Debug.LogError("Error : This part should never be reached."); + receiver.Close(); + } + + /// <summary> + /// Main pathfinding method. + /// This method will calculate the paths in the pathfinding queue. + /// + /// See: CalculatePathsThreaded + /// See: StartPath + /// </summary> + IEnumerator CalculatePaths (PathHandler pathHandler) { + // Max number of ticks before yielding/sleeping + long maxTicks = (long)(astar.maxFrameTime*10000); + long targetTick = System.DateTime.UtcNow.Ticks + maxTicks; + + while (true) { + // The path we are currently calculating + Path p = null; + + // Try to get the next path to be calculated + bool blockedBefore = false; + while (p == null) { + switch (coroutineReceiver.ReceiveNoBlock(blockedBefore, out p)) { + case BlockableChannel<Path>.PopState.Ok: + break; + case BlockableChannel<Path>.PopState.Wait: + blockedBefore = true; + yield return null; + break; + case BlockableChannel<Path>.PopState.Closed: + yield break; + } + } + + IPathInternals ip = (IPathInternals)p; + + // Max number of ticks we are allowed to use for pathfinding in one frame + // One tick is 1/10000 of a millisecond + maxTicks = (long)(astar.maxFrameTime*10000); + + ip.PrepareBase(pathHandler); + + // Now processing the path + // Will advance to Processing + ip.AdvanceState(PathState.Processing); + + // Call some callbacks + // It needs to be stored in a local variable to avoid race conditions + var tmpOnPathPreSearch = OnPathPreSearch; + if (tmpOnPathPreSearch != null) tmpOnPathPreSearch(p); + + // Tick for when the path started, used for calculating how long time the calculation took + long startTicks = System.DateTime.UtcNow.Ticks; + long totalTicks = 0; + + ip.Prepare(); + + // Check if the Prepare call caused the path to complete + // If this happens the path usually failed + if (p.CompleteState == PathCompleteState.NotCalculated) { + // For debug uses, we set the last computed path to p, so we can view debug info on it in the editor (scene view). + astar.debugPathData = ip.PathHandler; + astar.debugPathID = p.pathID; + + // The error can turn up in the Init function + while (p.CompleteState == PathCompleteState.NotCalculated) { + // Run some pathfinding calculations. + // The function will return when it has taken too much time + // or when it has finished calculating the path. + ip.CalculateStep(targetTick); + + + // If the path has finished calculating, we can break here directly instead of sleeping + // Improves latency + if (p.CompleteState != PathCompleteState.NotCalculated) break; + + totalTicks += System.DateTime.UtcNow.Ticks-startTicks; + // Yield/sleep so other threads can work + + yield return null; + + startTicks = System.DateTime.UtcNow.Ticks; + + // Cancel function (and thus the thread) if no more paths should be accepted. + // This is done when the A* object is about to be destroyed + // The path is returned and then this function will be terminated (see similar IF statement higher up in the function) + if (queue.isClosed) { + p.FailWithError("AstarPath object destroyed"); + } + + targetTick = System.DateTime.UtcNow.Ticks + maxTicks; + } + + totalTicks += System.DateTime.UtcNow.Ticks-startTicks; + p.duration = totalTicks*0.0001F; + +#if ProfileAstar + System.Threading.Interlocked.Increment(ref AstarPath.PathsCompleted); +#endif + } + + // Cleans up node tagging and other things + ip.Cleanup(); + pathHandler.heap.Clear(pathHandler.pathNodes); + + + // Call the immediate callback + // It needs to be stored in a local variable to avoid race conditions + var tmpImmediateCallback = p.immediateCallback; + if (tmpImmediateCallback != null) tmpImmediateCallback(p); + + + // It needs to be stored in a local variable to avoid race conditions + var tmpOnPathPostSearch = OnPathPostSearch; + if (tmpOnPathPostSearch != null) tmpOnPathPostSearch(p); + + + // Push the path onto the return stack + // It will be detected by the main Unity thread and returned as fast as possible (the next late update) + returnQueue.Enqueue(p); + + ip.AdvanceState(PathState.ReturnQueue); + + + // Wait a bit if we have calculated a lot of paths + if (System.DateTime.UtcNow.Ticks > targetTick) { + yield return null; + targetTick = System.DateTime.UtcNow.Ticks + maxTicks; + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathProcessor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathProcessor.cs.meta new file mode 100644 index 0000000..764f634 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathProcessor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: f1b519f8b6e2c450aaae5cb2df6c73be +timeCreated: 1443114816 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathReturnQueue.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathReturnQueue.cs new file mode 100644 index 0000000..196c39b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathReturnQueue.cs @@ -0,0 +1,85 @@ +using UnityEngine; +using System.Collections.Generic; +using UnityEngine.Profiling; + +namespace Pathfinding { + class PathReturnQueue { + /// <summary> + /// Holds all paths which are waiting to be flagged as completed. + /// See: <see cref="ReturnPaths"/> + /// </summary> + readonly Queue<Path> pathReturnQueue = new Queue<Path>(); + + /// <summary> + /// Paths are claimed silently by some object to prevent them from being recycled while still in use. + /// This will be set to the AstarPath object. + /// </summary> + readonly System.Object pathsClaimedSilentlyBy; + + readonly System.Action OnReturnedPaths; + + public PathReturnQueue (System.Object pathsClaimedSilentlyBy, System.Action OnReturnedPaths) { + this.pathsClaimedSilentlyBy = pathsClaimedSilentlyBy; + this.OnReturnedPaths = OnReturnedPaths; + } + + public void Enqueue (Path path) { + lock (pathReturnQueue) { + pathReturnQueue.Enqueue(path); + } + } + + /// <summary> + /// Returns all paths in the return stack. + /// Paths which have been processed are put in the return stack. + /// This function will pop all items from the stack and return them to e.g the Seeker requesting them. + /// </summary> + /// <param name="timeSlice">Do not return all paths at once if it takes a long time, instead return some and wait until the next call.</param> + public void ReturnPaths (bool timeSlice) { + Profiler.BeginSample("Calling Path Callbacks"); + + // Hard coded limit on 1.0 ms + long targetTick = timeSlice ? System.DateTime.UtcNow.Ticks + 1 * 10000 : 0; + + int counter = 0; + int totalReturned = 0; + // Loop through the linked list and return all paths + while (true) { + // Move to the next path + Path path; + lock (pathReturnQueue) { + if (pathReturnQueue.Count == 0) break; + path = pathReturnQueue.Dequeue(); + } + + // Will increment path state to Returned + ((IPathInternals)path).AdvanceState(PathState.Returning); + + try { + // Return the path + ((IPathInternals)path).ReturnPath(); + } catch (System.Exception e) { + Debug.LogException(e); + } + + // Will increment path state to Returned + ((IPathInternals)path).AdvanceState(PathState.Returned); + + path.Release(pathsClaimedSilentlyBy, true); + + counter++; + totalReturned++; + // At least 5 paths will be returned, even if timeSlice is enabled + if (counter > 5 && timeSlice) { + counter = 0; + if (System.DateTime.UtcNow.Ticks >= targetTick) { + break; + } + } + } + + if (totalReturned > 0) OnReturnedPaths(); + Profiler.EndSample(); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathReturnQueue.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathReturnQueue.cs.meta new file mode 100644 index 0000000..4181918 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathReturnQueue.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 70ca370ae38794366be2fa32880f362e +timeCreated: 1443114816 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling.meta new file mode 100644 index 0000000..0d347ad --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 23af21159d0e0934f81f256ba806658a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ArrayPool.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ArrayPool.cs new file mode 100644 index 0000000..f7b6300 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ArrayPool.cs @@ -0,0 +1,200 @@ +#if !UNITY_EDITOR +// Extra optimizations when not running in the editor, but less error checking +#define ASTAR_OPTIMIZE_POOLING +#endif + +using System; +using System.Collections.Generic; + +namespace Pathfinding.Util { + /// <summary> + /// Lightweight Array Pool. + /// Handy class for pooling arrays of type T. + /// + /// Usage: + /// - Claim a new array using <code> SomeClass[] foo = ArrayPool<SomeClass>.Claim (capacity); </code> + /// - Use it and do stuff with it + /// - Release it with <code> ArrayPool<SomeClass>.Release (ref foo); </code> + /// + /// Warning: Arrays returned from the Claim method may contain arbitrary data. + /// You cannot rely on it being zeroed out. + /// + /// After you have released a array, you should never use it again, if you do use it + /// your code may modify it at the same time as some other code is using it which + /// will likely lead to bad results. + /// + /// Since: Version 3.8.6 + /// See: Pathfinding.Util.ListPool + /// </summary> + public static class ArrayPool<T> { +#if !ASTAR_NO_POOLING + /// <summary> + /// Maximum length of an array pooled using ClaimWithExactLength. + /// Arrays with lengths longer than this will silently not be pooled. + /// </summary> + const int MaximumExactArrayLength = 256; + + /// <summary> + /// Internal pool. + /// The arrays in each bucket have lengths of 2^i + /// </summary> + static readonly Stack<T[]>[] pool = new Stack<T[]>[31]; + static readonly Stack<T[]>[] exactPool = new Stack<T[]>[MaximumExactArrayLength+1]; +#if !ASTAR_OPTIMIZE_POOLING + static readonly HashSet<T[]> inPool = new HashSet<T[]>(); +#endif +#endif + + /// <summary> + /// Returns an array with at least the specified length. + /// Warning: Returned arrays may contain arbitrary data. + /// You cannot rely on it being zeroed out. + /// + /// The returned array will always be a power of two, or zero. + /// </summary> + public static T[] Claim (int minimumLength) { + if (minimumLength <= 0) { + return ClaimWithExactLength(0); + } + + int bucketIndex = 0; + while ((1 << bucketIndex) < minimumLength && bucketIndex < 30) { + bucketIndex++; + } + + if (bucketIndex == 30) + throw new System.ArgumentException("Too high minimum length"); + +#if !ASTAR_NO_POOLING + lock (pool) { + if (pool[bucketIndex] == null) { + pool[bucketIndex] = new Stack<T[]>(); + } + + if (pool[bucketIndex].Count > 0) { + var array = pool[bucketIndex].Pop(); +#if !ASTAR_OPTIMIZE_POOLING + inPool.Remove(array); +#endif + return array; + } + } +#endif + return new T[1 << bucketIndex]; + } + + /// <summary> + /// Returns an array with the specified length. + /// Use with caution as pooling too many arrays with different lengths that + /// are rarely being reused will lead to an effective memory leak. + /// + /// Use <see cref="Claim"/> if you just need an array that is at least as large as some value. + /// + /// Warning: Returned arrays may contain arbitrary data. + /// You cannot rely on it being zeroed out. + /// </summary> + public static T[] ClaimWithExactLength (int length) { +#if !ASTAR_NO_POOLING + bool isPowerOfTwo = length != 0 && (length & (length - 1)) == 0; + if (isPowerOfTwo) { + // Will return the correct array length + return Claim(length); + } + + if (length <= MaximumExactArrayLength) { + lock (pool) { + Stack<T[]> stack = exactPool[length]; + if (stack != null && stack.Count > 0) { + var array = stack.Pop(); +#if !ASTAR_OPTIMIZE_POOLING + inPool.Remove(array); +#endif + return array; + } + } + } +#endif + return new T[length]; + } + + /// <summary> + /// Pool an array. + /// If the array was got using the <see cref="ClaimWithExactLength"/> method then the allowNonPowerOfTwo parameter must be set to true. + /// The parameter exists to make sure that non power of two arrays are not pooled unintentionally which could lead to memory leaks. + /// </summary> + public static void Release (ref T[] array, bool allowNonPowerOfTwo = false) { + if (array == null) return; + if (array.GetType() != typeof(T[])) { + throw new System.ArgumentException("Expected array type " + typeof(T[]).Name + " but found " + array.GetType().Name + "\nAre you using the correct generic class?\n"); + } + +#if !ASTAR_NO_POOLING + bool isPowerOfTwo = array.Length != 0 && (array.Length & (array.Length - 1)) == 0; + if (!isPowerOfTwo && !allowNonPowerOfTwo && array.Length != 0) throw new System.ArgumentException("Length is not a power of 2"); + + lock (pool) { +#if !ASTAR_OPTIMIZE_POOLING + if (!inPool.Add(array)) { + throw new InvalidOperationException("You are trying to pool an array twice. Please make sure that you only pool it once."); + } +#endif + if (isPowerOfTwo) { + int bucketIndex = 0; + while ((1 << bucketIndex) < array.Length && bucketIndex < 30) { + bucketIndex++; + } + + if (pool[bucketIndex] == null) { + pool[bucketIndex] = new Stack<T[]>(); + } + + pool[bucketIndex].Push(array); + } else if (array.Length <= MaximumExactArrayLength) { + Stack<T[]> stack = exactPool[array.Length]; + if (stack == null) stack = exactPool[array.Length] = new Stack<T[]>(); + stack.Push(array); + } + } +#endif + array = null; + } + } + + /// <summary>Extension methods for List<T></summary> + public static class ListExtensions { + /// <summary> + /// Identical to ToArray but it uses ArrayPool<T> to avoid allocations if possible. + /// + /// Use with caution as pooling too many arrays with different lengths that + /// are rarely being reused will lead to an effective memory leak. + /// </summary> + public static T[] ToArrayFromPool<T>(this List<T> list) { + var arr = ArrayPool<T>.ClaimWithExactLength(list.Count); + + for (int i = 0; i < arr.Length; i++) { + arr[i] = list[i]; + } + return arr; + } + + /// <summary> + /// Clear a list faster than List<T>.Clear. + /// It turns out that the List<T>.Clear method will clear all elements in the underlaying array + /// not just the ones up to Count. If the list only has a few elements, but the capacity + /// is huge, this can cause performance problems. Using the RemoveRange method to remove + /// all elements in the list does not have this problem, however it is implemented in a + /// stupid way, so it will clear the elements twice (completely unnecessarily) so it will + /// only be faster than using the Clear method if the number of elements in the list is + /// less than half of the capacity of the list. + /// + /// Hopefully this method can be removed when Unity upgrades to a newer version of Mono. + /// </summary> + public static void ClearFast<T>(this List<T> list) { + if (list.Count*2 < list.Capacity) { + list.RemoveRange(0, list.Count); + } else { + list.Clear(); + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ArrayPool.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ArrayPool.cs.meta new file mode 100644 index 0000000..25e0d39 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ArrayPool.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 787a564bf2d894ee09284b775074864c +timeCreated: 1470483941 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ListPool.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ListPool.cs new file mode 100644 index 0000000..22ff246 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ListPool.cs @@ -0,0 +1,211 @@ +#if !UNITY_EDITOR +// Extra optimizations when not running in the editor, but less error checking +#define ASTAR_OPTIMIZE_POOLING +#endif + +using System; +using System.Collections.Generic; + +namespace Pathfinding.Util { + /// <summary> + /// Lightweight List Pool. + /// Handy class for pooling lists of type T. + /// + /// Usage: + /// - Claim a new list using <code> List<SomeClass> foo = ListPool<SomeClass>.Claim (); </code> + /// - Use it and do stuff with it + /// - Release it with <code> ListPool<SomeClass>.Release (foo); </code> + /// + /// You do not need to clear the list before releasing it. + /// After you have released a list, you should never use it again, if you do use it, you will + /// mess things up quite badly in the worst case. + /// + /// Since: Version 3.2 + /// See: Pathfinding.Util.StackPool + /// </summary> + public static class ListPool<T> { + /// <summary>Internal pool</summary> + static readonly List<List<T> > pool = new List<List<T> >(); + +#if !ASTAR_NO_POOLING + static readonly List<List<T> > largePool = new List<List<T> >(); + static readonly HashSet<List<T> > inPool = new HashSet<List<T> >(); +#endif + + /// <summary> + /// When requesting a list with a specified capacity, search max this many lists in the pool before giving up. + /// Must be greater or equal to one. + /// </summary> + const int MaxCapacitySearchLength = 8; + const int LargeThreshold = 5000; + const int MaxLargePoolSize = 8; + + /// <summary> + /// Claim a list. + /// Returns a pooled list if any are in the pool. + /// Otherwise it creates a new one. + /// After usage, this list should be released using the Release function (though not strictly necessary). + /// </summary> + public static List<T> Claim () { +#if ASTAR_NO_POOLING + return new List<T>(); +#else + lock (pool) { + if (pool.Count > 0) { + List<T> ls = pool[pool.Count-1]; + pool.RemoveAt(pool.Count-1); + inPool.Remove(ls); + return ls; + } + + return new List<T>(); + } +#endif + } + + static int FindCandidate (List<List<T> > pool, int capacity) { + // Loop through the last MaxCapacitySearchLength items + // and check if any item has a capacity greater or equal to the one that + // is desired. If so return it. + // Otherwise take the largest one or if there are no lists in the pool + // then allocate a new one with the desired capacity + List<T> list = null; + int listIndex = -1; + + for (int i = 0; i < pool.Count && i < MaxCapacitySearchLength; i++) { + // ith last item + var candidate = pool[pool.Count-1-i]; + + // Find the largest list that is not too large (arbitrary decision to try to prevent some memory bloat if the list was not just a temporary list). + if ((list == null || candidate.Capacity > list.Capacity) && candidate.Capacity < capacity*16) { + list = candidate; + listIndex = pool.Count-1-i; + + if (list.Capacity >= capacity) { + return listIndex; + } + } + } + + return listIndex; + } + + /// <summary> + /// Claim a list with minimum capacity + /// Returns a pooled list if any are in the pool. + /// Otherwise it creates a new one. + /// After usage, this list should be released using the Release function (though not strictly necessary). + /// A subset of the pool will be searched for a list with a high enough capacity and one will be returned + /// if possible, otherwise the list with the largest capacity found will be returned. + /// </summary> + public static List<T> Claim (int capacity) { +#if ASTAR_NO_POOLING + return new List<T>(capacity); +#else + lock (pool) { + var currentPool = pool; + var listIndex = FindCandidate(pool, capacity); + + if (capacity > LargeThreshold) { + var largeListIndex = FindCandidate(largePool, capacity); + if (largeListIndex != -1) { + currentPool = largePool; + listIndex = largeListIndex; + } + } + + if (listIndex == -1) { + return new List<T>(capacity); + } else { + var list = currentPool[listIndex]; + // Swap current item and last item to enable a more efficient removal + inPool.Remove(list); + currentPool[listIndex] = currentPool[currentPool.Count-1]; + currentPool.RemoveAt(currentPool.Count-1); + return list; + } + } +#endif + } + + /// <summary> + /// Makes sure the pool contains at least count pooled items with capacity size. + /// This is good if you want to do all allocations at start. + /// </summary> + public static void Warmup (int count, int size) { + lock (pool) { + var tmp = new List<T>[count]; + for (int i = 0; i < count; i++) tmp[i] = Claim(size); + for (int i = 0; i < count; i++) Release(tmp[i]); + } + } + + + /// <summary> + /// Releases a list and sets the variable to null. + /// After the list has been released it should not be used anymore. + /// + /// Throws: System.InvalidOperationException + /// Releasing a list when it has already been released will cause an exception to be thrown. + /// + /// See: <see cref="Claim"/> + /// </summary> + public static void Release (ref List<T> list) { + Release(list); + list = null; + } + + /// <summary> + /// Releases a list. + /// After the list has been released it should not be used anymore. + /// + /// Throws: System.InvalidOperationException + /// Releasing a list when it has already been released will cause an exception to be thrown. + /// + /// See: <see cref="Claim"/> + /// </summary> + public static void Release (List<T> list) { +#if !ASTAR_NO_POOLING + list.ClearFast(); + + lock (pool) { +#if !ASTAR_OPTIMIZE_POOLING + if (!inPool.Add(list)) { + throw new InvalidOperationException("You are trying to pool a list twice. Please make sure that you only pool it once."); + } +#endif + if (list.Capacity > LargeThreshold) { + largePool.Add(list); + + // Remove the list which was used the longest time ago from the pool if it + // exceeds the maximum size as it probably just contributes to memory bloat + if (largePool.Count > MaxLargePoolSize) { + largePool.RemoveAt(0); + } + } else { + pool.Add(list); + } + } +#endif + } + + /// <summary> + /// Clears the pool for lists of this type. + /// This is an O(n) operation, where n is the number of pooled lists. + /// </summary> + public static void Clear () { + lock (pool) { +#if !ASTAR_OPTIMIZE_POOLING && !ASTAR_NO_POOLING + inPool.Clear(); +#endif + pool.Clear(); + } + } + + /// <summary>Number of lists of this type in the pool</summary> + public static int GetSize () { + // No lock required since int writes are atomic + return pool.Count; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ListPool.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ListPool.cs.meta new file mode 100644 index 0000000..92e0b9c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ListPool.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2b76b7593907b44d9a6ef1b186fee0a7 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ObjectPool.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ObjectPool.cs new file mode 100644 index 0000000..262830c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ObjectPool.cs @@ -0,0 +1,131 @@ +#if !UNITY_EDITOR +// Extra optimizations when not running in the editor, but less error checking +#define ASTAR_OPTIMIZE_POOLING +#endif + +using System; +using System.Collections.Generic; + +namespace Pathfinding.Util { + public interface IAstarPooledObject { + void OnEnterPool(); + } + + /// <summary> + /// Lightweight object Pool for IAstarPooledObject. + /// Handy class for pooling objects of type T which implements the IAstarPooledObject interface. + /// + /// Usage: + /// - Claim a new object using <code> SomeClass foo = ObjectPool<SomeClass>.Claim (); </code> + /// - Use it and do stuff with it + /// - Release it with <code> ObjectPool<SomeClass>.Release (foo); </code> + /// + /// After you have released a object, you should never use it again. + /// + /// Since: Version 3.2 + /// Version: Since 3.7.6 this class is thread safe + /// See: Pathfinding.Util.ListPool + /// See: ObjectPoolSimple + /// </summary> + public static class ObjectPool<T> where T : class, IAstarPooledObject, new(){ + public static T Claim () { + return ObjectPoolSimple<T>.Claim(); + } + + public static void Release (ref T obj) { + obj.OnEnterPool(); + ObjectPoolSimple<T>.Release(ref obj); + } + } + + /// <summary> + /// Lightweight object Pool. + /// Handy class for pooling objects of type T. + /// + /// Usage: + /// - Claim a new object using <code> SomeClass foo = ObjectPool<SomeClass>.Claim (); </code> + /// - Use it and do stuff with it + /// - Release it with <code> ObjectPool<SomeClass>.Release (foo); </code> + /// + /// After you have released a object, you should never use it again. + /// + /// Since: Version 3.2 + /// Version: Since 3.7.6 this class is thread safe + /// See: Pathfinding.Util.ListPool + /// See: ObjectPool + /// </summary> + public static class ObjectPoolSimple<T> where T : class, new(){ + /// <summary>Internal pool</summary> + static List<T> pool = new List<T>(); + +#if !ASTAR_NO_POOLING + static readonly HashSet<T> inPool = new HashSet<T>(); +#endif + + /// <summary> + /// Claim a object. + /// Returns a pooled object if any are in the pool. + /// Otherwise it creates a new one. + /// After usage, this object should be released using the Release function (though not strictly necessary). + /// </summary> + public static T Claim () { +#if ASTAR_NO_POOLING + return new T(); +#else + lock (pool) { + if (pool.Count > 0) { + T ls = pool[pool.Count-1]; + pool.RemoveAt(pool.Count-1); + inPool.Remove(ls); + return ls; + } else { + return new T(); + } + } +#endif + } + + /// <summary> + /// Releases an object. + /// After the object has been released it should not be used anymore. + /// The variable will be set to null to prevent silly mistakes. + /// + /// Throws: System.InvalidOperationException + /// Releasing an object when it has already been released will cause an exception to be thrown. + /// However enabling ASTAR_OPTIMIZE_POOLING will prevent this check. + /// + /// See: Claim + /// </summary> + public static void Release (ref T obj) { +#if !ASTAR_NO_POOLING + lock (pool) { +#if !ASTAR_OPTIMIZE_POOLING + if (!inPool.Add(obj)) { + throw new InvalidOperationException("You are trying to pool an object twice. Please make sure that you only pool it once."); + } +#endif + pool.Add(obj); + } +#endif + obj = null; + } + + /// <summary> + /// Clears the pool for objects of this type. + /// This is an O(n) operation, where n is the number of pooled objects. + /// </summary> + public static void Clear () { + lock (pool) { +#if !ASTAR_OPTIMIZE_POOLING && !ASTAR_NO_POOLING + inPool.Clear(); +#endif + pool.Clear(); + } + } + + /// <summary>Number of objects of this type in the pool</summary> + public static int GetSize () { + return pool.Count; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ObjectPool.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ObjectPool.cs.meta new file mode 100644 index 0000000..6240b52 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/ObjectPool.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 10837c5b030bd47a2a0e6e213fea0868 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/PathPool.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/PathPool.cs new file mode 100644 index 0000000..9fa75d1 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/PathPool.cs @@ -0,0 +1,88 @@ +//#define ASTAR_NO_POOLING // Disable pooling for some reason. Maybe for debugging or just for measuring the difference. +using System; +using System.Collections.Generic; + +namespace Pathfinding { + /// <summary>Pools path objects to reduce load on the garbage collector</summary> + public static class PathPool { + static readonly Dictionary<Type, Stack<Path> > pool = new Dictionary<Type, Stack<Path> >(); + static readonly Dictionary<Type, int> totalCreated = new Dictionary<Type, int>(); + + /// <summary> + /// Adds a path to the pool. + /// This function should not be used directly. Instead use the Path.Claim and Path.Release functions. + /// </summary> + public static void Pool (Path path) { +#if !ASTAR_NO_POOLING + lock (pool) { + if (((IPathInternals)path).Pooled) { + throw new System.ArgumentException("The path is already pooled."); + } + + Stack<Path> poolStack; + if (!pool.TryGetValue(path.GetType(), out poolStack)) { + poolStack = new Stack<Path>(); + pool[path.GetType()] = poolStack; + } + + ((IPathInternals)path).Pooled = true; + ((IPathInternals)path).OnEnterPool(); + poolStack.Push(path); + } +#endif + } + + /// <summary>Total created instances of paths of the specified type</summary> + public static int GetTotalCreated (Type type) { + int created; + + if (totalCreated.TryGetValue(type, out created)) { + return created; + } else { + return 0; + } + } + + /// <summary>Number of pooled instances of a path of the specified type</summary> + public static int GetSize (Type type) { + Stack<Path> poolStack; + + if (pool.TryGetValue(type, out poolStack)) { + return poolStack.Count; + } else { + return 0; + } + } + + /// <summary>Get a path from the pool or create a new one if the pool is empty</summary> + public static T GetPath<T>() where T : Path, new() { +#if ASTAR_NO_POOLING + T result = new T(); + ((IPathInternals)result).Reset(); + return result; +#else + lock (pool) { + T result; + Stack<Path> poolStack; + if (pool.TryGetValue(typeof(T), out poolStack) && poolStack.Count > 0) { + // Guaranteed to have the correct type + result = poolStack.Pop() as T; + } else { + result = new T(); + + // Make sure an entry for the path type exists + if (!totalCreated.ContainsKey(typeof(T))) { + totalCreated[typeof(T)] = 0; + } + + totalCreated[typeof(T)]++; + } + + ((IPathInternals)result).Pooled = false; + ((IPathInternals)result).Reset(); + return result; + } +#endif + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/PathPool.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/PathPool.cs.meta new file mode 100644 index 0000000..9ff4458 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/PathPool.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cefe1014ab62848a89016fb97b1f8f7b +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/StackPool.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/StackPool.cs new file mode 100644 index 0000000..daf7a53 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/StackPool.cs @@ -0,0 +1,98 @@ +//#define ASTAR_NO_POOLING //@SHOWINEDITOR Disable pooling for some reason. Could be debugging or just for measuring the difference. + +using System.Collections.Generic; + +namespace Pathfinding.Util { + /// <summary> + /// Lightweight Stack Pool. + /// Handy class for pooling stacks of type T. + /// + /// Usage: + /// - Claim a new stack using <code> Stack<SomeClass> foo = StackPool<SomeClass>.Claim (); </code> + /// - Use it and do stuff with it + /// - Release it with <code> StackPool<SomeClass>.Release (foo); </code> + /// + /// You do not need to clear the stack before releasing it. + /// After you have released a stack, you should never use it again. + /// + /// Warning: This class is not thread safe + /// + /// Since: Version 3.2 + /// See: Pathfinding.Util.ListPool + /// </summary> + public static class StackPool<T> { + /// <summary>Internal pool</summary> + static readonly List<Stack<T> > pool; + + /// <summary>Static constructor</summary> + static StackPool () { + pool = new List<Stack<T> >(); + } + + /// <summary> + /// Claim a stack. + /// Returns a pooled stack if any are in the pool. + /// Otherwise it creates a new one. + /// After usage, this stack should be released using the Release function (though not strictly necessary). + /// </summary> + public static Stack<T> Claim () { +#if ASTAR_NO_POOLING + return new Stack<T>(); +#else + lock (pool) { + if (pool.Count > 0) { + Stack<T> ls = pool[pool.Count-1]; + pool.RemoveAt(pool.Count-1); + return ls; + } + } + + return new Stack<T>(); +#endif + } + + /// <summary> + /// Makes sure the pool contains at least count pooled items. + /// This is good if you want to do all allocations at start. + /// </summary> + public static void Warmup (int count) { + var tmp = new Stack<T>[count]; + + for (int i = 0; i < count; i++) tmp[i] = Claim(); + for (int i = 0; i < count; i++) Release(tmp[i]); + } + + /// <summary> + /// Releases a stack. + /// After the stack has been released it should not be used anymore. + /// Releasing a stack twice will cause an error. + /// </summary> + public static void Release (Stack<T> stack) { +#if !ASTAR_NO_POOLING + stack.Clear(); + + lock (pool) { + for (int i = 0; i < pool.Count; i++) + if (pool[i] == stack) UnityEngine.Debug.LogError("The Stack is released even though it is inside the pool"); + + pool.Add(stack); + } +#endif + } + + /// <summary> + /// Clears all pooled stacks of this type. + /// This is an O(n) operation, where n is the number of pooled stacks + /// </summary> + public static void Clear () { + lock (pool) { + pool.Clear(); + } + } + + /// <summary>Number of stacks of this type in the pool</summary> + public static int GetSize () { + return pool.Count; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/StackPool.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/StackPool.cs.meta new file mode 100644 index 0000000..1c8a3b5 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pooling/StackPool.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: de467bbbb1ff84668ae8262caad00941 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO.meta new file mode 100644 index 0000000..ff5b6ea --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 51dd3ade9cc0849ab9be4c7bad40988d diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgent.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgent.cs new file mode 100644 index 0000000..3b6ed49 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgent.cs @@ -0,0 +1,4 @@ + +// This file has been removed from the project. Since UnityPackages cannot +// delete files, only replace them, this message is left here to prevent old +// files from causing compiler errors diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgent.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgent.cs.meta new file mode 100644 index 0000000..521b101 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgent.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5a85e178962ba475ca424001ea4c13ca +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgentBurst.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgentBurst.cs new file mode 100644 index 0000000..5af0518 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgentBurst.cs @@ -0,0 +1,1998 @@ +using UnityEngine; +using System.Collections.Generic; + +namespace Pathfinding.RVO { + using Pathfinding; + using Pathfinding.Util; + using Unity.Burst; + using Unity.Jobs; + using Unity.Mathematics; + using Unity.Collections; + using Unity.IL2CPP.CompilerServices; + using Pathfinding.Drawing; + using Pathfinding.ECS.RVO; + using static Unity.Burst.CompilerServices.Aliasing; + using Unity.Profiling; + using System.Diagnostics; + + [BurstCompile(CompileSynchronously = false, FloatMode = FloatMode.Fast)] + public struct JobRVOPreprocess : IJob { + [ReadOnly] + public SimulatorBurst.AgentData agentData; + + [ReadOnly] + public SimulatorBurst.AgentOutputData previousOutput; + + [WriteOnly] + public SimulatorBurst.TemporaryAgentData temporaryAgentData; + + public int startIndex; + public int endIndex; + + public void Execute () { + for (int i = startIndex; i < endIndex; i++) { + if (!agentData.version[i].Valid) continue; + + // Manually controlled overrides the agent being locked. + // If one for some reason uses them at the same time. + var locked = agentData.locked[i] & !agentData.manuallyControlled[i]; + + if (locked) { + temporaryAgentData.desiredTargetPointInVelocitySpace[i] = float2.zero; + temporaryAgentData.desiredVelocity[i] = float3.zero; + temporaryAgentData.currentVelocity[i] = float3.zero; + } else { + var desiredTargetPointInVelocitySpace = agentData.movementPlane[i].ToPlane(agentData.targetPoint[i] - agentData.position[i]); + temporaryAgentData.desiredTargetPointInVelocitySpace[i] = desiredTargetPointInVelocitySpace; + + // Estimate our current velocity + // This is necessary because other agents need to know + // how this agent is moving to be able to avoid it + var currentVelocity = math.normalizesafe(previousOutput.targetPoint[i] - agentData.position[i]) * previousOutput.speed[i]; + + // Calculate the desired velocity from the point we want to reach + temporaryAgentData.desiredVelocity[i] = agentData.movementPlane[i].ToWorld(math.normalizesafe(desiredTargetPointInVelocitySpace) * agentData.desiredSpeed[i], 0); + + var collisionNormal = math.normalizesafe(agentData.collisionNormal[i]); + // Check if the velocity is going into the wall + // If so: remove that component from the velocity + // Note: if the collisionNormal is zero then the dot prodct will produce a zero as well and nothing will happen. + float dot = math.dot(currentVelocity, collisionNormal); + currentVelocity -= math.min(0, dot) * collisionNormal; + temporaryAgentData.currentVelocity[i] = currentVelocity; + } + } + } + } + + /// <summary> + /// Inspired by StarCraft 2's avoidance of locked units. + /// See: http://www.gdcvault.com/play/1014514/AI-Navigation-It-s-Not + /// </summary> + [BurstCompile(FloatMode = FloatMode.Fast)] + public struct JobHorizonAvoidancePhase1 : Pathfinding.Jobs.IJobParallelForBatched { + [ReadOnly] + public SimulatorBurst.AgentData agentData; + + [ReadOnly] + public NativeArray<float2> desiredTargetPointInVelocitySpace; + + [ReadOnly] + public NativeArray<int> neighbours; + + public SimulatorBurst.HorizonAgentData horizonAgentData; + + public CommandBuilder draw; + + public bool allowBoundsChecks { get { return true; } } + + /// <summary> + /// Super simple bubble sort. + /// TODO: This will be replaced by a better implementation from the Unity.Collections library when that is stable. + /// </summary> + static void Sort<T>(NativeSlice<T> arr, NativeSlice<float> keys) where T : struct { + bool changed = true; + + while (changed) { + changed = false; + for (int i = 0; i < arr.Length - 1; i++) { + if (keys[i] > keys[i+1]) { + var tmp = keys[i]; + var tmp2 = arr[i]; + keys[i] = keys[i+1]; + keys[i+1] = tmp; + arr[i] = arr[i+1]; + arr[i+1] = tmp2; + changed = true; + } + } + } + } + + + /// <summary>Calculates the shortest difference between two given angles given in radians.</summary> + public static float DeltaAngle (float current, float target) { + float num = Mathf.Repeat(target - current, math.PI*2); + + if (num > math.PI) { + num -= math.PI*2; + } + return num; + } + + public void Execute (int startIndex, int count) { + NativeArray<float> angles = new NativeArray<float>(SimulatorBurst.MaxNeighbourCount*2, Allocator.Temp); + NativeArray<int> deltas = new NativeArray<int>(SimulatorBurst.MaxNeighbourCount*2, Allocator.Temp); + + for (int i = startIndex; i < startIndex + count; i++) { + if (!agentData.version[i].Valid) continue; + + if (agentData.locked[i] || agentData.manuallyControlled[i]) { + horizonAgentData.horizonSide[i] = 0; + horizonAgentData.horizonMinAngle[i] = 0; + horizonAgentData.horizonMaxAngle[i] = 0; + continue; + } + + float minAngle = 0; + float maxAngle = 0; + + float desiredAngle = math.atan2(desiredTargetPointInVelocitySpace[i].y, desiredTargetPointInVelocitySpace[i].x); + + int eventCount = 0; + + int inside = 0; + + float radius = agentData.radius[i]; + + var position = agentData.position[i]; + var movementPlane = agentData.movementPlane[i]; + + var agentNeighbours = neighbours.Slice(i*SimulatorBurst.MaxNeighbourCount, SimulatorBurst.MaxNeighbourCount); + for (int j = 0; j < agentNeighbours.Length && agentNeighbours[j] != -1; j++) { + var other = agentNeighbours[j]; + if (!agentData.locked[other] && !agentData.manuallyControlled[other]) continue; + + var relativePosition = movementPlane.ToPlane(agentData.position[other] - position); + float dist = math.length(relativePosition); + + float angle = math.atan2(relativePosition.y, relativePosition.x) - desiredAngle; + float deltaAngle; + + var otherRadius = agentData.radius[other]; + if (dist < radius + otherRadius) { + // Collision + deltaAngle = math.PI * 0.49f; + } else { + // One degree + const float AngleMargin = math.PI / 180f; + deltaAngle = math.asin((radius + otherRadius)/dist) + AngleMargin; + } + + float aMin = DeltaAngle(0, angle - deltaAngle); + float aMax = aMin + DeltaAngle(aMin, angle + deltaAngle); + + if (aMin < 0 && aMax > 0) inside++; + + angles[eventCount] = aMin; + deltas[eventCount] = 1; + eventCount++; + angles[eventCount] = aMax; + deltas[eventCount] = -1; + eventCount++; + } + + // If no angle range includes angle 0 then we are already done + if (inside == 0) { + horizonAgentData.horizonSide[i] = 0; + horizonAgentData.horizonMinAngle[i] = 0; + horizonAgentData.horizonMaxAngle[i] = 0; + continue; + } + + // Sort the events by their angle in ascending order + Sort(deltas.Slice(0, eventCount), angles.Slice(0, eventCount)); + + // Find the first index for which the angle is positive + int firstPositiveIndex = 0; + for (; firstPositiveIndex < eventCount; firstPositiveIndex++) if (angles[firstPositiveIndex] > 0) break; + + // Walk in the positive direction from angle 0 until the end of the group of angle ranges that include angle 0 + int tmpInside = inside; + int tmpIndex = firstPositiveIndex; + for (; tmpIndex < eventCount; tmpIndex++) { + tmpInside += deltas[tmpIndex]; + if (tmpInside == 0) break; + } + maxAngle = tmpIndex == eventCount ? math.PI : angles[tmpIndex]; + + // Walk in the negative direction from angle 0 until the end of the group of angle ranges that include angle 0 + tmpInside = inside; + tmpIndex = firstPositiveIndex - 1; + for (; tmpIndex >= 0; tmpIndex--) { + tmpInside -= deltas[tmpIndex]; + if (tmpInside == 0) break; + } + minAngle = tmpIndex == -1 ? -math.PI : angles[tmpIndex]; + + //horizonBias = -(minAngle + maxAngle); + + // Indicates that a new side should be chosen. The "best" one will be chosen later. + if (horizonAgentData.horizonSide[i] == 0) horizonAgentData.horizonSide[i] = 2; + //else horizonBias = math.PI * horizonSide; + + horizonAgentData.horizonMinAngle[i] = minAngle + desiredAngle; + horizonAgentData.horizonMaxAngle[i] = maxAngle + desiredAngle; + } + } + } + + /// <summary> + /// Inspired by StarCraft 2's avoidance of locked units. + /// See: http://www.gdcvault.com/play/1014514/AI-Navigation-It-s-Not + /// </summary> + [BurstCompile(FloatMode = FloatMode.Fast)] + public struct JobHorizonAvoidancePhase2 : Pathfinding.Jobs.IJobParallelForBatched { + [ReadOnly] + public NativeArray<int> neighbours; + [ReadOnly] + public NativeArray<AgentIndex> versions; + public NativeArray<float3> desiredVelocity; + public NativeArray<float2> desiredTargetPointInVelocitySpace; + + [ReadOnly] + public NativeArray<NativeMovementPlane> movementPlane; + + public SimulatorBurst.HorizonAgentData horizonAgentData; + + public bool allowBoundsChecks => false; + + public void Execute (int startIndex, int count) { + for (int i = startIndex; i < startIndex + count; i++) { + if (!versions[i].Valid) continue; + + // Note: Assumes this code is run synchronous (i.e not included in the double buffering part) + //offsetVelocity = (position - Position) / simulator.DeltaTime; + + if (horizonAgentData.horizonSide[i] == 0) { + continue; + } + + if (horizonAgentData.horizonSide[i] == 2) { + float sum = 0; + var agentNeighbours = neighbours.Slice(i*SimulatorBurst.MaxNeighbourCount, SimulatorBurst.MaxNeighbourCount); + for (int j = 0; j < agentNeighbours.Length && agentNeighbours[j] != -1; j++) { + var other = agentNeighbours[j]; + var otherHorizonBias = -(horizonAgentData.horizonMinAngle[other] + horizonAgentData.horizonMaxAngle[other]); + sum += otherHorizonBias; + } + var horizonBias = -(horizonAgentData.horizonMinAngle[i] + horizonAgentData.horizonMaxAngle[i]); + sum += horizonBias; + + horizonAgentData.horizonSide[i] = sum < 0 ? -1 : 1; + } + + float bestAngle = horizonAgentData.horizonSide[i] < 0 ? horizonAgentData.horizonMinAngle[i] : horizonAgentData.horizonMaxAngle[i]; + float2 desiredDirection; + math.sincos(bestAngle, out desiredDirection.y, out desiredDirection.x); + desiredVelocity[i] = movementPlane[i].ToWorld(math.length(desiredVelocity[i]) * desiredDirection, 0); + desiredTargetPointInVelocitySpace[i] = math.length(desiredTargetPointInVelocitySpace[i]) * desiredDirection; + } + } + } + + [BurstCompile(FloatMode = FloatMode.Fast)] + public struct JobHardCollisions<MovementPlaneWrapper> : Pathfinding.Jobs.IJobParallelForBatched where MovementPlaneWrapper : struct, IMovementPlaneWrapper { + [ReadOnly] + public SimulatorBurst.AgentData agentData; + [ReadOnly] + public NativeArray<int> neighbours; + [WriteOnly] + public NativeArray<float2> collisionVelocityOffsets; + + public float deltaTime; + public bool enabled; + + /// <summary> + /// How aggressively hard collisions are resolved. + /// Should be a value between 0 and 1. + /// </summary> + const float CollisionStrength = 0.8f; + + public bool allowBoundsChecks => false; + + public void Execute (int startIndex, int count) { + if (!enabled) { + for (int i = startIndex; i < startIndex + count; i++) { + collisionVelocityOffsets[i] = float2.zero; + } + return; + } + + for (int i = startIndex; i < startIndex + count; i++) { + if (!agentData.version[i].Valid || agentData.locked[i]) { + collisionVelocityOffsets[i] = float2.zero; + continue; + } + + var agentNeighbours = neighbours.Slice(i*SimulatorBurst.MaxNeighbourCount, SimulatorBurst.MaxNeighbourCount); + var radius = agentData.radius[i]; + var totalOffset = float2.zero; + float totalWeight = 0; + + var position = agentData.position[i]; + var movementPlane = new MovementPlaneWrapper(); + movementPlane.Set(agentData.movementPlane[i]); + + for (int j = 0; j < agentNeighbours.Length && agentNeighbours[j] != -1; j++) { + var other = agentNeighbours[j]; + var relativePosition = movementPlane.ToPlane(position - agentData.position[other]); + + var dirSqrLength = math.lengthsq(relativePosition); + var combinedRadius = agentData.radius[other] + radius; + if (dirSqrLength < combinedRadius*combinedRadius && dirSqrLength > 0.00000001f) { + // Collision + var dirLength = math.sqrt(dirSqrLength); + var normalizedDir = relativePosition * (1.0f / dirLength); + + // Overlap amount + var weight = combinedRadius - dirLength; + + // Position offset required to make the agents not collide anymore + var offset = normalizedDir * weight; + // In a later step a weighted average will be taken so that the average offset is extracted + var weightedOffset = offset * weight; + + totalOffset += weightedOffset; + totalWeight += weight; + } + } + + var offsetVelocity = totalOffset * (1.0f / (0.0001f + totalWeight)); + offsetVelocity *= (CollisionStrength * 0.5f) / deltaTime; + + collisionVelocityOffsets[i] = offsetVelocity; + } + } + } + + [BurstCompile(CompileSynchronously = false, FloatMode = FloatMode.Fast)] + public struct JobRVOCalculateNeighbours<MovementPlaneWrapper> : Pathfinding.Jobs.IJobParallelForBatched where MovementPlaneWrapper : struct, IMovementPlaneWrapper { + [ReadOnly] + public SimulatorBurst.AgentData agentData; + + [ReadOnly] + public RVOQuadtreeBurst quadtree; + + public NativeArray<int> outNeighbours; + + [WriteOnly] + public SimulatorBurst.AgentOutputData output; + + public bool allowBoundsChecks { get { return false; } } + + public void Execute (int startIndex, int count) { + NativeArray<float> neighbourDistances = new NativeArray<float>(SimulatorBurst.MaxNeighbourCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + + for (int i = startIndex; i < startIndex + count; i++) { + if (!agentData.version[i].Valid) continue; + CalculateNeighbours(i, outNeighbours, neighbourDistances); + } + } + + void CalculateNeighbours (int agentIndex, NativeArray<int> neighbours, NativeArray<float> neighbourDistances) { + int maxNeighbourCount = math.min(SimulatorBurst.MaxNeighbourCount, agentData.maxNeighbours[agentIndex]); + // Write the output starting at this index in the neighbours array + var outputIndex = agentIndex * SimulatorBurst.MaxNeighbourCount; + + quadtree.QueryKNearest(new RVOQuadtreeBurst.QuadtreeQuery { + position = agentData.position[agentIndex], + speed = agentData.maxSpeed[agentIndex], + agentRadius = agentData.radius[agentIndex], + timeHorizon = agentData.agentTimeHorizon[agentIndex], + outputStartIndex = outputIndex, + maxCount = maxNeighbourCount, + result = neighbours, + resultDistances = neighbourDistances, + }); + + int numNeighbours = 0; + while (numNeighbours < maxNeighbourCount && math.isfinite(neighbourDistances[numNeighbours])) numNeighbours++; + output.numNeighbours[agentIndex] = numNeighbours; + + MovementPlaneWrapper movementPlane = default; + movementPlane.Set(agentData.movementPlane[agentIndex]); + movementPlane.ToPlane(agentData.position[agentIndex], out float localElevation); + + // Filter out invalid neighbours + for (int i = 0; i < numNeighbours; i++) { + int otherIndex = neighbours[outputIndex + i]; + // Interval along the y axis in which the agents overlap + movementPlane.ToPlane(agentData.position[otherIndex], out float otherElevation); + float maxY = math.min(localElevation + agentData.height[agentIndex], otherElevation + agentData.height[otherIndex]); + float minY = math.max(localElevation, otherElevation); + + // The agents cannot collide if they are on different y-levels. + // Also do not avoid the agent itself. + // Apply the layer masks for agents. + // Use binary OR to reduce branching. + if ((maxY < minY) | (otherIndex == agentIndex) | (((int)agentData.collidesWith[agentIndex] & (int)agentData.layer[otherIndex]) == 0)) { + numNeighbours--; + neighbours[outputIndex + i] = neighbours[outputIndex + numNeighbours]; + i--; + } + } + + // Add a token indicating the size of the neighbours list + if (numNeighbours < SimulatorBurst.MaxNeighbourCount) neighbours[outputIndex + numNeighbours] = -1; + } + } + + /// <summary> + /// Calculates if the agent has reached the end of its path and if its blocked from further progress towards it. + /// + /// If many agents have the same destination they can often end up crowded around a single point. + /// It is often desirable to detect this and mark all agents around that destination as having at least + /// partially reached the end of their paths. + /// + /// This job uses the following heuristics to determine this: + /// + /// 1. If an agent wants to move in a particular direction, but there's another agent in the way that makes it have to reduce its velocity, + /// the other agent is considered to be "blocking" the current agent. + /// 2. If the agent is within a small distance of the destination + /// THEN it is considered to have reached the end of its path. + /// 3. If the agent is blocked by another agent, + /// AND the other agent is blocked by this agent in turn, + /// AND if the destination is between the two agents, + /// THEN the the agent is considered to have reached the end of its path. + /// 4. If the agent is blocked by another agent which has reached the end of its path, + /// AND this agent is is moving slowly + /// AND this agent cannot move furter forward than 50% of its radius. + /// THEN the agent is considered to have reached the end of its path. + /// + /// Heuristics 2 and 3 are calculated initially, and then using heuristic 4 the set of agents which have reached their destinations expands outwards. + /// + /// These heuristics are robust enough that they can be used even if for example the agents are stuck in a winding maze + /// and only one agent is actually able to reach the destination. + /// + /// This job doesn't affect the movement of the agents by itself. + /// However, it is built with the intention that the FlowFollowingStrength parameter will be set + /// elsewhere to 1 for agents which have reached the end of their paths. This will make the agents stop gracefully + /// when the end of their paths is crowded instead of continuing to try to desperately reach the destination. + /// </summary> + [BurstCompile(CompileSynchronously = false, FloatMode = FloatMode.Fast)] + public struct JobDestinationReached<MovementPlaneWrapper>: IJob where MovementPlaneWrapper : struct, IMovementPlaneWrapper { + [ReadOnly] + public SimulatorBurst.AgentData agentData; + + [ReadOnly] + public SimulatorBurst.TemporaryAgentData temporaryAgentData; + + [ReadOnly] + public SimulatorBurst.ObstacleData obstacleData; + + public SimulatorBurst.AgentOutputData output; + public int numAgents; + public CommandBuilder draw; + + private static readonly ProfilerMarker MarkerInvert = new ProfilerMarker("InvertArrows"); + private static readonly ProfilerMarker MarkerAlloc = new ProfilerMarker("Alloc"); + private static readonly ProfilerMarker MarkerFirstPass = new ProfilerMarker("FirstPass"); + + struct TempAgentData { + public bool blockedAndSlow; + public float distToEndSq; + } + + public void Execute () { + MarkerAlloc.Begin(); + for (int agentIndex = 0; agentIndex < numAgents; agentIndex++) { + output.effectivelyReachedDestination[agentIndex] = ReachedEndOfPath.NotReached; + } + + // For each agent, store which agents it blocks + var inArrows = new NativeArray<int>(agentData.position.Length*SimulatorBurst.MaxBlockingAgentCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + // Number of agents that each agent blocks + var inArrowCounts = new NativeArray<int>(agentData.position.Length, Allocator.Temp, NativeArrayOptions.ClearMemory); + var que = new NativeCircularBuffer<int>(16, Allocator.Temp); + // True for an agent if it is in the queue, or if it should never be queued again + var queued = new NativeArray<bool>(numAgents, Allocator.Temp, NativeArrayOptions.ClearMemory); + var tempData = new NativeArray<TempAgentData>(numAgents, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + MarkerAlloc.End(); + MarkerInvert.Begin(); + + for (int agentIndex = 0; agentIndex < numAgents; agentIndex++) { + if (!agentData.version[agentIndex].Valid) continue; + for (int i = 0; i < SimulatorBurst.MaxBlockingAgentCount; i++) { + var blockingAgentIndex = output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount + i]; + if (blockingAgentIndex == -1) break; + var count = inArrowCounts[blockingAgentIndex]; + if (count >= SimulatorBurst.MaxBlockingAgentCount) continue; + inArrows[blockingAgentIndex*SimulatorBurst.MaxBlockingAgentCount + count] = agentIndex; + inArrowCounts[blockingAgentIndex] = count+1; + } + } + MarkerInvert.End(); + + MarkerFirstPass.Begin(); + for (int agentIndex = 0; agentIndex < numAgents; agentIndex++) { + if (!agentData.version[agentIndex].Valid) continue; + + var position = agentData.position[agentIndex]; + var movementPlane = agentData.movementPlane[agentIndex]; + var ourSpeed = output.speed[agentIndex]; + var ourEndOfPath = agentData.endOfPath[agentIndex]; + + // Ignore if destination is not set + if (!math.isfinite(ourEndOfPath.x)) continue; + + var distToEndSq = math.lengthsq(movementPlane.ToPlane(ourEndOfPath - position, out float endOfPathElevationDifference)); + var ourHeight = agentData.height[agentIndex]; + var reachedEndOfPath = false; + var flowFollowing = false; + var ourRadius = agentData.radius[agentIndex]; + var forwardClearance = output.forwardClearance[agentIndex]; + + // Heuristic 2 + if (distToEndSq < ourRadius*ourRadius*(0.5f*0.5f) && endOfPathElevationDifference < ourHeight && endOfPathElevationDifference > -ourHeight*0.5f) { + reachedEndOfPath = true; + } + + var closeToBlocked = forwardClearance < ourRadius*0.5f; + var slowish = ourSpeed*ourSpeed < math.max(0.01f*0.01f, math.lengthsq(temporaryAgentData.desiredVelocity[agentIndex])*0.25f); + var blockedAndSlow = closeToBlocked && slowish; + tempData[agentIndex] = new TempAgentData { + blockedAndSlow = blockedAndSlow, + distToEndSq = distToEndSq + }; + + // Heuristic 3 + for (int i = 0; i < SimulatorBurst.MaxBlockingAgentCount; i++) { + var blockingAgentIndex = output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount + i]; + if (blockingAgentIndex == -1) break; + + var otherPosition = agentData.position[blockingAgentIndex]; + var distBetweenAgentsSq = math.lengthsq(movementPlane.ToPlane(position - otherPosition)); + var circleRadius = (math.sqrt(distBetweenAgentsSq) + ourRadius + agentData.radius[blockingAgentIndex])*0.5f; + var endWithinCircle = math.lengthsq(movementPlane.ToPlane(ourEndOfPath - 0.5f*(position + otherPosition))) < circleRadius*circleRadius; + if (endWithinCircle) { + // Check if the other agent has an arrow pointing to this agent (i.e. it is blocked by this agent) + var loop = false; + for (int j = 0; j < SimulatorBurst.MaxBlockingAgentCount; j++) { + var arrowFromAgent = inArrows[agentIndex*SimulatorBurst.MaxBlockingAgentCount + j]; + if (arrowFromAgent == -1) break; + if (arrowFromAgent == blockingAgentIndex) { + loop = true; + break; + } + } + + if (loop) { + flowFollowing = true; + + if (blockedAndSlow) { + reachedEndOfPath = true; + } + } + } + } + + var effectivelyReached = reachedEndOfPath ? ReachedEndOfPath.Reached : (flowFollowing ? ReachedEndOfPath.ReachedSoon : ReachedEndOfPath.NotReached); + if (effectivelyReached != output.effectivelyReachedDestination[agentIndex]) { + output.effectivelyReachedDestination[agentIndex] = effectivelyReached; + + if (effectivelyReached == ReachedEndOfPath.Reached) { + // Mark this agent as queued to prevent it from being added to the queue again. + queued[agentIndex] = true; + + // Changing to the Reached flag may affect the calculations for other agents. + // So we iterate over all agents that may be affected and enqueue them again. + var count = inArrowCounts[agentIndex]; + for (int i = 0; i < count; i++) { + var inArrow = inArrows[agentIndex*SimulatorBurst.MaxBlockingAgentCount + i]; + if (!queued[inArrow]) que.PushEnd(inArrow); + } + } + } + } + MarkerFirstPass.End(); + + + int iteration = 0; + while (que.Length > 0) { + var agentIndex = que.PopStart(); + iteration++; + // If we are already at the reached stage, the result can never change. + if (output.effectivelyReachedDestination[agentIndex] == ReachedEndOfPath.Reached) continue; + queued[agentIndex] = false; + + var ourSpeed = output.speed[agentIndex]; + var ourEndOfPath = agentData.endOfPath[agentIndex]; + // Ignore if destination is not set + if (!math.isfinite(ourEndOfPath.x)) continue; + + var ourPosition = agentData.position[agentIndex]; + var blockedAndSlow = tempData[agentIndex].blockedAndSlow; + var distToEndSq = tempData[agentIndex].distToEndSq; + var ourRadius = agentData.radius[agentIndex]; + var reachedEndOfPath = false; + var flowFollowing = false; + + // Heuristic 4 + for (int i = 0; i < SimulatorBurst.MaxBlockingAgentCount; i++) { + var blockingAgentIndex = output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount + i]; + if (blockingAgentIndex == -1) break; + + var otherEndOfPath = agentData.endOfPath[blockingAgentIndex]; + var otherRadius = agentData.radius[blockingAgentIndex]; + + // Check if the other agent has a destination in roughly the same position as this agent. + // If we are further from the destination we tolarate larger deviations. + var endOfPathsOverlapping = math.lengthsq(otherEndOfPath - ourEndOfPath) <= distToEndSq*(0.5f*0.5f); + var otherReached = output.effectivelyReachedDestination[blockingAgentIndex] == ReachedEndOfPath.Reached; + + if (otherReached && (endOfPathsOverlapping || math.lengthsq(ourEndOfPath - agentData.position[blockingAgentIndex]) < math.lengthsq(ourRadius+otherRadius))) { + var otherSpeed = output.speed[blockingAgentIndex]; + flowFollowing |= math.min(ourSpeed, otherSpeed) < 0.01f; + reachedEndOfPath |= blockedAndSlow; + } + } + + var effectivelyReached = reachedEndOfPath ? ReachedEndOfPath.Reached : (flowFollowing ? ReachedEndOfPath.ReachedSoon : ReachedEndOfPath.NotReached); + // We do not check for all things that are checked in the first pass. So incorporate the previous information by taking the max. + effectivelyReached = (ReachedEndOfPath)math.max((int)effectivelyReached, (int)output.effectivelyReachedDestination[agentIndex]); + + if (effectivelyReached != output.effectivelyReachedDestination[agentIndex]) { + output.effectivelyReachedDestination[agentIndex] = effectivelyReached; + + if (effectivelyReached == ReachedEndOfPath.Reached) { + // Mark this agent as queued to prevent it from being added to the queue again. + queued[agentIndex] = true; + + // Changes to the Reached flag may affect the calculations for other agents. + // So we iterate over all agents that may be affected and enqueue them again. + var count = inArrowCounts[agentIndex]; + for (int i = 0; i < count; i++) { + var inArrow = inArrows[agentIndex*SimulatorBurst.MaxBlockingAgentCount + i]; + if (!queued[inArrow]) que.PushEnd(inArrow); + } + } + } + } + } + } + + // Note: FloatMode should not be set to Fast because that causes inaccuracies which can lead to + // agents failing to avoid walls sometimes. + [BurstCompile(CompileSynchronously = true, FloatMode = FloatMode.Default)] + public struct JobRVO<MovementPlaneWrapper> : Pathfinding.Jobs.IJobParallelForBatched where MovementPlaneWrapper : struct, IMovementPlaneWrapper { + [ReadOnly] + public SimulatorBurst.AgentData agentData; + + [ReadOnly] + public SimulatorBurst.TemporaryAgentData temporaryAgentData; + + [ReadOnly] + public NavmeshEdges.NavmeshBorderData navmeshEdgeData; + + [WriteOnly] + public SimulatorBurst.AgentOutputData output; + + public float deltaTime; + public float symmetryBreakingBias; + public float priorityMultiplier; + public bool useNavmeshAsObstacle; + + public bool allowBoundsChecks { get { return true; } } + + const int MaxObstacleCount = 50; + + public CommandBuilder draw; + + public void Execute (int startIndex, int batchSize) { + ExecuteORCA(startIndex, batchSize); + } + + struct SortByKey : IComparer<int> { + public UnsafeSpan<float> keys; + + public int Compare (int x, int y) { + return keys[x].CompareTo(keys[y]); + } + } + + /// <summary> + /// Sorts the array in place using insertion sort. + /// This is a stable sort. + /// See: http://en.wikipedia.org/wiki/Insertion_sort + /// + /// Used only because Unity.Collections.NativeSortExtension.Sort seems to have some kind of code generation bug when using Burst 1.8.2, causing it to throw exceptions. + /// </summary> + static void InsertionSort<T, U>(UnsafeSpan<T> data, U comparer) where T : unmanaged where U : IComparer<T> { + for (int i = 1; i < data.Length; i++) { + var value = data[i]; + int j = i - 1; + while (j >= 0 && comparer.Compare(data[j], value) > 0) { + data[j + 1] = data[j]; + j--; + } + data[j + 1] = value; + } + } + + private static readonly ProfilerMarker MarkerConvertObstacles1 = new ProfilerMarker("RVOConvertObstacles1"); + private static readonly ProfilerMarker MarkerConvertObstacles2 = new ProfilerMarker("RVOConvertObstacles2"); + + /// <summary> + /// Generates ORCA half-planes for all obstacles near the agent. + /// For more details refer to the ORCA (Optimal Reciprocal Collision Avoidance) paper. + /// + /// This function takes in several arrays which are just used for temporary data. This is to avoid the overhead of allocating the arrays once for every agent. + /// </summary> + void GenerateObstacleVOs (int agentIndex, NativeList<int> adjacentObstacleIdsScratch, NativeArray<int2> adjacentObstacleVerticesScratch, NativeArray<float> segmentDistancesScratch, NativeArray<int> sortedVerticesScratch, NativeArray<ORCALine> orcaLines, NativeArray<int> orcaLineToAgent, [NoAlias] ref int numLines, [NoAlias] in MovementPlaneWrapper movementPlane, float2 optimalVelocity) { + if (!useNavmeshAsObstacle) return; + + var localPosition = movementPlane.ToPlane(agentData.position[agentIndex], out var agentElevation); + var agentHeight = agentData.height[agentIndex]; + var agentRadius = agentData.radius[agentIndex]; + var obstacleRadius = agentRadius * 0.01f; + var inverseObstacleTimeHorizon = math.rcp(agentData.obstacleTimeHorizon[agentIndex]); + + ExpectNotAliased(in agentData.collisionNormal, in agentData.position); + + var hierarchicalNodeIndex = agentData.hierarchicalNodeIndex[agentIndex]; + if (hierarchicalNodeIndex == -1) return; + + var size = (obstacleRadius + agentRadius + agentData.obstacleTimeHorizon[agentIndex] * agentData.maxSpeed[agentIndex]) * new float3(2, 0, 2); + size.y = agentData.height[agentIndex] * 2f; + var bounds = new Bounds(new Vector3(localPosition.x, agentElevation, localPosition.y), size); + var boundingRadiusSq = math.lengthsq(bounds.extents); + adjacentObstacleIdsScratch.Clear(); + + var worldBounds = movementPlane.ToWorld(bounds); + navmeshEdgeData.GetObstaclesInRange(hierarchicalNodeIndex, worldBounds, adjacentObstacleIdsScratch); + +#if UNITY_EDITOR + if (agentData.HasDebugFlag(agentIndex, AgentDebugFlags.Obstacles)) { + draw.PushMatrix(movementPlane.matrix); + draw.PushMatrix(new float4x4( + new float4(1, 0, 0, 0), + new float4(0, 0, -1, 0), + new float4(0, 1, 0, 0), + new float4(0, 0, 0, 1) + )); + draw.WireBox(bounds, Color.blue); + draw.PopMatrix(); + draw.PopMatrix(); + } +#endif + + // TODO: For correctness all obstacles should be added in nearest-to-farthest order. + // This loop should be split up. + for (int oi = 0; oi < adjacentObstacleIdsScratch.Length; oi++) { + MarkerConvertObstacles1.Begin(); + var obstacleId = adjacentObstacleIdsScratch[oi]; + + var obstacleAllocations = navmeshEdgeData.obstacleData.obstacles[obstacleId]; + var vertices = navmeshEdgeData.obstacleData.obstacleVertices.GetSpan(obstacleAllocations.verticesAllocation); + var groups = navmeshEdgeData.obstacleData.obstacleVertexGroups.GetSpan(obstacleAllocations.groupsAllocation); + int vertexOffset = 0; + int candidateVertexCount = 0; + for (int i = 0; i < groups.Length; i++) { + var group = groups[i]; + // Check if the group does not overlap with our bounds at all + if (!math.all((group.boundsMx >= worldBounds.min) & (group.boundsMn <= worldBounds.max))) { + vertexOffset += group.vertexCount; + continue; + } + + + var startVertex = vertexOffset; + var endVertex = vertexOffset + group.vertexCount - 1; + if (endVertex >= adjacentObstacleVerticesScratch.Length) { + // Too many vertices. Skip remaining vertices. + break; + } + + for (int vi = startVertex; vi < startVertex + group.vertexCount; vi++) { + // X coordinate is the index of the previous vertex, the y coordinate is the next vertex + adjacentObstacleVerticesScratch[vi] = new int2(vi - 1, vi + 1); + } + // UnityEngine.Assertions.Assert.AreEqual(vertexCount, endVertex + 1); + + // Patch the start and end vertices to be correct. + // In a chain the last vertex doesn't start a new segment so we just make it loop back on itself. + // In a loop the last vertex connects to the first vertex. + adjacentObstacleVerticesScratch[startVertex] = new int2(group.type == ObstacleType.Loop ? endVertex : startVertex, adjacentObstacleVerticesScratch[startVertex].y); + adjacentObstacleVerticesScratch[endVertex] = new int2(adjacentObstacleVerticesScratch[endVertex].x, group.type == ObstacleType.Loop ? startVertex : endVertex); + + for (int vi = 0; vi < group.vertexCount; vi++) { + var vertex = vertices[vi + vertexOffset]; + int next = adjacentObstacleVerticesScratch[vi + startVertex].y; + var pos = movementPlane.ToPlane(vertex) - localPosition; + var nextPos = movementPlane.ToPlane(vertices[next]) - localPosition; + var dir = nextPos - pos; + var closestT = ClosestPointOnSegment(pos, dir / math.lengthsq(dir), float2.zero, 0, 1); + var dist = math.lengthsq(pos + dir*closestT); + segmentDistancesScratch[vi + startVertex] = dist; + + if (dist <= boundingRadiusSq && candidateVertexCount < sortedVerticesScratch.Length) { + sortedVerticesScratch[candidateVertexCount] = vi + startVertex; + candidateVertexCount++; + } + } + + vertexOffset += group.vertexCount; + } + + MarkerConvertObstacles1.End(); + + MarkerConvertObstacles2.Begin(); + // Sort obstacle segments by distance from the agent + InsertionSort(sortedVerticesScratch.AsUnsafeSpan().Slice(0, candidateVertexCount), new SortByKey { + keys = segmentDistancesScratch.AsUnsafeSpan().Slice(0, vertexOffset) + }); + + for (int i = 0; i < candidateVertexCount; i++) { + // In the unlikely event that we exceed the maximum number of obstacles, we just skip the remaining ones. + if (numLines >= MaxObstacleCount) break; + + // Processing the obstacle defined by v1 and v2 + // + // v0 v3 + // \ / + // \ / + // v1 ========= v2 + // + var v1Index = sortedVerticesScratch[i]; + + // If the obstacle is too far away, we can skip it. + // Since the obstacles are sorted by distance we can break here. + if (segmentDistancesScratch[v1Index] > 0.25f*size.x*size.x) break; + + var v0Index = adjacentObstacleVerticesScratch[v1Index].x; + var v2Index = adjacentObstacleVerticesScratch[v1Index].y; + if (v2Index == v1Index) continue; + var v3Index = adjacentObstacleVerticesScratch[v2Index].y; + UnityEngine.Assertions.Assert.AreNotEqual(v1Index, v3Index); + UnityEngine.Assertions.Assert.AreNotEqual(v0Index, v2Index); + + var v0 = vertices[v0Index]; + var v1 = vertices[v1Index]; + var v2 = vertices[v2Index]; + var v3 = vertices[v3Index]; + + var v0Position = movementPlane.ToPlane(v0) - localPosition; + var v1Position = movementPlane.ToPlane(v1, out var e1) - localPosition; + var v2Position = movementPlane.ToPlane(v2, out var e2) - localPosition; + var v3Position = movementPlane.ToPlane(v3) - localPosition; + + // Assume the obstacle has the same height as the agent, then check if they overlap along the elevation axis. + if (math.max(e1, e2) + agentHeight < agentElevation || math.min(e1, e2) > agentElevation + agentHeight) { + // The obstacle is not in the agent's elevation range. Ignore it. + continue; + } + + var length = math.length(v2Position - v1Position); + if (length < 0.0001f) continue; + var segmentDir = (v2Position - v1Position) * math.rcp(length); + + if (det(segmentDir, -v1Position) > obstacleRadius) { + // Agent is significantly on the wrong side of the segment (on the "inside"). Ignore it. + continue; + } + + // Check if this velocity obstacle completely behind previously added ORCA lines. + // If so, this obstacle is redundant and we can ignore it. + // This is not just a performance optimization. Using the ORCA lines for closer + // obstacles is better since obstacles further away can add ORCA lines that + // restrict the velocity space unnecessarily. The ORCA line is more conservative than the VO. + bool alreadyCovered = false; + + const float EPSILON = 0.0001f; + for (var j = 0; j < numLines; j++) { + var line = orcaLines[j]; + if ( + // Check if this velocity-obstacle is completely inside the previous ORCA line's infeasible half-plane region. + det(inverseObstacleTimeHorizon * v1Position - line.point, line.direction) - inverseObstacleTimeHorizon * obstacleRadius >= -EPSILON && + det(inverseObstacleTimeHorizon * v2Position - line.point, line.direction) - inverseObstacleTimeHorizon * obstacleRadius >= -EPSILON + ) { + alreadyCovered = true; + break; + } + } + if (alreadyCovered) { + continue; + } + + var obstacleOptimizationVelocity = float2.zero; + var distanceAlongSegment = math.dot(obstacleOptimizationVelocity - v1Position, segmentDir); + var closestPointOnSegment = v1Position + distanceAlongSegment * segmentDir; + var distanceToLineSq = math.lengthsq(closestPointOnSegment - obstacleOptimizationVelocity); + var distanceToSegmentSq = math.lengthsq((v1Position + math.clamp(distanceAlongSegment, 0, length) * segmentDir)); + + var v1Convex = leftOrColinear(v1Position - v0Position, segmentDir); + var v2Convex = leftOrColinear(segmentDir, v3Position - v2Position); + + if (distanceToSegmentSq < obstacleRadius*obstacleRadius) { + if (distanceAlongSegment < 0.0f) { + // Collision with left vertex, ignore if the vertex is not convex + if (v1Convex) { + orcaLineToAgent[numLines] = -1; + orcaLines[numLines++] = new ORCALine { + point = -v1Position * 0.1f, + direction = math.normalizesafe(rot90(v1Position)), + }; + } + } else if (distanceAlongSegment > length) { + // Collision with right vertex + // Ignore if the vertex is not convex, or if it will be taken care of + // by the neighbour obstacle segment. + if (v2Convex && leftOrColinear(v2Position, v3Position - v2Position)) { + orcaLineToAgent[numLines] = -1; + orcaLines[numLines++] = new ORCALine { + point = -v2Position * 0.1f, + direction = math.normalizesafe(rot90(v2Position)), + }; + } + } else { + // Collision with segment + orcaLineToAgent[numLines] = -1; + orcaLines[numLines++] = new ORCALine { + point = -closestPointOnSegment * 0.1f, + direction = -segmentDir, + }; + } + continue; + } + + // Represents rays starting points on the VO circles, going in a tangent direction away from the agent. + float2 leftLegDirection, rightLegDirection; + + if ((distanceAlongSegment < 0 || distanceAlongSegment > 1) && distanceToLineSq <= obstacleRadius*obstacleRadius) { + // Obliquely viewed so that the circle around one of the vertices is all that is visible from p. p = obstacleOptimizationVelocity + // _____________________________ _ _ _ _ _ _ _ _ _ _ _ _ + // _/ \_ _/ \_ + // / \ / \ + // | v1 | | v2 | + // \_ _/ \_ _/ p + // \_____/_________________\_____/ _ _ _ _ _ _ _ _ _ _ _ _ + + // Collapse segment to a single point, making sure that v0 and v3 are still the neighbouring vertices. + if (distanceAlongSegment < 0) { + // Collapse to v1 + // Ignore if not convex + if (!v1Convex) continue; + v3Position = v2Position; + v2Position = v1Position; + v2Convex = v1Convex; + } else { + // Collapse to v2 + if (!v2Convex) continue; + v0Position = v1Position; + v1Position = v2Position; + v1Convex = v2Convex; + } + var vertexDistSq = math.lengthsq(v1Position); + // Distance from p to the points where the legs (tangents) touch the circle around the vertex. + float leg = math.sqrt(vertexDistSq - obstacleRadius*obstacleRadius); + var posNormal = new float2(-v1Position.y, v1Position.x); + // These become normalized + leftLegDirection = (v1Position*leg + posNormal*obstacleRadius) / vertexDistSq; + rightLegDirection = (v1Position*leg - posNormal*obstacleRadius) / vertexDistSq; + } else { + // This is the common case (several valid positions of p are shown). p = obstacleOptimizationVelocity + // + // p + // _____________________________ + // _/ \_ _/ \_ + // / \ / \ + // | v1 | | v2 | + // \_ _/ \_ _/ + // \_____/_________________\_____/ + // + // p p + + if (v1Convex) { + var vertexDistSq = math.lengthsq(v1Position); + float leg = math.sqrt(vertexDistSq - obstacleRadius*obstacleRadius); + var posNormal = new float2(-v1Position.y, v1Position.x); + // This becomes normalized + leftLegDirection = (v1Position*leg + posNormal*obstacleRadius) / vertexDistSq; + } else { + leftLegDirection = -segmentDir; + } + + if (v2Convex) { + var vertexDistSq = math.lengthsq(v2Position); + float leg = math.sqrt(vertexDistSq - obstacleRadius*obstacleRadius); + var posNormal = new float2(-v2Position.y, v2Position.x); + rightLegDirection = (v2Position*leg - posNormal*obstacleRadius) / vertexDistSq; + } else { + rightLegDirection = segmentDir; + } + } + + // Legs should never point into the obstacle for legs added by convex vertices. + // The neighbouring vertex will add a better obstacle for those cases. + // + // In that case we replace the legs with the neighbouring segments, and if the closest + // point is on those segments we know we can ignore them because the + // neighbour will handle it. + // + // It's important that we don't include the case when they are colinear, + // because if v1=v0 (or v2=v3), which can happen at the end of a chain, the + // determinant will always be zero and so they will seem colinear. + // + // Note: One might think that this should apply to all vertices, not just convex ones. + // Consider this case where you might think a non-convex vertices otherwise would + // cause 'ghost' obstacles: + // ___ + // | | A + // | | + // | \ + // |____\ B + // <-X + // + // If X is an agent, moving to the left. It could get stuck against the segment A. + // This is because the vertex between A and B is concave, and it will generate a leg + // pointing downwards. + // + // However, this does not cause a problem in practice. Because if the horizontal segment at the bottom is added first (as it should be) + // then A and B will be discarded since they will be completely behind the ORCA line added by the horizontal segment. + bool isLeftLegForeign = false; + bool isRightLegForeign = false; + if (v1Convex && left(leftLegDirection, v0Position - v1Position)) { + // Left leg points into obstacle + leftLegDirection = v0Position - v1Position; + isLeftLegForeign = true; + } + + if (v2Convex && right(rightLegDirection, v3Position - v2Position)) { + // Right leg points into obstacle + rightLegDirection = v3Position - v2Position; + isRightLegForeign = true; + } + + + // The velocity obstacle for this segment consists of a left leg, right leg, + // a cutoff line, and two circular arcs where the legs and the cutoff line join together. + // LeftLeg RightLeg + // \ _____________________________ / + // \ _/ \_ _/ \_ / + // \ / \ / \ / + // \| v1 | | v2 |/ + // \_ _/ \_ _/ + // \_____/_________________\_____/ + // Cutoff Line + // + // In case only one vertex makes up the obstacle then we instead have just a left leg, right leg, and a single circular arc. + // + // LeftLeg RightLeg + // \ _____ / + // \ _/ \_ / + // \ / \ / + // \| |/ + // \_ _/ + // \_____/ + // + + + // We first check if the velocity will be projected on those circular segments. + var leftCutoff = inverseObstacleTimeHorizon * v1Position; + var rightCutoff = inverseObstacleTimeHorizon * v2Position; + var cutoffDir = rightCutoff - leftCutoff; + var cutoffLength = math.lengthsq(cutoffDir); + + // Projection on the cutoff line (between 0 and 1 if the projection is on the cutoff segment) + var t = cutoffLength <= 0.00001f ? 0.5f : math.dot(optimalVelocity - leftCutoff, cutoffDir)/cutoffLength; + // Negative if the closest point on the rays reprensenting the legs is before the ray starts + var tLeft = math.dot(optimalVelocity - leftCutoff, leftLegDirection); + var tRight = math.dot(optimalVelocity - rightCutoff, rightLegDirection); + + + // Check if the projected velocity is on the circular arcs + if ((t < 0.0f && tLeft < 0.0f) || (t > 1.0f && tRight < 0.0f) || (cutoffLength <= 0.00001f && tLeft < 0.0f && tRight < 0.0f)) { + var arcCenter = t <= 0.5f ? leftCutoff : rightCutoff; + + var unitW = math.normalizesafe(optimalVelocity - arcCenter); + orcaLineToAgent[numLines] = -1; + orcaLines[numLines++] = new ORCALine { + point = arcCenter + obstacleRadius * inverseObstacleTimeHorizon * unitW, + direction = new float2(unitW.y, -unitW.x), + }; + continue; + } + + // If the closest point is not on the arcs, then we project it on the legs or the cutoff line and pick the closest one. + // Note that all these distances should be reduced by obstacleRadius, but we only compare the values, so this doesn't matter. + float distToCutoff = (t > 1.0f || t < 0.0f || cutoffLength < 0.0001f ? math.INFINITY : math.lengthsq(optimalVelocity - (leftCutoff + t * cutoffDir))); + float distToLeftLeg = (tLeft < 0.0f ? math.INFINITY : math.lengthsq(optimalVelocity - (leftCutoff + tLeft * leftLegDirection))); + float distToRightLeg = (tRight < 0.0f ? math.INFINITY : math.lengthsq(optimalVelocity - (rightCutoff + tRight * rightLegDirection))); + var selected = 0; + var mn = distToCutoff; + if (distToLeftLeg < mn) { + mn = distToLeftLeg; + selected = 1; + } + if (distToRightLeg < mn) { + mn = distToRightLeg; + selected = 2; + } + + if (selected == 0) { + // Project on cutoff line + orcaLineToAgent[numLines] = -1; + orcaLines[numLines++] = new ORCALine { + point = leftCutoff + obstacleRadius * inverseObstacleTimeHorizon * new float2(segmentDir.y, -segmentDir.x), + direction = -segmentDir, + }; + } else if (selected == 1) { + if (!isLeftLegForeign) { + orcaLineToAgent[numLines] = -1; + orcaLines[numLines++] = new ORCALine { + point = leftCutoff + obstacleRadius * inverseObstacleTimeHorizon * new float2(-leftLegDirection.y, leftLegDirection.x), + direction = leftLegDirection, + }; + } + } else if (selected == 2) { + if (!isRightLegForeign) { + orcaLineToAgent[numLines] = -1; + orcaLines[numLines++] = new ORCALine { + point = rightCutoff + obstacleRadius * inverseObstacleTimeHorizon * new float2(rightLegDirection.y, -rightLegDirection.x), + direction = -rightLegDirection, + }; + } + } + } + MarkerConvertObstacles2.End(); + } + } + + public void ExecuteORCA (int startIndex, int batchSize) { + int endIndex = startIndex + batchSize; + + NativeArray<ORCALine> orcaLines = new NativeArray<ORCALine>(SimulatorBurst.MaxNeighbourCount + MaxObstacleCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + NativeArray<ORCALine> scratchBuffer = new NativeArray<ORCALine>(SimulatorBurst.MaxNeighbourCount + MaxObstacleCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + NativeArray<float> segmentDistancesScratch = new NativeArray<float>(SimulatorBurst.MaxObstacleVertices, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + NativeArray<int> sortedVerticesScratch = new NativeArray<int>(SimulatorBurst.MaxObstacleVertices, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + NativeArray<int2> adjacentObstacleVertices = new NativeArray<int2>(4 * SimulatorBurst.MaxObstacleVertices, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + NativeArray<int> orcaLineToAgent = new NativeArray<int>(SimulatorBurst.MaxNeighbourCount + MaxObstacleCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + NativeList<int> adjacentObstacleIdsScratch = new NativeList<int>(16, Allocator.Temp); + + for (int agentIndex = startIndex; agentIndex < endIndex; agentIndex++) { + if (!agentData.version[agentIndex].Valid) continue; + + if (agentData.manuallyControlled[agentIndex]) { + output.speed[agentIndex] = agentData.desiredSpeed[agentIndex]; + output.targetPoint[agentIndex] = agentData.targetPoint[agentIndex]; + output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount] = -1; + continue; + } + + var position = agentData.position[agentIndex]; + + if (agentData.locked[agentIndex]) { + output.speed[agentIndex] = 0; + output.targetPoint[agentIndex] = position; + output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount] = -1; + continue; + } + + MovementPlaneWrapper movementPlane = default; + movementPlane.Set(agentData.movementPlane[agentIndex]); + + // The RVO algorithm assumes we will continue to + // move in roughly the same direction + float2 optimalVelocity = movementPlane.ToPlane(temporaryAgentData.currentVelocity[agentIndex]); + int numLines = 0; + // TODO: Obstacles are typically behind agents, so it's better to add the agent orca lines first to improve culling. + // However, the 3D optimization program requires obstacle lines to be added first. Not to mention that the culling + // is not strictly accurate for fixed obstacle since they cannot be moved backwards by the 3D linear program. + GenerateObstacleVOs(agentIndex, adjacentObstacleIdsScratch, adjacentObstacleVertices, segmentDistancesScratch, sortedVerticesScratch, orcaLines, orcaLineToAgent, ref numLines, in movementPlane, optimalVelocity); + int numFixedLines = numLines; + + var neighbours = temporaryAgentData.neighbours.Slice(agentIndex*SimulatorBurst.MaxNeighbourCount, SimulatorBurst.MaxNeighbourCount); + + float agentTimeHorizon = agentData.agentTimeHorizon[agentIndex]; + float inverseAgentTimeHorizon = math.rcp(agentTimeHorizon); + float priority = agentData.priority[agentIndex]; + + var localPosition = movementPlane.ToPlane(position); + var agentRadius = agentData.radius[agentIndex]; + + for (int neighbourIndex = 0; neighbourIndex < neighbours.Length; neighbourIndex++) { + int otherIndex = neighbours[neighbourIndex]; + // Indicates that there are no more neighbours (see JobRVOCalculateNeighbours) + if (otherIndex == -1) break; + + var otherPosition = agentData.position[otherIndex]; + var relativePosition = movementPlane.ToPlane(otherPosition - position); + float combinedRadius = agentRadius + agentData.radius[otherIndex]; + + var otherPriority = agentData.priority[otherIndex] * priorityMultiplier; + + // TODO: Remove branches to possibly vectorize + float avoidanceStrength; + if (agentData.locked[otherIndex] || agentData.manuallyControlled[otherIndex]) { + avoidanceStrength = 1; + } else if (otherPriority > 0.00001f || priority > 0.00001f) { + avoidanceStrength = otherPriority / (priority + otherPriority); + } else { + // Both this agent's priority and the other agent's priority is zero or negative + // Assume they have the same priority + avoidanceStrength = 0.5f; + } + + // We assume that the other agent will continue to move with roughly the same velocity if the priorities for the agents are similar. + // If the other agent has a higher priority than this agent (avoidanceStrength > 0.5) then we will assume it will move more along its + // desired velocity. This will have the effect of other agents trying to clear a path for where a high priority agent wants to go. + // If this is not done then even high priority agents can get stuck when it is really crowded and they have had to slow down. + float2 otherOptimalVelocity = movementPlane.ToPlane(math.lerp(temporaryAgentData.currentVelocity[otherIndex], temporaryAgentData.desiredVelocity[otherIndex], math.clamp(2*avoidanceStrength - 1, 0, 1))); + + if (agentData.flowFollowingStrength[otherIndex] > 0) { + // When flow following strength is 1 the component of the other agent's velocity that is in the direction of this agent is removed. + // That is, we pretend that the other agent does not move towards this agent at all. + // This will make it impossible for the other agent to "push" this agent away. + var strength = agentData.flowFollowingStrength[otherIndex] * agentData.flowFollowingStrength[agentIndex]; + var relativeDir = math.normalizesafe(relativePosition); + otherOptimalVelocity -= relativeDir * (strength * math.min(0, math.dot(otherOptimalVelocity, relativeDir))); + } + + var dist = math.length(relativePosition); + // Figure out an approximate time to collision. We avoid using the current velocities of the agents because that leads to oscillations, + // as the agents change their velocities, which results in a change to the time to collision, which makes them change their velocities again. + var minimumTimeToCollision = math.max(0, dist - combinedRadius) / math.max(combinedRadius, agentData.desiredSpeed[agentIndex] + agentData.desiredSpeed[otherIndex]); + + // Adjust the radius to make the avoidance smoother. + // The agent will slowly start to take another agent into account instead of making a sharp turn. + float normalizedTime = minimumTimeToCollision * inverseAgentTimeHorizon; + // normalizedTime <= 0.5 => 0% effect + // normalizedTime = 1.0 => 100% effect + var factor = math.clamp((normalizedTime - 0.5f)*2.0f, 0, 1); + combinedRadius *= 1 - factor; + + // Adjust the time horizon to make the agent approach another agent less conservatively. + // This makes the velocity curve closer to sqrt(1-t) instead of exp(-t) as it comes to a stop, which looks nicer. + var tempInverseTimeHorizon = 1.0f/math.max(0.1f*agentTimeHorizon, agentTimeHorizon * math.clamp(math.sqrt(2f*minimumTimeToCollision), 0, 1)); + + orcaLines[numLines] = new ORCALine(localPosition, relativePosition, optimalVelocity, otherOptimalVelocity, combinedRadius, 0.1f, tempInverseTimeHorizon); + orcaLineToAgent[numLines] = otherIndex; + numLines++; +#if UNITY_EDITOR + if (agentData.HasDebugFlag(agentIndex, AgentDebugFlags.AgentVOs)) { + draw.PushMatrix(math.mul(float4x4.TRS(position, quaternion.identity, 1), movementPlane.matrix)); + var voCenter = math.lerp(optimalVelocity, otherOptimalVelocity, 0.5f); + DrawVO(draw, relativePosition * tempInverseTimeHorizon + otherOptimalVelocity, combinedRadius * tempInverseTimeHorizon, otherOptimalVelocity, Color.black); + draw.PopMatrix(); + } +#endif + } + + // Add an obstacle for the collision normal. + // This is mostly deprecated, but kept for compatibility. + var collisionNormal = math.normalizesafe(movementPlane.ToPlane(agentData.collisionNormal[agentIndex])); + if (math.any(collisionNormal != 0)) { + orcaLines[numLines] = new ORCALine { + point = float2.zero, + direction = new float2(collisionNormal.y, -collisionNormal.x), + }; + orcaLineToAgent[numLines] = -1; + numLines++; + } + + var desiredVelocity = movementPlane.ToPlane(temporaryAgentData.desiredVelocity[agentIndex]); + var desiredTargetPointInVelocitySpace = temporaryAgentData.desiredTargetPointInVelocitySpace[agentIndex]; + var originalDesiredVelocity = desiredVelocity; + var symmetryBias = symmetryBreakingBias * (1 - agentData.flowFollowingStrength[agentIndex]); + // Bias the desired velocity to avoid symmetry issues (esp. when two agents are heading straight towards one another). + // Do not bias velocities if the agent is heading towards an obstacle (not an agent). + bool insideAnyVO = BiasDesiredVelocity(orcaLines.AsUnsafeSpan().Slice(numFixedLines, numLines - numFixedLines), ref desiredVelocity, ref desiredTargetPointInVelocitySpace, symmetryBias); + // If the velocity is outside all agent orca half-planes, do a more thorough check of all orca lines (including obstacles). + insideAnyVO = insideAnyVO || DistanceInsideVOs(orcaLines.AsUnsafeSpan().Slice(0, numLines), desiredVelocity) > 0; + + +#if UNITY_EDITOR + if (agentData.HasDebugFlag(agentIndex, AgentDebugFlags.ObstacleVOs)) { + draw.PushColor(new Color(1, 1, 1, 0.2f)); + draw.PushMatrix(math.mul(float4x4.TRS(position, quaternion.identity, 1), movementPlane.matrix)); + for (int i = 0; i < numLines; i++) { + orcaLines[i].DrawAsHalfPlane(draw, agentData.radius[agentIndex] * 5.0f, 1.0f, i >= numFixedLines ? Color.magenta : Color.Lerp(Color.magenta, Color.black, 0.5f)); + } + draw.PopMatrix(); + draw.PopColor(); + } +#endif + + if (!insideAnyVO && math.all(math.abs(temporaryAgentData.collisionVelocityOffsets[agentIndex]) < 0.001f)) { + // Desired velocity can be used directly since it was not inside any velocity obstacle. + // No need to run optimizer because this will be the global minima. + // This is also a special case in which we can set the + // calculated target point to the desired target point + // instead of calculating a point based on a calculated velocity + // which is an important difference when the agent is very close + // to the target point + // TODO: Not actually guaranteed to be global minima if desiredTargetPointInVelocitySpace.magnitude < desiredSpeed + // maybe do something different here? +#if UNITY_EDITOR + if (agentData.HasDebugFlag(agentIndex, AgentDebugFlags.DesiredVelocity)) { + draw.xy.Cross(movementPlane.ToWorld(localPosition + desiredVelocity), Color.magenta); + draw.xy.Cross(movementPlane.ToWorld(localPosition + desiredTargetPointInVelocitySpace), Color.yellow); + } +#endif + + output.targetPoint[agentIndex] = position + movementPlane.ToWorld(desiredTargetPointInVelocitySpace, 0); + output.speed[agentIndex] = agentData.desiredSpeed[agentIndex]; + output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount] = -1; + output.forwardClearance[agentIndex] = float.PositiveInfinity; + } else { + var maxSpeed = agentData.maxSpeed[agentIndex]; + var allowedVelocityDeviationAngles = agentData.allowedVelocityDeviationAngles[agentIndex]; + LinearProgram2Output lin; + if (math.all(allowedVelocityDeviationAngles == 0)) { + // Common case, the desired velocity is a point + lin = LinearProgram2D(orcaLines, numLines, maxSpeed, desiredVelocity, false); + } else { + // The desired velocity is a segment, not a point + + // Rotate the desired velocity allowedVelocityDeviationAngles.x radians and allowedVelocityDeviationAngles.y radians respectively + math.sincos(allowedVelocityDeviationAngles, out float2 s, out float2 c); + var xs = desiredVelocity.x*c - desiredVelocity.y*s; + var ys = desiredVelocity.x*s + desiredVelocity.y*c; + var desiredVelocityLeft = new float2(xs.x, ys.x); + var desiredVelocityRight = new float2(xs.y, ys.y); + + var desiredVelocityLeftDir = desiredVelocity - desiredVelocityLeft; + + // Normalize and store length + var desiredVelocityLeftSegmentLength = math.length(desiredVelocityLeftDir); + desiredVelocityLeftDir = math.select(float2.zero, desiredVelocityLeftDir * math.rcp(desiredVelocityLeftSegmentLength), desiredVelocityLeftSegmentLength > math.FLT_MIN_NORMAL); + + var desiredVelocityRightDir = desiredVelocity - desiredVelocityRight; + var desiredVelocityRightSegmentLength = math.length(desiredVelocityRightDir); + desiredVelocityRightDir = math.select(float2.zero, desiredVelocityRightDir * math.rcp(desiredVelocityRightSegmentLength), desiredVelocityRightSegmentLength > math.FLT_MIN_NORMAL); + + // var tOptimal = ClosestPointOnSegment(desiredVelocityLeft, desiredVelocityDir, desiredVelocity, 0, desiredVelocitySegmentLength); + + var lin1 = LinearProgram2DSegment(orcaLines, numLines, maxSpeed, desiredVelocityLeft, desiredVelocityLeftDir, 0, desiredVelocityLeftSegmentLength, 1.0f); + var lin2 = LinearProgram2DSegment(orcaLines, numLines, maxSpeed, desiredVelocityRight, desiredVelocityRightDir, 0, desiredVelocityRightSegmentLength, 1.0f); + + if (lin1.firstFailedLineIndex < lin2.firstFailedLineIndex) { + lin = lin1; + } else if (lin2.firstFailedLineIndex < lin1.firstFailedLineIndex) { + lin = lin2; + } else { + lin = math.lengthsq(lin1.velocity - desiredVelocity) < math.lengthsq(lin2.velocity - desiredVelocity) ? lin1 : lin2; + } + } + + float2 newVelocity; + if (lin.firstFailedLineIndex < numLines) { + newVelocity = lin.velocity; + LinearProgram3D(orcaLines, numLines, numFixedLines, lin.firstFailedLineIndex, maxSpeed, ref newVelocity, scratchBuffer); + } else { + newVelocity = lin.velocity; + } + +#if UNITY_EDITOR + if (agentData.HasDebugFlag(agentIndex, AgentDebugFlags.ChosenVelocity)) { + draw.xy.Cross(position + movementPlane.ToWorld(newVelocity), Color.white); + draw.Arrow(position + movementPlane.ToWorld(desiredVelocity), position + movementPlane.ToWorld(newVelocity), Color.magenta); + } +#endif + + var blockedByAgentCount = 0; + for (int i = 0; i < numLines && blockedByAgentCount < SimulatorBurst.MaxBlockingAgentCount; i++) { + if (orcaLineToAgent[i] != -1 && det(orcaLines[i].direction, orcaLines[i].point - newVelocity) >= -0.001f) { + // We are blocked by this line + output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount + blockedByAgentCount] = orcaLineToAgent[i]; + blockedByAgentCount++; + } + } + if (blockedByAgentCount < SimulatorBurst.MaxBlockingAgentCount) output.blockedByAgents[agentIndex*SimulatorBurst.MaxBlockingAgentCount + blockedByAgentCount] = -1; + + var collisionVelocityOffset = temporaryAgentData.collisionVelocityOffsets[agentIndex]; + if (math.any(collisionVelocityOffset != 0)) { + // Make the agent move to avoid intersecting other agents (hard collisions) + newVelocity += temporaryAgentData.collisionVelocityOffsets[agentIndex]; + + // Adding the collision offset may have made the velocity invalid, causing it to intersect the wall-velocity-obstacles. + // We run a second optimization on only the wall-velocity-obstacles to make sure the velocity is valid. + newVelocity = LinearProgram2D(orcaLines, numFixedLines, maxSpeed, newVelocity, false).velocity; + } + + output.targetPoint[agentIndex] = position + movementPlane.ToWorld(newVelocity, 0); + output.speed[agentIndex] = math.min(math.length(newVelocity), maxSpeed); + + var targetDir = math.normalizesafe(movementPlane.ToPlane(agentData.targetPoint[agentIndex] - position)); + var forwardClearance = CalculateForwardClearance(neighbours, movementPlane, position, agentRadius, targetDir); + output.forwardClearance[agentIndex] = forwardClearance; + if (agentData.HasDebugFlag(agentIndex, AgentDebugFlags.ForwardClearance) && forwardClearance < float.PositiveInfinity) { + draw.PushLineWidth(2); + draw.Ray(position, movementPlane.ToWorld(targetDir) * forwardClearance, Color.red); + draw.PopLineWidth(); + } + } + } + } + + /// <summary> + /// Find the distance we can move towards our target without colliding with anything. + /// May become negative if we are currently colliding with something. + /// </summary> + float CalculateForwardClearance (NativeSlice<int> neighbours, MovementPlaneWrapper movementPlane, float3 position, float radius, float2 targetDir) { + // TODO: Take obstacles into account. + var smallestIntersectionDistance = float.PositiveInfinity; + for (int i = 0; i < neighbours.Length; i++) { + var other = neighbours[i]; + if (other == -1) break; + var otherPosition = agentData.position[other]; + var combinedRadius = radius + agentData.radius[other]; + // Intersect the ray from our agent towards the destination and check the distance to the intersection with the other agent. + var otherDir = movementPlane.ToPlane(otherPosition - position); + // Squared cosine of the angle between otherDir and ourTargetDir + var cosAlpha = math.dot(math.normalizesafe(otherDir), targetDir); + + // Check if the agent is behind us + if (cosAlpha < 0) continue; + + var distToOtherSq = math.lengthsq(otherDir); + var distToClosestPointAlongRay = math.sqrt(distToOtherSq) * cosAlpha; + var discriminant = combinedRadius*combinedRadius - (distToOtherSq - distToClosestPointAlongRay*distToClosestPointAlongRay); + + // Check if we have any intersection at all + if (discriminant < 0) continue; + var distToIntersection = distToClosestPointAlongRay - math.sqrt(discriminant); + smallestIntersectionDistance = math.min(smallestIntersectionDistance, distToIntersection); + } + return smallestIntersectionDistance; + } + + /// <summary>True if vector2 is to the left of vector1 or if they are colinear.</summary> + static bool leftOrColinear (float2 vector1, float2 vector2) { + return det(vector1, vector2) >= 0; + } + + /// <summary>True if vector2 is to the left of vector1.</summary> + static bool left (float2 vector1, float2 vector2) { + return det(vector1, vector2) > 0; + } + + /// <summary>True if vector2 is to the right of vector1 or if they are colinear.</summary> + static bool rightOrColinear (float2 vector1, float2 vector2) { + return det(vector1, vector2) <= 0; + } + + /// <summary>True if vector2 is to the right of vector1.</summary> + static bool right (float2 vector1, float2 vector2) { + return det(vector1, vector2) < 0; + } + + /// <summary> + /// Determinant of the 2x2 matrix defined by vector1 and vector2. + /// Alternatively, the Z component of the cross product of vector1 and vector2. + /// </summary> + static float det (float2 vector1, float2 vector2) { + return vector1.x * vector2.y - vector1.y * vector2.x; + } + + static float2 rot90 (float2 v) { + return new float2(-v.y, v.x); + } + + /// <summary> + /// A half-plane defined as the line splitting plane. + /// + /// For ORCA purposes, the infeasible region of the half-plane is on the right side of the line. + /// </summary> + struct ORCALine { + public float2 point; + public float2 direction; + + public void DrawAsHalfPlane (CommandBuilder draw, float halfPlaneLength, float halfPlaneWidth, Color color) { + var normal = new float2(direction.y, -direction.x); + draw.xy.Line(point - direction*10, point + direction*10, color); + + var p = point + normal*halfPlaneWidth*0.5f; + draw.SolidBox(new float3(p, 0), quaternion.RotateZ(math.atan2(direction.y, direction.x)), new float3(halfPlaneLength, halfPlaneWidth, 0.01f), new Color(0, 0, 0, 0.5f)); + } + + public ORCALine(float2 position, float2 relativePosition, float2 velocity, float2 otherVelocity, float combinedRadius, float timeStep, float invTimeHorizon) { + var relativeVelocity = velocity - otherVelocity; + float combinedRadiusSq = combinedRadius*combinedRadius; + float distSq = math.lengthsq(relativePosition); + + if (distSq > combinedRadiusSq) { + combinedRadius *= 1.001f; + // No collision + + // A velocity obstacle is built which is shaped like a truncated cone (see ORCA paper). + // The cone is truncated by an arc centered at relativePosition/timeHorizon + // with radius combinedRadius/timeHorizon. + // The cone extends in the direction of relativePosition. + + // Vector from truncation arc center to relative velocity + var w = relativeVelocity - invTimeHorizon * relativePosition; + var wLengthSq = math.lengthsq(w); + + float dot1 = math.dot(w, relativePosition); + + if (dot1 < 0.0f && dot1*dot1 > combinedRadiusSq * wLengthSq) { + // Project on cut-off circle + float wLength = math.sqrt(wLengthSq); + var normalizedW = w / wLength; + + direction = new float2(normalizedW.y, -normalizedW.x); + var u = (combinedRadius * invTimeHorizon - wLength) * normalizedW; + point = velocity + 0.5f * u; + } else { + // Project on legs + // Distance from the agent to the point where the "legs" start on the VO + float legDistance = math.sqrt(distSq - combinedRadiusSq); + + if (det(relativePosition, w) > 0.0f) { + // Project on left leg + // Note: This vector is actually normalized + direction = (relativePosition * legDistance + new float2(-relativePosition.y, relativePosition.x) * combinedRadius) / distSq; + } else { + // Project on right leg + // Note: This vector is actually normalized + direction = (-relativePosition * legDistance + new float2(-relativePosition.y, relativePosition.x) * combinedRadius) / distSq; + } + + float dot2 = math.dot(relativeVelocity, direction); + var u = dot2 * direction - relativeVelocity; + point = velocity + 0.5f * u; + } + } else { + float invTimeStep = math.rcp(timeStep); + var dist = math.sqrt(distSq); + var normalizedDir = math.select(0, relativePosition / dist, dist > math.FLT_MIN_NORMAL); + var u = normalizedDir * (dist - combinedRadius - 0.001f) * 0.3f * invTimeStep; + direction = math.normalizesafe(new float2(u.y, -u.x)); + point = math.lerp(velocity, otherVelocity, 0.5f) + u * 0.5f; + + + // Original code, the above is a version which works better + // Collision + // Project on cut-off circle of timeStep + //float invTimeStep = 1.0f / timeStep; + // Vector from cutoff center to relative velocity + //float2 w = relativeVelocity - invTimeStep * relativePosition; + //float wLength = math.length(w); + //float2 unitW = w / wLength; + //direction = new float2(unitW.y, -unitW.x); + //var u = (combinedRadius * invTimeStep - wLength) * unitW; + //point = velocity + 0.5f * u; + } + } + } + + /// <summary> + /// Calculates how far inside the infeasible region of the ORCA half-planes the velocity is. + /// Returns 0 if the velocity is in the feasible region of all half-planes. + /// </summary> + static float DistanceInsideVOs (UnsafeSpan<ORCALine> lines, float2 velocity) { + float maxDistance = 0.0f; + + for (int i = 0; i < lines.Length; i++) { + var distance = det(lines[i].direction, lines[i].point - velocity); + maxDistance = math.max(maxDistance, distance); + } + + return maxDistance; + } + + /// <summary> + /// Bias towards the right side of agents. + /// Rotate desiredVelocity at most [value] number of radians. 1 radian ≈ 57° + /// This breaks up symmetries. + /// + /// The desired velocity will only be rotated if it is inside a velocity obstacle (VO). + /// If it is inside one, it will not be rotated further than to the edge of it + /// + /// The targetPointInVelocitySpace will be rotated by the same amount as the desired velocity + /// + /// Returns: True if the desired velocity was inside any VO + /// </summary> + static bool BiasDesiredVelocity (UnsafeSpan<ORCALine> lines, ref float2 desiredVelocity, ref float2 targetPointInVelocitySpace, float maxBiasRadians) { + float maxDistance = DistanceInsideVOs(lines, desiredVelocity); + + if (maxDistance == 0.0f) return false; + + var desiredVelocityMagn = math.length(desiredVelocity); + + // Avoid division by zero below + if (desiredVelocityMagn >= 0.001f) { + // Rotate the desired velocity clockwise (to the right) at most maxBiasRadians number of radians. + // We clamp the angle so that we do not rotate more than to the edge of the VO. + // Assuming maxBiasRadians is small, we can just move it instead and it will give approximately the same effect. + // See https://en.wikipedia.org/wiki/Small-angle_approximation + var angle = math.min(maxBiasRadians, maxDistance / desiredVelocityMagn); + desiredVelocity += new float2(desiredVelocity.y, -desiredVelocity.x) * angle; + targetPointInVelocitySpace += new float2(targetPointInVelocitySpace.y, -targetPointInVelocitySpace.x) * angle; + } + return true; + } + + /// <summary> + /// Clip a line to the feasible region of the half-plane given by the clipper. + /// The clipped line is `line.point + line.direction*tLeft` to `line.point + line.direction*tRight`. + /// + /// Returns false if the line is parallel to the clipper's border. + /// </summary> + static bool ClipLine (ORCALine line, ORCALine clipper, ref float tLeft, ref float tRight) { + float denominator = det(line.direction, clipper.direction); + float numerator = det(clipper.direction, line.point - clipper.point); + + if (math.abs(denominator) < 0.0001f) { + // The two lines are almost parallel + return false; + } + + float t = numerator / denominator; + + if (denominator >= 0.0f) { + // Line i bounds the line on the right + tRight = math.min(tRight, t); + } else { + // Line i bounds the line on the left + tLeft = math.max(tLeft, t); + } + return true; + } + + static bool ClipBoundary (NativeArray<ORCALine> lines, int lineIndex, float radius, out float tLeft, out float tRight) { + var line = lines[lineIndex]; + if (!VectorMath.LineCircleIntersectionFactors(line.point, line.direction, radius, out tLeft, out tRight)) { + return false; + } + + // Go through all previous lines/half-planes and clip the current line against them + for (int i = 0; i < lineIndex; i++) { + float denominator = det(line.direction, lines[i].direction); + float numerator = det(lines[i].direction, line.point - lines[i].point); + + if (math.abs(denominator) < 0.0001f) { + // The two lines are almost parallel + if (numerator < 0.0f) { + // This line is completely "behind" the other line. So we can ignore it. + return false; + } else continue; + } + + float t = numerator / denominator; + + if (denominator >= 0.0f) { + // Line i bounds the line on the right + tRight = math.min(tRight, t); + } else { + // Line i bounds the line on the left + tLeft = math.max(tLeft, t); + } + + if (tLeft > tRight) { + // The line is completely outside the previous half-planes + return false; + } + } + return true; + } + + static bool LinearProgram1D (NativeArray<ORCALine> lines, int lineIndex, float radius, float2 optimalVelocity, bool directionOpt, ref float2 result) { + if (!ClipBoundary(lines, lineIndex, radius, out float tLeft, out float tRight)) return false; + var line = lines[lineIndex]; + + if (directionOpt) { + // Optimize direction + if (math.dot(optimalVelocity, line.direction) > 0.0f) { + // Take right extreme + result = line.point + tRight * line.direction; + } else { + // Take left extreme + result = line.point + tLeft * line.direction; + } + } else { + // Optimize closest point + float t = math.dot(line.direction, optimalVelocity - line.point); + result = line.point + math.clamp(t, tLeft, tRight) * line.direction; + } + return true; + } + + struct LinearProgram2Output { + public float2 velocity; + public int firstFailedLineIndex; + } + + static LinearProgram2Output LinearProgram2D (NativeArray<ORCALine> lines, int numLines, float radius, float2 optimalVelocity, bool directionOpt) { + float2 result; + + if (directionOpt) { + // Optimize direction. Note that the optimization velocity is of unit length in this case + result = optimalVelocity * radius; + } else if (math.lengthsq(optimalVelocity) > radius*radius) { + // Optimize closest point and outside circle + result = math.normalize(optimalVelocity) * radius; + } else { + // Optimize closest point and inside circle + result = optimalVelocity; + } + + for (int i = 0; i < numLines; i++) { + // Check if point is in the infeasible region of the half-plane + if (det(lines[i].direction, lines[i].point - result) > 0.0f) { + // Result does not satisfy constraint i. Compute new optimal result + var tempResult = result; + if (!LinearProgram1D(lines, i, radius, optimalVelocity, directionOpt, ref result)) { + return new LinearProgram2Output { + velocity = tempResult, + firstFailedLineIndex = i, + }; + } + } + } + + return new LinearProgram2Output { + velocity = result, + firstFailedLineIndex = numLines, + }; + } + + static float ClosestPointOnSegment (float2 a, float2 dir, float2 p, float t0, float t1) { + return math.clamp(math.dot(p - a, dir), t0, t1); + } + + /// <summary> + /// Closest point on segment a to segment b. + /// The segments are given by infinite lines and bounded by t values. p = line.point + line.dir*t. + /// + /// It is assumed that the two segments do not intersect. + /// </summary> + static float2 ClosestSegmentSegmentPointNonIntersecting (ORCALine a, ORCALine b, float ta1, float ta2, float tb1, float tb2) { + // We know that the two segments do not intersect, so at least one of the closest points + // must be one of the line segment endpoints. + var ap0 = a.point + a.direction*ta1; + var ap1 = a.point + a.direction*ta2; + var bp0 = b.point + b.direction * tb1; + var bp1 = b.point + b.direction * tb2; + + var t0 = ClosestPointOnSegment(a.point, a.direction, bp0, ta1, ta2); + var t1 = ClosestPointOnSegment(a.point, a.direction, bp1, ta1, ta2); + var t2 = ClosestPointOnSegment(b.point, b.direction, ap0, tb1, tb2); + var t3 = ClosestPointOnSegment(b.point, b.direction, ap1, tb1, tb2); + + var c0 = a.point + a.direction * t0; + var c1 = a.point + a.direction * t1; + var c2 = b.point + b.direction * t2; + var c3 = b.point + b.direction * t3; + + var d0 = math.lengthsq(c0 - bp0); + var d1 = math.lengthsq(c1 - bp1); + var d2 = math.lengthsq(c2 - ap0); + var d3 = math.lengthsq(c3 - ap1); + + var result = c0; + var d = d0; + if (d1 < d) { + result = c1; + d = d1; + } + if (d2 < d) { + result = ap0; + d = d2; + } + if (d3 < d) { + result = ap1; + d = d3; + } + return result; + } + + /// <summary>Like LinearProgram2D, but the optimal velocity space is a segment instead of a point, however the current result has collapsed to a point</summary> + static LinearProgram2Output LinearProgram2DCollapsedSegment (NativeArray<ORCALine> lines, int numLines, int startLine, float radius, float2 currentResult, float2 optimalVelocityStart, float2 optimalVelocityDir, float optimalTLeft, float optimalTRight) { + for (int i = startLine; i < numLines; i++) { + // Check if point is in the infeasible region of the half-plane + if (det(lines[i].direction, lines[i].point - currentResult) > 0.0f) { + // Result does not satisfy constraint i. Compute new optimal result + if (!ClipBoundary(lines, i, radius, out float tLeft2, out float tRight2)) { + // We are partially not feasible, but no part of this constraint's boundary is in the feasible region. + // This means that there is no feasible solution at all. + return new LinearProgram2Output { + velocity = currentResult, + firstFailedLineIndex = i, + }; + } + + // Optimize closest point + currentResult = ClosestSegmentSegmentPointNonIntersecting(lines[i], new ORCALine { + point = optimalVelocityStart, + direction = optimalVelocityDir, + }, tLeft2, tRight2, optimalTLeft, optimalTRight); + } + } + + return new LinearProgram2Output { + velocity = currentResult, + firstFailedLineIndex = numLines, + }; + } + + /// <summary>Like LinearProgram2D, but the optimal velocity space is a segment instead of a point</summary> + static LinearProgram2Output LinearProgram2DSegment (NativeArray<ORCALine> lines, int numLines, float radius, float2 optimalVelocityStart, float2 optimalVelocityDir, float optimalTLeft, float optimalTRight, float optimalT) { + var hasIntersection = VectorMath.LineCircleIntersectionFactors(optimalVelocityStart, optimalVelocityDir, radius, out float resultTLeft, out float resultTRight); + resultTLeft = math.max(resultTLeft, optimalTLeft); + resultTRight = math.min(resultTRight, optimalTRight); + hasIntersection &= resultTLeft <= resultTRight; + + if (!hasIntersection) { + // In case the optimal velocity segment is not inside the max velocity circle, then collapse to a single optimal velocity which + // is closest segment point to the circle + var t = math.clamp(math.dot(-optimalVelocityStart, optimalVelocityDir), optimalTLeft, optimalTRight); + var closestOnCircle = math.normalizesafe(optimalVelocityStart + optimalVelocityDir * t) * radius; + + // The best point is now a single point, not a segment. + // So we can fall back to simpler code. + return LinearProgram2DCollapsedSegment(lines, numLines, 0, radius, closestOnCircle, optimalVelocityStart, optimalVelocityDir, optimalTLeft, optimalTRight); + } + + for (int i = 0; i < numLines; i++) { + // Check if optimal line segment is at least partially in the infeasible region of the half-plane + var line = lines[i]; + var leftInfeasible = det(line.direction, line.point - (optimalVelocityStart + optimalVelocityDir*resultTLeft)) > 0.0f; + var rightInfeasible = det(line.direction, line.point - (optimalVelocityStart + optimalVelocityDir*resultTRight)) > 0.0f; + if (leftInfeasible || rightInfeasible) { + if (!ClipBoundary(lines, i, radius, out float tLeft, out float tRight)) { + // We are partially not feasible, but no part of this constraint's boundary is in the feasible region. + // This means that there is no feasible solution at all. + return new LinearProgram2Output { + velocity = optimalVelocityStart + optimalVelocityDir * math.clamp(optimalT, resultTLeft, resultTRight), + firstFailedLineIndex = i, + }; + } + + // Check if the optimal line segment is completely in the infeasible region + if (leftInfeasible && rightInfeasible) { + if (math.abs(det(line.direction, optimalVelocityDir)) < 0.001f) { + // Lines are almost parallel. + // Project the optimal velocity on the boundary + var t1 = ClosestPointOnSegment(line.point, line.direction, optimalVelocityStart + optimalVelocityDir*resultTLeft, tLeft, tRight); + var t2 = ClosestPointOnSegment(line.point, line.direction, optimalVelocityStart + optimalVelocityDir*resultTRight, tLeft, tRight); + var t3 = ClosestPointOnSegment(line.point, line.direction, optimalVelocityStart + optimalVelocityDir*optimalT, tLeft, tRight); + optimalVelocityStart = line.point; + optimalVelocityDir = line.direction; + resultTLeft = t1; + resultTRight = t2; + optimalT = t3; + } else { + // Find closest point on the constraint boundary segment to the optimal velocity segment + var result = ClosestSegmentSegmentPointNonIntersecting(line, new ORCALine { + point = optimalVelocityStart, + direction = optimalVelocityDir, + }, tLeft, tRight, optimalTLeft, optimalTRight); + + // The best point is now a single point, not a segment. + // So we can fall back to simpler code. + return LinearProgram2DCollapsedSegment(lines, numLines, i+1, radius, result, optimalVelocityStart, optimalVelocityDir, optimalTLeft, optimalTRight); + } + } else { + // Clip optimal velocity segment to the constraint boundary. + // If this returns false and the lines are almost parallel, then we don't do anything + // because we already know they intersect. So the two lines must be almost identical. + ClipLine(new ORCALine { + point = optimalVelocityStart, + direction = optimalVelocityDir, + }, line, ref resultTLeft, ref resultTRight); + } + } + } + + var resultT = math.clamp(optimalT, resultTLeft, resultTRight); + + return new LinearProgram2Output { + velocity = optimalVelocityStart + optimalVelocityDir * resultT, + firstFailedLineIndex = numLines, + }; + } + + /// <summary> + /// Finds the velocity with the smallest maximum penetration into the given half-planes. + /// + /// Assumes there are no points in the feasible region of the given half-planes. + /// + /// Runs a 3-dimensional linear program, but projected down to 2D. + /// If there are no feasible regions outside all half-planes then we want to find the velocity + /// for which the maximum penetration into infeasible regions is minimized. + /// Conceptually we can solve this by taking our half-planes, and moving them outwards at a fixed speed + /// until there is exactly 1 feasible point. + /// We can formulate this in 3D space by thinking of the half-planes in 3D (velocity.x, velocity.y, penetration-depth) space, as sloped planes. + /// Moving the planes outwards then corresponds to decreasing the z coordinate. + /// In 3D space we want to find the point above all planes with the lowest z coordinate. + /// We do this by going through each plane and testing if it is possible that this plane + /// is the one with the maximum penetration. + /// If so, we know that the point will lie on the portion of that plane bounded by the intersections + /// with the other planes. We generate projected half-planes which represent the intersections with the + /// other 3D planes, and then we run a new optimization to find the point which penetrates this + /// half-plane the least. + /// </summary> + /// <param name="lines">The half-planes of all obstacles and agents.</param> + /// <param name="numLines">The number of half-planes in lines.</param> + /// <param name="numFixedLines">The number of half-planes in lines which are fixed (0..numFixedLines). These will be treated as static obstacles which should be avoided at all costs.</param> + /// <param name="beginLine">The index of the first half-plane in lines for which the previous optimization failed (see \reflink{LinearProgram2Output.firstFailedLineIndex}).</param> + /// <param name="radius">Maximum possible speed. This represents a circular velocity obstacle.</param> + /// <param name="result">Input is best velocity as output by \reflink{LinearProgram2D}. Output is the new best velocity. The velocity with the smallest maximum penetration into the given half-planes.</param> + /// <param name="scratchBuffer">A buffer of length at least numLines to use for scratch space.</param> + static void LinearProgram3D (NativeArray<ORCALine> lines, int numLines, int numFixedLines, int beginLine, float radius, ref float2 result, NativeArray<ORCALine> scratchBuffer) { + float distance = 0.0f; + + NativeArray<ORCALine> projectedLines = scratchBuffer; + NativeArray<ORCALine>.Copy(lines, projectedLines, numFixedLines); + + for (int i = beginLine; i < numLines; i++) { + // Check if #result is more than #distance units inside the infeasible region of the half-plane + if (det(lines[i].direction, lines[i].point - result) > distance) { + int numProjectedLines = numFixedLines; + for (int j = numFixedLines; j < i; j++) { + float determinant = det(lines[i].direction, lines[j].direction); + if (math.abs(determinant) < 0.001f) { + // Lines i and j are parallel + if (math.dot(lines[i].direction, lines[j].direction) > 0.0f) { + // Line i and j point in the same direction + continue; + } else { + // Line i and j point in the opposite direction + projectedLines[numProjectedLines] = new ORCALine { + point = 0.5f * (lines[i].point + lines[j].point), + direction = math.normalize(lines[j].direction - lines[i].direction), + }; + numProjectedLines++; + } + } else { + projectedLines[numProjectedLines] = new ORCALine { + // The intersection between the two lines + point = lines[i].point + (det(lines[j].direction, lines[i].point - lines[j].point) / determinant) * lines[i].direction, + // The direction along which the intersection of the two 3D-planes intersect (projected onto the XY plane) + direction = math.normalize(lines[j].direction - lines[i].direction), + }; + numProjectedLines++; + } + } + + var lin = LinearProgram2D(projectedLines, numProjectedLines, radius, new float2(-lines[i].direction.y, lines[i].direction.x), true); + if (lin.firstFailedLineIndex < numProjectedLines) { + // This should in principle not happen. The result is by definition + // already in the feasible region of this linear program. If it fails, + // it is due to small floating point error, and the current result is + // kept. + } else { + result = lin.velocity; + } + + distance = det(lines[i].direction, lines[i].point - result); + } + } + } + + static void DrawVO (CommandBuilder draw, float2 circleCenter, float radius, float2 origin, Color color) { +#if UNITY_EDITOR + draw.PushColor(color); + float alpha = math.atan2((origin - circleCenter).y, (origin - circleCenter).x); + float gamma = radius/math.length(origin-circleCenter); + float delta = gamma <= 1.0f ? math.abs(math.acos(gamma)) : 0; + + draw.xy.Circle(circleCenter, radius, alpha-delta, alpha+delta); + float2 p1 = new float2(math.cos(alpha-delta), math.sin(alpha-delta)) * radius; + float2 p2 = new float2(math.cos(alpha+delta), math.sin(alpha+delta)) * radius; + + float2 p1t = -new float2(-p1.y, p1.x); + float2 p2t = new float2(-p2.y, p2.x); + p1 += circleCenter; + p2 += circleCenter; + + draw.xy.Ray(p1, math.normalizesafe(p1t)*100); + draw.xy.Ray(p2, math.normalizesafe(p2t)*100); + draw.PopColor(); +#endif + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgentBurst.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgentBurst.cs.meta new file mode 100644 index 0000000..1e6a7e3 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOAgentBurst.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87a0a74f7df9c401eaeb12dab0863446 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreObstacle.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreObstacle.cs new file mode 100644 index 0000000..3b6ed49 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreObstacle.cs @@ -0,0 +1,4 @@ + +// This file has been removed from the project. Since UnityPackages cannot +// delete files, only replace them, this message is left here to prevent old +// files from causing compiler errors diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreObstacle.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreObstacle.cs.meta new file mode 100644 index 0000000..0811a7a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreObstacle.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 19c43d572022a4278a4d426f536b5ee4 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulator.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulator.cs new file mode 100644 index 0000000..3b6ed49 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulator.cs @@ -0,0 +1,4 @@ + +// This file has been removed from the project. Since UnityPackages cannot +// delete files, only replace them, this message is left here to prevent old +// files from causing compiler errors diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulator.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulator.cs.meta new file mode 100644 index 0000000..d9a2b4a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulator.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f373cccc6991444b0b8b6e8c512842db +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulatorBurst.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulatorBurst.cs new file mode 100644 index 0000000..bd93893 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulatorBurst.cs @@ -0,0 +1,1226 @@ +using UnityEngine; +using System.Collections.Generic; + +using Unity.Burst; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Collections; + +/// <summary>Local avoidance related classes</summary> +namespace Pathfinding.RVO { + using System; + using Pathfinding.Jobs; + using Pathfinding.Drawing; + using Pathfinding.Util; + using Pathfinding.ECS.RVO; + + public interface IMovementPlaneWrapper { + float2 ToPlane(float3 p); + float2 ToPlane(float3 p, out float elevation); + float3 ToWorld(float2 p, float elevation = 0); + Bounds ToWorld(Bounds bounds); + + /// <summary>Maps from 2D (X, Y, 0) coordinates to world coordinates</summary> + float4x4 matrix { get; } + void Set(NativeMovementPlane plane); + } + + public struct XYMovementPlane : IMovementPlaneWrapper { + public float2 ToPlane(float3 p) => p.xy; + public float2 ToPlane (float3 p, out float elevation) { + elevation = p.z; + return p.xy; + } + public float3 ToWorld(float2 p, float elevation = 0) => new float3(p.x, p.y, elevation); + public Bounds ToWorld (Bounds bounds) { + var center = bounds.center; + var size = bounds.size; + return new Bounds(new Vector3(center.x, center.z, center.y), new Vector3(size.x, size.z, size.y)); + } + + public float4x4 matrix { + get { + return float4x4.identity; + } + } + public void Set (NativeMovementPlane plane) { } + } + + public struct XZMovementPlane : IMovementPlaneWrapper { + public float2 ToPlane(float3 p) => p.xz; + public float2 ToPlane (float3 p, out float elevation) { + elevation = p.y; + return p.xz; + } + public float3 ToWorld(float2 p, float elevation = 0) => new float3(p.x, elevation, p.y); + public Bounds ToWorld(Bounds bounds) => bounds; + public void Set (NativeMovementPlane plane) { } + public float4x4 matrix => float4x4.RotateX(math.radians(90)); + } + + public struct ArbitraryMovementPlane : IMovementPlaneWrapper { + NativeMovementPlane plane; + + public float2 ToPlane(float3 p) => plane.ToPlane(p); + public float2 ToPlane(float3 p, out float elevation) => plane.ToPlane(p, out elevation); + public float3 ToWorld(float2 p, float elevation = 0) => plane.ToWorld(p, elevation); + public Bounds ToWorld(Bounds bounds) => plane.ToWorld(bounds); + public void Set (NativeMovementPlane plane) { + this.plane = plane; + } + public float4x4 matrix { + get { + return math.mul(float4x4.TRS(0, plane.rotation, 1), new float4x4( + new float4(1, 0, 0, 0), + new float4(0, 0, 1, 0), + new float4(0, 1, 0, 0), + new float4(0, 0, 0, 1) + )); + } + } + } + + public struct IReadOnlySlice<T> : System.Collections.Generic.IReadOnlyList<T> { + public T[] data; + public int length; + + public T this[int index] => data[index]; + + public int Count => length; + + public IEnumerator<T> GetEnumerator () { + throw new System.NotImplementedException(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () { + throw new System.NotImplementedException(); + } + } + + [System.Flags] + public enum AgentDebugFlags : byte { + Nothing = 0, + ObstacleVOs = 1 << 0, + AgentVOs = 1 << 1, + ReachedState = 1 << 2, + DesiredVelocity = 1 << 3, + ChosenVelocity = 1 << 4, + Obstacles = 1 << 5, + ForwardClearance = 1 << 6, + } + + /// <summary> + /// Exposes properties of an Agent class. + /// + /// See: RVOController + /// See: RVOSimulator + /// </summary> + public interface IAgent { + /// <summary> + /// Internal index of the agent. + /// See: <see cref="Pathfinding.RVO.SimulatorBurst.simulationData"/> + /// </summary> + int AgentIndex { get; } + + /// <summary> + /// Position of the agent. + /// The agent does not move by itself, a movement script has to be responsible for + /// reading the CalculatedTargetPoint and CalculatedSpeed properties and move towards that point with that speed. + /// This property should ideally be set every frame. + /// </summary> + Vector3 Position { get; set; } + + /// <summary> + /// Optimal point to move towards to avoid collisions. + /// The movement script should move towards this point with a speed of <see cref="CalculatedSpeed"/>. + /// + /// See: RVOController.CalculateMovementDelta. + /// </summary> + Vector3 CalculatedTargetPoint { get; } + + /// <summary> + /// True if the agent's movement is affected by any other agents or obstacles. + /// + /// If the agent is all alone, and can just move in a straight line to its target, this will be false. + /// If it has to adjust its velocity, even slightly, to avoid collisions, this will be true. + /// </summary> + bool AvoidingAnyAgents { get; } + + /// <summary> + /// Optimal speed of the agent to avoid collisions. + /// The movement script should move towards <see cref="CalculatedTargetPoint"/> with this speed. + /// </summary> + float CalculatedSpeed { get; } + + /// <summary> + /// Point towards which the agent should move. + /// Usually you set this once per frame. The agent will try move as close to the target point as possible. + /// Will take effect at the next simulation step. + /// + /// Note: The system assumes that the agent will stop when it reaches the target point + /// so if you just want to move the agent in a particular direction, make sure that you set the target point + /// a good distance in front of the character as otherwise the system may not avoid colisions that well. + /// What would happen is that the system (in simplified terms) would think that the agents would stop + /// before the collision and thus it wouldn't slow down or change course. See the image below. + /// In the image the desiredSpeed is the length of the blue arrow and the target point + /// is the point where the black arrows point to. + /// In the upper case the agent does not avoid the red agent (you can assume that the red + /// agent has a very small velocity for simplicity) while in the lower case it does. + /// If you are following a path a good way to pick the target point is to set it to + /// <code> + /// targetPoint = directionToNextWaypoint.normalized * remainingPathDistance + /// </code> + /// Where remainingPathDistance is the distance until the character would reach the end of the path. + /// This works well because at the end of the path the direction to the next waypoint will just be the + /// direction to the last point on the path and remainingPathDistance will be the distance to the last point + /// in the path, so targetPoint will be set to simply the last point in the path. However when remainingPathDistance + /// is large the target point will be so far away that the agent will essentially be told to move in a particular + /// direction, which is precisely what we want. + /// [Open online documentation to see images] + /// </summary> + /// <param name="targetPoint">Target point in world space.</param> + /// <param name="desiredSpeed">Desired speed of the agent. In world units per second. The agent will try to move with this + /// speed if possible.</param> + /// <param name="maxSpeed">Max speed of the agent. In world units per second. If necessary (for example if another agent + /// is on a collision trajectory towards this agent) the agent can move at this speed. + /// Should be at least as high as desiredSpeed, but it is recommended to use a slightly + /// higher value than desiredSpeed (for example desiredSpeed*1.2).</param> + /// <param name="endOfPath">Point in world space which is the agent's final desired destination on the navmesh. + /// This is typically the end of the path the agent is following. + /// May be set to (+inf,+inf,+inf) to mark the agent as not having a well defined end of path. + /// If this is set, multiple agents with roughly the same end of path will crowd more naturally around this point. + /// They will be able to realize that they cannot get closer if there are many agents trying to get closer to the same destination and then stop.</param> + void SetTarget(Vector3 targetPoint, float desiredSpeed, float maxSpeed, Vector3 endOfPath); + + /// <summary> + /// Plane in which the agent moves. + /// Local avoidance calculations are always done in 2D and this plane determines how to convert from 3D to 2D. + /// + /// In a typical 3D game the agents move in the XZ plane and in a 2D game they move in the XY plane. + /// By default this is set to the XZ plane. + /// + /// See: <see cref="Pathfinding.Util.GraphTransform.xyPlane"/> + /// See: <see cref="Pathfinding.Util.GraphTransform.xzPlane"/> + /// </summary> + Util.SimpleMovementPlane MovementPlane { get; set; } + + /// <summary>Locked agents will be assumed not to move</summary> + bool Locked { get; set; } + + /// <summary> + /// Radius of the agent in world units. + /// Agents are modelled as circles/cylinders. + /// </summary> + float Radius { get; set; } + + /// <summary> + /// Height of the agent in world units. + /// Agents are modelled as circles/cylinders. + /// </summary> + float Height { get; set; } + + /// <summary> + /// Max number of estimated seconds to look into the future for collisions with agents. + /// As it turns out, this variable is also very good for controling agent avoidance priorities. + /// Agents with lower values will avoid other agents less and thus you can make 'high priority agents' by + /// giving them a lower value. + /// </summary> + float AgentTimeHorizon { get; set; } + + /// <summary>Max number of estimated seconds to look into the future for collisions with obstacles</summary> + float ObstacleTimeHorizon { get; set; } + + /// <summary> + /// Max number of agents to take into account. + /// Decreasing this value can lead to better performance, increasing it can lead to better quality of the simulation. + /// </summary> + int MaxNeighbours { get; set; } + + /// <summary>Number of neighbours that the agent took into account during the last simulation step</summary> + int NeighbourCount { get; } + + /// <summary> + /// Specifies the avoidance layer for this agent. + /// The <see cref="CollidesWith"/> mask on other agents will determine if they will avoid this agent. + /// </summary> + RVOLayer Layer { get; set; } + + /// <summary> + /// Layer mask specifying which layers this agent will avoid. + /// You can set it as CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ... + /// + /// See: http://en.wikipedia.org/wiki/Mask_(computing) + /// See: bitmasks (view in online documentation for working links) + /// </summary> + RVOLayer CollidesWith { get; set; } + + /// <summary> + /// Determines how strongly this agent just follows the flow instead of making other agents avoid it. + /// The default value is 0, if it is greater than zero (up to the maximum value of 1) other agents will + /// not avoid this character as much. However it works in a different way to <see cref="Priority"/>. + /// + /// A group of agents with FlowFollowingStrength set to a high value that all try to reach the same point + /// will end up just settling to stationary positions around that point, none will push the others away to any significant extent. + /// This is tricky to achieve with priorities as priorities are all relative, so setting all agents to a low priority is the same thing + /// as not changing priorities at all. + /// + /// Should be a value in the range [0, 1]. + /// + /// TODO: Add video + /// </summary> + float FlowFollowingStrength { get; set; } + + /// <summary>Draw debug information in the scene view</summary> + AgentDebugFlags DebugFlags { get; set; } + + /// <summary> + /// How strongly other agents will avoid this agent. + /// Usually a value between 0 and 1. + /// Agents with similar priorities will avoid each other with an equal strength. + /// If an agent sees another agent with a higher priority than itself it will avoid that agent more strongly. + /// In the extreme case (e.g this agent has a priority of 0 and the other agent has a priority of 1) it will treat the other agent as being a moving obstacle. + /// Similarly if an agent sees another agent with a lower priority than itself it will avoid that agent less. + /// + /// In general the avoidance strength for this agent is: + /// <code> + /// if this.priority > 0 or other.priority > 0: + /// avoidanceStrength = other.priority / (this.priority + other.priority); + /// else: + /// avoidanceStrength = 0.5 + /// </code> + /// </summary> + float Priority { get; set; } + + int HierarchicalNodeIndex { get; set; } + + /// <summary> + /// Callback which will be called right before avoidance calculations are started. + /// Used to update the other properties with the most up to date values + /// </summary> + System.Action PreCalculationCallback { set; } + + /// <summary> + /// Callback which will be called right the agent is removed from the simulation. + /// This agent should not be used anymore after this callback has been called. + /// </summary> + System.Action DestroyedCallback { set; } + + /// <summary> + /// Set the normal of a wall (or something else) the agent is currently colliding with. + /// This is used to make the RVO system aware of things like physics or an agent being clamped to the navmesh. + /// The velocity of this agent that other agents observe will be modified so that there is no component + /// into the wall. The agent will however not start to avoid the wall, for that you will need to add RVO obstacles. + /// + /// This value will be cleared after the next simulation step, normally it should be set every frame + /// when the collision is still happening. + /// </summary> + void SetCollisionNormal(Vector3 normal); + + /// <summary> + /// Set the current velocity of the agent. + /// This will override the local avoidance input completely. + /// It is useful if you have a player controlled character and want other agents to avoid it. + /// + /// Calling this method will mark the agent as being externally controlled for 1 simulation step. + /// Local avoidance calculations will be skipped for the next simulation step but will be resumed + /// after that unless this method is called again. + /// </summary> + void ForceSetVelocity(Vector3 velocity); + + public ReachedEndOfPath CalculatedEffectivelyReachedDestination { get; } + + /// <summary> + /// Add obstacles to avoid for this agent. + /// + /// The obstacles are based on nearby borders of the navmesh. + /// You should call this method every frame. + /// </summary> + /// <param name="sourceNode">The node to start the obstacle search at. This is typically the node the agent is standing on.</param> + public void SetObstacleQuery(GraphNode sourceNode); + } + + /// <summary> + /// Type of obstacle shape. + /// See: <see cref="ObstacleVertexGroup"/> + /// </summary> + public enum ObstacleType { + /// <summary>A chain of vertices, the first and last segments end at a point</summary> + Chain, + /// <summary>A loop of vertices, the last vertex connects back to the first one</summary> + Loop, + } + + public struct ObstacleVertexGroup { + /// <summary>Type of obstacle shape</summary> + public ObstacleType type; + /// <summary>Number of vertices that this group consists of</summary> + public int vertexCount; + public float3 boundsMn; + public float3 boundsMx; + } + + /// <summary>Represents a set of obstacles</summary> + public struct UnmanagedObstacle { + /// <summary>The allocation in <see cref="ObstacleData.obstacleVertices"/> which represents all vertices used for these obstacles</summary> + public int verticesAllocation; + /// <summary>The allocation in <see cref="ObstacleData.obstacles"/> which represents the obstacle groups</summary> + public int groupsAllocation; + } + + // TODO: Change to byte? + public enum ReachedEndOfPath { + /// <summary>The agent has no reached the end of its path yet</summary> + NotReached, + /// <summary> + /// The agent will soon reached the end of the path, or be blocked by other agents such that it cannot get closer. + /// Typically the agent can only move forward for a fraction of a second before it will become blocked. + /// </summary> + ReachedSoon, + /// <summary> + /// The agent has reached the end of the path, or it is blocked by other agents such that it cannot get closer right now. + /// If multiple have roughly the same end of path they will end up crowding around that point and all agents in the crowd will get this status. + /// </summary> + Reached, + } + + // TODO: Change to byte? + /// <summary>Plane which movement is primarily happening in</summary> + public enum MovementPlane { + /// <summary>Movement happens primarily in the XZ plane (3D)</summary> + XZ, + /// <summary>Movement happens primarily in the XY plane (2D)</summary> + XY, + /// <summary>For curved worlds. See: spherical (view in online documentation for working links)</summary> + Arbitrary, + } + + // Note: RVOLayer must not be marked with the [System.Flags] attribute because then Unity will show all RVOLayer fields as mask fields + // which we do not want + public enum RVOLayer { + DefaultAgent = 1 << 0, + DefaultObstacle = 1 << 1, + Layer2 = 1 << 2, + Layer3 = 1 << 3, + Layer4 = 1 << 4, + Layer5 = 1 << 5, + Layer6 = 1 << 6, + Layer7 = 1 << 7, + Layer8 = 1 << 8, + Layer9 = 1 << 9, + Layer10 = 1 << 10, + Layer11 = 1 << 11, + Layer12 = 1 << 12, + Layer13 = 1 << 13, + Layer14 = 1 << 14, + Layer15 = 1 << 15, + Layer16 = 1 << 16, + Layer17 = 1 << 17, + Layer18 = 1 << 18, + Layer19 = 1 << 19, + Layer20 = 1 << 20, + Layer21 = 1 << 21, + Layer22 = 1 << 22, + Layer23 = 1 << 23, + Layer24 = 1 << 24, + Layer25 = 1 << 25, + Layer26 = 1 << 26, + Layer27 = 1 << 27, + Layer28 = 1 << 28, + Layer29 = 1 << 29, + Layer30 = 1 << 30 + } + + /// <summary> + /// Local Avoidance Simulator. + /// This class handles local avoidance simulation for a number of agents using + /// Reciprocal Velocity Obstacles (RVO) and Optimal Reciprocal Collision Avoidance (ORCA). + /// + /// This class will handle calculation of velocities from desired velocities supplied by a script. + /// It is, however, not responsible for moving any objects in a Unity Scene. For that there are other scripts (see below). + /// + /// Agents be added and removed at any time. + /// + /// See: RVOSimulator + /// See: RVOAgentBurst + /// See: Pathfinding.RVO.IAgent + /// + /// You will most likely mostly use the wrapper class <see cref="RVOSimulator"/>. + /// </summary> + public class SimulatorBurst { + /// <summary> + /// Inverse desired simulation fps. + /// See: DesiredDeltaTime + /// </summary> + private float desiredDeltaTime = 0.05f; + + /// <summary>Number of agents in this simulation</summary> + int numAgents = 0; + + /// <summary> + /// Scope for drawing gizmos even on frames during which the simulation is not running. + /// This is used to draw the obstacles, quadtree and agent debug lines. + /// </summary> + Drawing.RedrawScope debugDrawingScope; + + /// <summary> + /// Quadtree for this simulation. + /// Used internally by the simulation to perform fast neighbour lookups for each agent. + /// Please only read from this tree, do not rebuild it since that can interfere with the simulation. + /// It is rebuilt when necessary. + /// </summary> + public RVOQuadtreeBurst quadtree; + + public bool drawQuadtree; + + Action[] agentPreCalculationCallbacks = new Action[0]; + Action[] agentDestroyCallbacks = new Action[0]; + + Stack<int> freeAgentIndices = new Stack<int>(); + TemporaryAgentData temporaryAgentData; + HorizonAgentData horizonAgentData; + + /// <summary> + /// Internal obstacle data. + /// Normally you will never need to access this directly + /// </summary> + public ObstacleData obstacleData; + + /// <summary> + /// Internal simulation data. + /// Can be used if you need very high performance access to the agent data. + /// Normally you would use the SimulatorBurst.Agent class instead (implements the IAgent interface). + /// </summary> + public AgentData simulationData; + + /// <summary> + /// Internal simulation data. + /// Can be used if you need very high performance access to the agent data. + /// Normally you would use the SimulatorBurst.Agent class instead (implements the IAgent interface). + /// </summary> + public AgentOutputData outputData; + + public const int MaxNeighbourCount = 50; + public const int MaxBlockingAgentCount = 7; + + public const int MaxObstacleVertices = 256; + + struct Agent : IAgent { + public SimulatorBurst simulator; + public AgentIndex agentIndex; + + public int AgentIndex => agentIndex.Index; + public Vector3 Position { get => simulator.simulationData.position[AgentIndex]; set => simulator.simulationData.position[AgentIndex] = value; } + public bool Locked { get => simulator.simulationData.locked[AgentIndex]; set => simulator.simulationData.locked[AgentIndex] = value; } + public float Radius { get => simulator.simulationData.radius[AgentIndex]; set => simulator.simulationData.radius[AgentIndex] = value; } + public float Height { get => simulator.simulationData.height[AgentIndex]; set => simulator.simulationData.height[AgentIndex] = value; } + public float AgentTimeHorizon { get => simulator.simulationData.agentTimeHorizon[AgentIndex]; set => simulator.simulationData.agentTimeHorizon[AgentIndex] = value; } + public float ObstacleTimeHorizon { get => simulator.simulationData.obstacleTimeHorizon[AgentIndex]; set => simulator.simulationData.obstacleTimeHorizon[AgentIndex] = value; } + public int MaxNeighbours { get => simulator.simulationData.maxNeighbours[AgentIndex]; set => simulator.simulationData.maxNeighbours[AgentIndex] = value; } + public RVOLayer Layer { get => simulator.simulationData.layer[AgentIndex]; set => simulator.simulationData.layer[AgentIndex] = value; } + public RVOLayer CollidesWith { get => simulator.simulationData.collidesWith[AgentIndex]; set => simulator.simulationData.collidesWith[AgentIndex] = value; } + public float FlowFollowingStrength { get => simulator.simulationData.flowFollowingStrength[AgentIndex]; set => simulator.simulationData.flowFollowingStrength[AgentIndex] = value; } + public AgentDebugFlags DebugFlags { get => simulator.simulationData.debugFlags[AgentIndex]; set => simulator.simulationData.debugFlags[AgentIndex] = value; } + public float Priority { get => simulator.simulationData.priority[AgentIndex]; set => simulator.simulationData.priority[AgentIndex] = value; } + public int HierarchicalNodeIndex { get => simulator.simulationData.hierarchicalNodeIndex[AgentIndex]; set => simulator.simulationData.hierarchicalNodeIndex[AgentIndex] = value; } + public SimpleMovementPlane MovementPlane { get => new SimpleMovementPlane(simulator.simulationData.movementPlane[AgentIndex].rotation); set => simulator.simulationData.movementPlane[AgentIndex] = new NativeMovementPlane(value); } + public Action PreCalculationCallback { set => simulator.agentPreCalculationCallbacks[AgentIndex] = value; } + public Action DestroyedCallback { set => simulator.agentDestroyCallbacks[AgentIndex] = value; } + + public Vector3 CalculatedTargetPoint { + get { + simulator.BlockUntilSimulationStepDone(); + return simulator.outputData.targetPoint[AgentIndex]; + } + } + + public float CalculatedSpeed { + get { + simulator.BlockUntilSimulationStepDone(); + return simulator.outputData.speed[AgentIndex]; + } + } + + public ReachedEndOfPath CalculatedEffectivelyReachedDestination { + get { + simulator.BlockUntilSimulationStepDone(); + return simulator.outputData.effectivelyReachedDestination[AgentIndex]; + } + } + + public int NeighbourCount { + get { + simulator.BlockUntilSimulationStepDone(); + return simulator.outputData.numNeighbours[AgentIndex]; + } + } + + public bool AvoidingAnyAgents { + get { + simulator.BlockUntilSimulationStepDone(); + return simulator.outputData.blockedByAgents[AgentIndex*SimulatorBurst.MaxBlockingAgentCount] != -1; + } + } + + public void SetObstacleQuery (GraphNode sourceNode) { + HierarchicalNodeIndex = sourceNode != null && !sourceNode.Destroyed && sourceNode.Walkable ? sourceNode.HierarchicalNodeIndex : -1; + } + + public void SetTarget (Vector3 targetPoint, float desiredSpeed, float maxSpeed, Vector3 endOfPath) { + simulator.simulationData.SetTarget(AgentIndex, targetPoint, desiredSpeed, maxSpeed, endOfPath); + } + + public void SetCollisionNormal (Vector3 normal) { + simulator.simulationData.collisionNormal[AgentIndex] = normal; + } + + public void ForceSetVelocity (Vector3 velocity) { + // A bit hacky, but it is approximately correct + // assuming the agent does not move significantly + simulator.simulationData.targetPoint[AgentIndex] = simulator.simulationData.position[AgentIndex] + (float3)velocity * 1000; + simulator.simulationData.desiredSpeed[AgentIndex] = velocity.magnitude; + simulator.simulationData.allowedVelocityDeviationAngles[AgentIndex] = float2.zero; + simulator.simulationData.manuallyControlled[AgentIndex] = true; + } + } + + /// <summary>Holds internal obstacle data for the local avoidance simulation</summary> + public struct ObstacleData { + /// <summary> + /// Groups of vertices representing obstacles. + /// An obstacle is either a cycle or a chain of vertices + /// </summary> + public SlabAllocator<ObstacleVertexGroup> obstacleVertexGroups; + /// <summary>Vertices of all obstacles</summary> + public SlabAllocator<float3> obstacleVertices; + /// <summary>Obstacle sets, each one is represented as a set of obstacle vertex groups</summary> + public NativeList<UnmanagedObstacle> obstacles; + + public void Init (Allocator allocator) { + if (!obstacles.IsCreated) obstacles = new NativeList<UnmanagedObstacle>(0, allocator); + if (!obstacleVertexGroups.IsCreated) obstacleVertexGroups = new SlabAllocator<ObstacleVertexGroup>(4, allocator); + if (!obstacleVertices.IsCreated) obstacleVertices = new SlabAllocator<float3>(16, allocator); + } + + public void Dispose () { + if (obstacleVertexGroups.IsCreated) { + obstacleVertexGroups.Dispose(); + obstacleVertices.Dispose(); + obstacles.Dispose(); + } + } + } + + /// <summary>Holds internal agent data for the local avoidance simulation</summary> + public struct AgentData { + // Note: All 3D vectors are in world space + public NativeArray<AgentIndex> version; + public NativeArray<float> radius; + public NativeArray<float> height; + public NativeArray<float> desiredSpeed; + public NativeArray<float> maxSpeed; + public NativeArray<float> agentTimeHorizon; + public NativeArray<float> obstacleTimeHorizon; + public NativeArray<bool> locked; + public NativeArray<int> maxNeighbours; + public NativeArray<RVOLayer> layer; + public NativeArray<RVOLayer> collidesWith; + public NativeArray<float> flowFollowingStrength; + public NativeArray<float3> position; + public NativeArray<float3> collisionNormal; + public NativeArray<bool> manuallyControlled; + public NativeArray<float> priority; + public NativeArray<AgentDebugFlags> debugFlags; + public NativeArray<float3> targetPoint; + /// <summary>x = signed left angle in radians, y = signed right angle in radians (should be greater than x)</summary> + public NativeArray<float2> allowedVelocityDeviationAngles; + public NativeArray<NativeMovementPlane> movementPlane; + public NativeArray<float3> endOfPath; + /// <summary>Which obstacle data in the <see cref="ObstacleData.obstacles"/> array the agent should use for avoidance</summary> + public NativeArray<int> agentObstacleMapping; + public NativeArray<int> hierarchicalNodeIndex; + + public void Realloc (int size, Allocator allocator) { + Util.Memory.Realloc(ref version, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref radius, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref height, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref desiredSpeed, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref maxSpeed, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref agentTimeHorizon, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref obstacleTimeHorizon, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref locked, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref maxNeighbours, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref layer, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref collidesWith, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref flowFollowingStrength, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref position, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref collisionNormal, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref manuallyControlled, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref priority, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref debugFlags, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref targetPoint, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref movementPlane, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref allowedVelocityDeviationAngles, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref endOfPath, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref agentObstacleMapping, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref hierarchicalNodeIndex, size, allocator, NativeArrayOptions.UninitializedMemory); + } + + public void SetTarget (int agentIndex, float3 targetPoint, float desiredSpeed, float maxSpeed, float3 endOfPath) { + maxSpeed = math.max(maxSpeed, 0); + desiredSpeed = math.clamp(desiredSpeed, 0, maxSpeed); + + this.targetPoint[agentIndex] = targetPoint; + this.desiredSpeed[agentIndex] = desiredSpeed; + this.maxSpeed[agentIndex] = maxSpeed; + this.endOfPath[agentIndex] = endOfPath; + // TODO: Set allowedVelocityDeviationAngles here + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public bool HasDebugFlag(int agentIndex, AgentDebugFlags flag) => Unity.Burst.CompilerServices.Hint.Unlikely((debugFlags[agentIndex] & flag) != 0); + + public void Dispose () { + version.Dispose(); + radius.Dispose(); + height.Dispose(); + desiredSpeed.Dispose(); + maxSpeed.Dispose(); + agentTimeHorizon.Dispose(); + obstacleTimeHorizon.Dispose(); + locked.Dispose(); + maxNeighbours.Dispose(); + layer.Dispose(); + collidesWith.Dispose(); + flowFollowingStrength.Dispose(); + position.Dispose(); + collisionNormal.Dispose(); + manuallyControlled.Dispose(); + priority.Dispose(); + debugFlags.Dispose(); + targetPoint.Dispose(); + movementPlane.Dispose(); + allowedVelocityDeviationAngles.Dispose(); + endOfPath.Dispose(); + agentObstacleMapping.Dispose(); + hierarchicalNodeIndex.Dispose(); + } + }; + + public struct AgentOutputData { + public NativeArray<float3> targetPoint; + public NativeArray<float> speed; + public NativeArray<int> numNeighbours; + [NativeDisableParallelForRestrictionAttribute] + public NativeArray<int> blockedByAgents; + public NativeArray<ReachedEndOfPath> effectivelyReachedDestination; + public NativeArray<float> forwardClearance; + + public void Realloc (int size, Allocator allocator) { + Util.Memory.Realloc(ref targetPoint, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref speed, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref numNeighbours, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref blockedByAgents, size * MaxBlockingAgentCount, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref effectivelyReachedDestination, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref forwardClearance, size, allocator, NativeArrayOptions.UninitializedMemory); + } + + public void Move (int fromIndex, int toIndex) { + targetPoint[toIndex] = targetPoint[fromIndex]; + speed[toIndex] = speed[fromIndex]; + numNeighbours[toIndex] = numNeighbours[fromIndex]; + effectivelyReachedDestination[toIndex] = effectivelyReachedDestination[fromIndex]; + for (int i = 0; i < MaxBlockingAgentCount; i++) { + blockedByAgents[toIndex * MaxBlockingAgentCount + i] = blockedByAgents[fromIndex * MaxBlockingAgentCount + i]; + } + forwardClearance[toIndex] = forwardClearance[fromIndex]; + } + + public void Dispose () { + targetPoint.Dispose(); + speed.Dispose(); + numNeighbours.Dispose(); + blockedByAgents.Dispose(); + effectivelyReachedDestination.Dispose(); + forwardClearance.Dispose(); + } + }; + + public struct HorizonAgentData { + public NativeArray<int> horizonSide; + public NativeArray<float> horizonMinAngle; + public NativeArray<float> horizonMaxAngle; + + public void Realloc (int size, Allocator allocator) { + Util.Memory.Realloc(ref horizonSide, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref horizonMinAngle, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref horizonMaxAngle, size, allocator, NativeArrayOptions.UninitializedMemory); + } + + public void Move (int fromIndex, int toIndex) { + horizonSide[toIndex] = horizonSide[fromIndex]; + // The other values are temporary values that don't have to be moved + } + + public void Dispose () { + horizonSide.Dispose(); + horizonMinAngle.Dispose(); + horizonMaxAngle.Dispose(); + } + } + + public struct TemporaryAgentData { + public NativeArray<float2> desiredTargetPointInVelocitySpace; + public NativeArray<float3> desiredVelocity; + public NativeArray<float3> currentVelocity; + public NativeArray<float2> collisionVelocityOffsets; + public NativeArray<int> neighbours; + + public void Realloc (int size, Allocator allocator) { + Util.Memory.Realloc(ref desiredTargetPointInVelocitySpace, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref desiredVelocity, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref currentVelocity, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref collisionVelocityOffsets, size, allocator, NativeArrayOptions.UninitializedMemory); + Util.Memory.Realloc(ref neighbours, size * MaxNeighbourCount, allocator, NativeArrayOptions.UninitializedMemory); + } + + public void Dispose () { + desiredTargetPointInVelocitySpace.Dispose(); + desiredVelocity.Dispose(); + currentVelocity.Dispose(); + neighbours.Dispose(); + collisionVelocityOffsets.Dispose(); + } + } + + /// <summary> + /// Time in seconds between each simulation step. + /// This is the desired delta time, the simulation will never run at a higher fps than + /// the rate at which the Update function is called. + /// </summary> + public float DesiredDeltaTime { get { return desiredDeltaTime; } set { desiredDeltaTime = System.Math.Max(value, 0.0f); } } + + /// <summary> + /// Bias agents to pass each other on the right side. + /// If the desired velocity of an agent puts it on a collision course with another agent or an obstacle + /// its desired velocity will be rotated this number of radians (1 radian is approximately 57°) to the right. + /// This helps to break up symmetries and makes it possible to resolve some situations much faster. + /// + /// When many agents have the same goal this can however have the side effect that the group + /// clustered around the target point may as a whole start to spin around the target point. + /// + /// Recommended values are in the range of 0 to 0.2. + /// + /// If this value is negative, the agents will be biased towards passing each other on the left side instead. + /// </summary> + public float SymmetryBreakingBias { get; set; } + + /// <summary>Use hard collisions</summary> + public bool HardCollisions { get; set; } + + public bool UseNavmeshAsObstacle { get; set; } + + public Rect AgentBounds { + get { + lastJob.Complete(); + return quadtree.bounds; + } + } + + /// <summary>Number of agents in the simulation</summary> + public int AgentCount => numAgents; + + public MovementPlane MovementPlane => movementPlane; + + /// <summary>Determines if the XY (2D) or XZ (3D) plane is used for movement</summary> + public readonly MovementPlane movementPlane = MovementPlane.XZ; + + /// <summary>Job handle for the last update</summary> + public JobHandle lastJob { get; private set; } + + public void BlockUntilSimulationStepDone () { + lastJob.Complete(); + } + + /// <summary>Create a new simulator.</summary> + /// <param name="movementPlane">The plane that the movement happens in. XZ for 3D games, XY for 2D games.</param> + public SimulatorBurst (MovementPlane movementPlane) { + this.DesiredDeltaTime = 1; + this.movementPlane = movementPlane; + + obstacleData.Init(Allocator.Persistent); + AllocateAgentSpace(); + + // Just to make sure the quadtree is in a valid state + quadtree.BuildJob(simulationData.position, simulationData.version, simulationData.desiredSpeed, simulationData.radius, 0, movementPlane).Run(); + } + + /// <summary>Removes all agents from the simulation</summary> + public void ClearAgents () { + BlockUntilSimulationStepDone(); + for (int i = 0; i < agentDestroyCallbacks.Length; i++) agentDestroyCallbacks[i]?.Invoke(); + numAgents = 0; + } + + /// <summary> + /// Frees all used memory. + /// Warning: You must call this when you are done with the simulator, otherwise some resources can linger and lead to memory leaks. + /// </summary> + public void OnDestroy () { + debugDrawingScope.Dispose(); + BlockUntilSimulationStepDone(); + ClearAgents(); + obstacleData.Dispose(); + simulationData.Dispose(); + temporaryAgentData.Dispose(); + outputData.Dispose(); + quadtree.Dispose(); + horizonAgentData.Dispose(); + } + + void AllocateAgentSpace () { + if (numAgents > agentPreCalculationCallbacks.Length || agentPreCalculationCallbacks.Length == 0) { + var prevSize = simulationData.version.Length; + int newSize = Mathf.Max(64, Mathf.Max(numAgents, agentPreCalculationCallbacks.Length * 2)); + simulationData.Realloc(newSize, Allocator.Persistent); + temporaryAgentData.Realloc(newSize, Allocator.Persistent); + outputData.Realloc(newSize, Allocator.Persistent); + horizonAgentData.Realloc(newSize, Allocator.Persistent); + Memory.Realloc(ref agentPreCalculationCallbacks, newSize); + Memory.Realloc(ref agentDestroyCallbacks, newSize); + for (int i = prevSize; i < newSize; i++) simulationData.version[i] = new AgentIndex(0, i); + } + } + + /// <summary> + /// Add an agent at the specified position. + /// You can use the returned interface to read and write parameters + /// and set for example radius and desired point to move to. + /// + /// See: <see cref="RemoveAgent"/> + /// + /// Deprecated: Use AddAgent(Vector3) instead + /// </summary> + [System.Obsolete("Use AddAgent(Vector3) instead")] + public IAgent AddAgent (Vector2 position, float elevationCoordinate) { + if (movementPlane == MovementPlane.XY) return AddAgent(new Vector3(position.x, position.y, elevationCoordinate)); + else return AddAgent(new Vector3(position.x, elevationCoordinate, position.y)); + } + + /// <summary> + /// Add an agent at the specified position. + /// You can use the returned interface to read and write parameters + /// and set for example radius and desired point to move to. + /// + /// See: <see cref="RemoveAgent"/> + /// </summary> + /// <param name="position">See \reflink{IAgent.Position}</param> + public IAgent AddAgent (Vector3 position) { + var agentIndex = AddAgentBurst(position); + return new Agent { simulator = this, agentIndex = agentIndex }; + } + + /// <summary> + /// Add an agent at the specified position. + /// You can use the returned index to read and write parameters + /// and set for example radius and desired point to move to. + /// + /// See: <see cref="RemoveAgent"/> + /// </summary> + public AgentIndex AddAgentBurst (float3 position) { + BlockUntilSimulationStepDone(); + + int agentIndex; + if (freeAgentIndices.Count > 0) { + agentIndex = freeAgentIndices.Pop(); + } else { + agentIndex = numAgents++; + AllocateAgentSpace(); + } + + var packedAgentIndex = simulationData.version[agentIndex].WithIncrementedVersion(); + UnityEngine.Assertions.Assert.AreEqual(packedAgentIndex.Index, agentIndex); + + simulationData.version[agentIndex] = packedAgentIndex; + simulationData.radius[agentIndex] = 5; + simulationData.height[agentIndex] = 5; + simulationData.desiredSpeed[agentIndex] = 0; + simulationData.maxSpeed[agentIndex] = 1; + simulationData.agentTimeHorizon[agentIndex] = 2; + simulationData.obstacleTimeHorizon[agentIndex] = 2; + simulationData.locked[agentIndex] = false; + simulationData.maxNeighbours[agentIndex] = 10; + simulationData.layer[agentIndex] = RVOLayer.DefaultAgent; + simulationData.collidesWith[agentIndex] = (RVOLayer)(-1); + simulationData.flowFollowingStrength[agentIndex] = 0; + simulationData.position[agentIndex] = position; + simulationData.collisionNormal[agentIndex] = float3.zero; + simulationData.manuallyControlled[agentIndex] = false; + simulationData.priority[agentIndex] = 0.5f; + simulationData.debugFlags[agentIndex] = AgentDebugFlags.Nothing; + simulationData.targetPoint[agentIndex] = position; + // Set the default movement plane. Default to the XZ plane even if movement plane is arbitrary (the user will have to set a custom one later) + simulationData.movementPlane[agentIndex] = new NativeMovementPlane((movementPlane == MovementPlane.XY ? SimpleMovementPlane.XYPlane : SimpleMovementPlane.XZPlane).rotation); + simulationData.allowedVelocityDeviationAngles[agentIndex] = float2.zero; + simulationData.endOfPath[agentIndex] = float3.zero; + simulationData.agentObstacleMapping[agentIndex] = -1; + simulationData.hierarchicalNodeIndex[agentIndex] = -1; + + outputData.speed[agentIndex] = 0; + outputData.numNeighbours[agentIndex] = 0; + outputData.targetPoint[agentIndex] = position; + outputData.blockedByAgents[agentIndex * MaxBlockingAgentCount] = -1; + outputData.effectivelyReachedDestination[agentIndex] = ReachedEndOfPath.NotReached; + + horizonAgentData.horizonSide[agentIndex] = 0; + agentPreCalculationCallbacks[agentIndex] = null; + agentDestroyCallbacks[agentIndex] = null; + + return packedAgentIndex; + } + + /// <summary>Deprecated: Use AddAgent(Vector3) instead</summary> + [System.Obsolete("Use AddAgent(Vector3) instead")] + public IAgent AddAgent (IAgent agent) { + throw new System.NotImplementedException("Use AddAgent(position) instead. Agents are not persistent after being removed."); + } + + /// <summary> + /// Removes a specified agent from this simulation. + /// The agent can be added again later by using AddAgent. + /// + /// See: AddAgent(IAgent) + /// See: ClearAgents + /// </summary> + public void RemoveAgent (IAgent agent) { + if (agent == null) throw new System.ArgumentNullException(nameof(agent)); + Agent realAgent = (Agent)agent; + RemoveAgent(realAgent.agentIndex); + } + + public bool AgentExists (AgentIndex agent) { + BlockUntilSimulationStepDone(); + if (!simulationData.version.IsCreated) return false; + var index = agent.Index; + if (index >= simulationData.version.Length) return false; + if (agent.Version != simulationData.version[index].Version) return false; + return true; + } + + public void RemoveAgent (AgentIndex agent) { + BlockUntilSimulationStepDone(); + if (!AgentExists(agent)) throw new System.InvalidOperationException("Trying to remove agent which does not exist"); + + var index = agent.Index; + // Increment version and set deleted bit + simulationData.version[index] = simulationData.version[index].WithIncrementedVersion().WithDeleted(); + // Avoid memory leaks + agentPreCalculationCallbacks[index] = null; + try { + if (agentDestroyCallbacks[index] != null) agentDestroyCallbacks[index](); + } catch (System.Exception e) { + Debug.LogException(e); + } + agentDestroyCallbacks[index] = null; + freeAgentIndices.Push(index); + } + + void PreCalculation (JobHandle dependency) { + bool blocked = false; + for (int i = 0; i < numAgents; i++) { + var callback = agentPreCalculationCallbacks[i]; + if (callback != null) { + if (!blocked) { + dependency.Complete(); + blocked = true; + } + callback.Invoke(); + } + } + } + + /// <summary>Should be called once per frame.</summary> + /// <param name="dependency">Jobs that need to complete before local avoidance runs.</param> + /// <param name="dt">Length of timestep in seconds.</param> + /// <param name="drawGizmos">If true, debug gizmos will be allowed to render (they never render in standalone games, though).</param> + /// <param name="allocator">Allocator to use for some temporary allocations. Should be a rewindable allocator since no disposal will be done.</param> + public JobHandle Update (JobHandle dependency, float dt, bool drawGizmos, Allocator allocator) { + var x = 0; + if (x != 0) { + // We need to specify these types somewhere in their concrete form. + // Otherwise the burst compiler doesn't understand that it has to compile them. + // This code will never run. + new JobRVO<XYMovementPlane>().ScheduleBatch(0, 0); + new JobRVO<XZMovementPlane>().ScheduleBatch(0, 0); + new JobRVO<ArbitraryMovementPlane>().ScheduleBatch(0, 0); + + new JobRVOCalculateNeighbours<XYMovementPlane>().ScheduleBatch(0, 0); + new JobRVOCalculateNeighbours<XZMovementPlane>().ScheduleBatch(0, 0); + new JobRVOCalculateNeighbours<ArbitraryMovementPlane>().ScheduleBatch(0, 0); + + new JobHardCollisions<XYMovementPlane>().ScheduleBatch(0, 0); + new JobHardCollisions<XZMovementPlane>().ScheduleBatch(0, 0); + new JobHardCollisions<ArbitraryMovementPlane>().ScheduleBatch(0, 0); + + new JobDestinationReached<XYMovementPlane>().Schedule(); + new JobDestinationReached<XZMovementPlane>().Schedule(); + new JobDestinationReached<ArbitraryMovementPlane>().Schedule(); + } + + // The burst jobs are specialized for the type of movement plane used. This improves performance for the XY and XZ movement planes quite a lot + if (movementPlane == MovementPlane.XY) return UpdateInternal<XYMovementPlane>(dependency, dt, drawGizmos, allocator); + else if (movementPlane == MovementPlane.XZ) return UpdateInternal<XZMovementPlane>(dependency, dt, drawGizmos, allocator); + else return UpdateInternal<ArbitraryMovementPlane>(dependency, dt, drawGizmos, allocator); + } + + public void LockSimulationDataReadOnly (JobHandle dependencies) { + this.lastJob = JobHandle.CombineDependencies(this.lastJob, dependencies); + } + + JobHandle UpdateInternal<T>(JobHandle dependency, float deltaTime, bool drawGizmos, Allocator allocator) where T : struct, IMovementPlaneWrapper { + // Prevent a zero delta time + deltaTime = math.max(deltaTime, 1.0f/2000f); + + BlockUntilSimulationStepDone(); + + UnityEngine.Profiling.Profiler.BeginSample("Read agent data"); + + // Read agent data from RVOController components on the main thread. + // We cannot do this in a job because RVOController data may be changed at any time + // on the main thread. + PreCalculation(dependency); + + UnityEngine.Profiling.Profiler.EndSample(); + + var quadtreeJob = quadtree.BuildJob(simulationData.position, simulationData.version, outputData.speed, simulationData.radius, numAgents, movementPlane).Schedule(dependency); + + var preprocessJob = new JobRVOPreprocess { + agentData = simulationData, + previousOutput = outputData, + temporaryAgentData = temporaryAgentData, + startIndex = 0, + endIndex = numAgents, + }.Schedule(dependency); + + int batchSize = math.max(numAgents / 64, 8); + var neighboursJob = new JobRVOCalculateNeighbours<T> { + agentData = simulationData, + quadtree = quadtree, + outNeighbours = temporaryAgentData.neighbours, + output = outputData, + }.ScheduleBatch(numAgents, batchSize, JobHandle.CombineDependencies(preprocessJob, quadtreeJob)); + + // Make the threads start working now, we have enough work scheduled that they have stuff to do. + JobHandle.ScheduleBatchedJobs(); + + var combinedJob = JobHandle.CombineDependencies(preprocessJob, neighboursJob); + + debugDrawingScope.Rewind(); + var draw = DrawingManager.GetBuilder(debugDrawingScope); + + var horizonJob1 = new JobHorizonAvoidancePhase1 { + agentData = simulationData, + neighbours = temporaryAgentData.neighbours, + desiredTargetPointInVelocitySpace = temporaryAgentData.desiredTargetPointInVelocitySpace, + horizonAgentData = horizonAgentData, + draw = draw, + }.ScheduleBatch(numAgents, batchSize, combinedJob); + + var horizonJob2 = new JobHorizonAvoidancePhase2 { + neighbours = temporaryAgentData.neighbours, + versions = simulationData.version, + desiredVelocity = temporaryAgentData.desiredVelocity, + desiredTargetPointInVelocitySpace = temporaryAgentData.desiredTargetPointInVelocitySpace, + horizonAgentData = horizonAgentData, + movementPlane = simulationData.movementPlane, + }.ScheduleBatch(numAgents, batchSize, horizonJob1); + + var hardCollisionsJob1 = new JobHardCollisions<T> { + agentData = simulationData, + neighbours = temporaryAgentData.neighbours, + collisionVelocityOffsets = temporaryAgentData.collisionVelocityOffsets, + deltaTime = deltaTime, + enabled = HardCollisions, + }.ScheduleBatch(numAgents, batchSize, combinedJob); + + RWLock.CombinedReadLockAsync navmeshEdgeDataLock; + NavmeshEdges.NavmeshBorderData navmeshEdgeData; + bool hasAstar = AstarPath.active != null; + if (hasAstar) { + navmeshEdgeData = AstarPath.active.GetNavmeshBorderData(out navmeshEdgeDataLock); + } else { + navmeshEdgeData = NavmeshEdges.NavmeshBorderData.CreateEmpty(allocator); + navmeshEdgeDataLock = default; + } + var rvoJobData = new JobRVO<T> { + agentData = simulationData, + temporaryAgentData = temporaryAgentData, + navmeshEdgeData = navmeshEdgeData, + output = outputData, + deltaTime = deltaTime, + symmetryBreakingBias = Mathf.Max(0, SymmetryBreakingBias), + draw = draw, + useNavmeshAsObstacle = UseNavmeshAsObstacle, + priorityMultiplier = 1f, + // priorityMultiplier = 0.1f, + }; + + combinedJob = JobHandle.CombineDependencies(horizonJob2, hardCollisionsJob1, navmeshEdgeDataLock.dependency); + + // JobHandle rvoJob = combinedJob; + // for (int k = 0; k < 3; k++) { + // var preprocessJob2 = new JobRVOPreprocess { + // agentData = simulationData, + // previousOutput = outputData, + // temporaryAgentData = temporaryAgentData, + // startIndex = 0, + // endIndex = numAgents, + // }.Schedule(rvoJob); + // rvoJob = new JobRVO<T> { + // agentData = simulationData, + // temporaryAgentData = temporaryAgentData, + // navmeshEdgeData = navmeshEdgeData, + // output = outputData, + // deltaTime = deltaTime, + // symmetryBreakingBias = Mathf.Max(0, SymmetryBreakingBias), + // draw = draw, + // priorityMultiplier = (k+1) * (1.0f/3.0f), + // }.ScheduleBatch(numAgents, batchSize, preprocessJob2); + // } + var rvoJob = rvoJobData.ScheduleBatch(numAgents, batchSize, combinedJob); + if (hasAstar) { + navmeshEdgeDataLock.UnlockAfter(rvoJob); + } else { + navmeshEdgeData.DisposeEmpty(rvoJob); + } + + var reachedJob = new JobDestinationReached<T> { + agentData = simulationData, + obstacleData = obstacleData, + temporaryAgentData = temporaryAgentData, + output = outputData, + draw = draw, + numAgents = numAgents, + }.Schedule(rvoJob); + + // Clear some fields that are reset every simulation tick + var clearJob = simulationData.collisionNormal.MemSet(float3.zero).Schedule(reachedJob); + var clearJob2 = simulationData.manuallyControlled.MemSet(false).Schedule(reachedJob); + var clearJob3 = simulationData.hierarchicalNodeIndex.MemSet(-1).Schedule(reachedJob); + + dependency = JobHandle.CombineDependencies(reachedJob, clearJob, clearJob2); + dependency = JobHandle.CombineDependencies(dependency, clearJob3); + + if (drawQuadtree && drawGizmos) { + dependency = JobHandle.CombineDependencies(dependency, new RVOQuadtreeBurst.DebugDrawJob { + draw = draw, + quadtree = quadtree, + }.Schedule(quadtreeJob)); + } + + draw.DisposeAfter(dependency); + + lastJob = dependency; + return dependency; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulatorBurst.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulatorBurst.cs.meta new file mode 100644 index 0000000..55b77cd --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOCoreSimulatorBurst.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b618542be33248ba974eb1f4c16e73b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOObstacleCache.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOObstacleCache.cs new file mode 100644 index 0000000..5261871 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOObstacleCache.cs @@ -0,0 +1,344 @@ +namespace Pathfinding.RVO { + using Pathfinding; + using UnityEngine; + using Pathfinding.Util; + using Unity.Mathematics; + using Unity.Collections; + using Unity.Jobs; + using System.Collections.Generic; + using Unity.Burst; + using Unity.Profiling; + using Pathfinding.Jobs; +#if MODULE_COLLECTIONS_2_1_0_OR_NEWER + using NativeHashMapIntInt = Unity.Collections.NativeHashMap<int, int>; +#else + using NativeHashMapIntInt = Unity.Collections.NativeParallelHashMap<int, int>; +#endif + + [BurstCompile] + public static class RVOObstacleCache { + public struct ObstacleSegment { + public float3 vertex1; + public float3 vertex2; + public int vertex1LinkId; + public int vertex2LinkId; + } + + static ulong HashKey (GraphNode sourceNode, int traversableTags, SimpleMovementPlane movementPlane) { + var hash = (ulong)sourceNode.NodeIndex; + hash = hash * 786433 ^ (ulong)traversableTags; + // The rotation is not particularly important for the obstacle. It's only used + // to simplify the output a bit. So we allow similar rotations to share the same hash. + const float RotationQuantization = 4; + hash = hash * 786433 ^ (ulong)(movementPlane.rotation.x*RotationQuantization); + hash = hash * 786433 ^ (ulong)(movementPlane.rotation.y*RotationQuantization); + hash = hash * 786433 ^ (ulong)(movementPlane.rotation.z*RotationQuantization); + hash = hash * 786433 ^ (ulong)(movementPlane.rotation.w*RotationQuantization); + return hash; + } + + /// <summary> + /// Collects an unordered list of contour segments based on the given nodes. + /// + /// Note: All nodes must be from the same graph. + /// </summary> + public static void CollectContours (List<GraphNode> nodes, NativeList<ObstacleSegment> obstacles) { + if (nodes.Count == 0) return; + if (nodes[0] is TriangleMeshNode) { + for (int i = 0; i < nodes.Count; i++) { + var tnode = nodes[i] as TriangleMeshNode; + var used = 0; + if (tnode.connections != null) { + for (int j = 0; j < tnode.connections.Length; j++) { + var conn = tnode.connections[j]; + if (conn.isEdgeShared) { + used |= 1 << conn.shapeEdge; + } + } + } + + tnode.GetVertices(out var v0, out var v1, out var v2); + for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { + if ((used & (1 << edgeIndex)) == 0) { + // This edge is not shared, therefore it's a contour edge + Int3 leftVertex, rightVertex; + switch (edgeIndex) { + case 0: + leftVertex = v0; + rightVertex = v1; + break; + case 1: + leftVertex = v1; + rightVertex = v2; + break; + case 2: + default: + leftVertex = v2; + rightVertex = v0; + break; + } + var leftVertexHash = leftVertex.GetHashCode(); + var rightVertexHash = rightVertex.GetHashCode(); + + obstacles.Add(new ObstacleSegment { + vertex1 = (Vector3)leftVertex, + vertex2 = (Vector3)rightVertex, + vertex1LinkId = leftVertexHash, + vertex2LinkId = rightVertexHash, + }); + } + } + } + } else if (nodes[0] is GridNodeBase) { + GridGraph graph; + if (nodes[0] is LevelGridNode) graph = LevelGridNode.GetGridGraph(nodes[0].GraphIndex); + else + graph = GridNode.GetGridGraph(nodes[0].GraphIndex); + unsafe { + // Offsets from the center of the node to the corners of the node, in world space + // Index dir is the offset to the left corner of the edge in direction dir + // See GridNodeBase.GetNeighbourAlongDirection for the direction indices + Vector3* offsets = stackalloc Vector3[4]; + for (int dir = 0; dir < 4; dir++) { + var dl = (dir + 1) % 4; + offsets[dir] = graph.transform.TransformVector(0.5f * new Vector3(GridGraph.neighbourXOffsets[dir] + GridGraph.neighbourXOffsets[dl], 0, GridGraph.neighbourZOffsets[dir] + GridGraph.neighbourZOffsets[dl])); + } + + for (int i = 0; i < nodes.Count; i++) { + var gnode = nodes[i] as GridNodeBase; + if (gnode.HasConnectionsToAllAxisAlignedNeighbours) continue; + + for (int dir = 0; dir < 4; dir++) { + if (!gnode.HasConnectionInDirection(dir)) { + // ┌─────────┬─────────┐ + // │ │ │ + // │ nl1 │ nl2 │ ^ + // │ │ │ | + // ├────────vL─────────┤ dl + // │ │#########│ + // │ node │#########│ dir-> + // │ │#########│ + // ├────────vR─────────┤ dr + // │ │ │ | + // │ nr1 │ nr2 │ v + // │ │ │ + // └─────────┴─────────┘ + var dl = (dir + 1) % 4; + var dr = (dir - 1 + 4) % 4; + var nl1 = gnode.GetNeighbourAlongDirection(dl); + var nr1 = gnode.GetNeighbourAlongDirection(dr); + + // All this hashing code looks slow, but really it's not compared to all the memory accesses to get the node data + uint leftVertexHash; + if (nl1 != null) { + var nl2 = nl1.GetNeighbourAlongDirection(dir); + if (nl2 != null) { + // Outer corner. Uniquely determined by the 3 nodes that touch the corner. + var a = gnode.NodeIndex; + var b = nl1.NodeIndex; + var c = nl2.NodeIndex; + // Sort the values so that a <= b <= c + if (a > b) Memory.Swap(ref a, ref b); + if (b > c) Memory.Swap(ref b, ref c); + if (a > b) Memory.Swap(ref a, ref b); + leftVertexHash = math.hash(new uint3(a, b, c)); + } else { + // Straight wall. Uniquely determined by the 2 nodes that touch the corner and the direction to the wall. + var a = gnode.NodeIndex; + var b = nl1.NodeIndex; + // Sort the values so that a <= b + if (a > b) Memory.Swap(ref a, ref b); + leftVertexHash = math.hash(new uint3(a, b, (uint)dir)); + } + } else { + // Inner corner. Uniquely determined by the single node that touches the corner and the direction to it. + var diagonalToCorner = dir + 4; + leftVertexHash = math.hash(new uint2(gnode.NodeIndex, (uint)diagonalToCorner)); + } + + uint rightVertexHash; + if (nr1 != null) { + var nr2 = nr1.GetNeighbourAlongDirection(dir); + if (nr2 != null) { + // Outer corner. Uniquely determined by the 3 nodes that touch the corner. + var a = gnode.NodeIndex; + var b = nr1.NodeIndex; + var c = nr2.NodeIndex; + // Sort the values so that a <= b <= c + if (a > b) Memory.Swap(ref a, ref b); + if (b > c) Memory.Swap(ref b, ref c); + if (a > b) Memory.Swap(ref a, ref b); + rightVertexHash = math.hash(new uint3(a, b, c)); + } else { + // Straight wall. Uniquely determined by the 2 nodes that touch the corner and the direction to the wall. + var a = gnode.NodeIndex; + var b = nr1.NodeIndex; + // Sort the values so that a <= b + if (a > b) Memory.Swap(ref a, ref b); + rightVertexHash = math.hash(new uint3(a, b, (uint)dir)); + } + } else { + // Inner corner. Uniquely determined by the single node that touches the corner and the direction to it. + // Note: It's not a typo that we use `dr+4` here and `dir+4` above. They are different directions. + var diagonalToCorner = dr + 4; + rightVertexHash = math.hash(new uint2(gnode.NodeIndex, (uint)diagonalToCorner)); + } + + var nodePos = (Vector3)gnode.position; + obstacles.Add(new ObstacleSegment { + vertex1 = nodePos + offsets[dir], // Left corner. Yes, it should be dir, not dl, as the offsets array already points to the left corners of each segment. + vertex2 = nodePos + offsets[dr], // Right corner + vertex1LinkId = (int)leftVertexHash, + vertex2LinkId = (int)rightVertexHash, + }); + } + } + } + } + } + } + + private static readonly ProfilerMarker MarkerAllocate = new ProfilerMarker("Allocate"); + + /// <summary>Trace contours generated by CollectContours.</summary> + /// <param name="obstaclesSpan">Obstacle segments, typically the borders of the navmesh. In no particular order. + /// Each edge must be oriented the same way (e.g. all clockwise, or all counter-clockwise around the obstacles).</param> + /// <param name="movementPlane">The movement plane used for simplification. The up direction will be treated as less important for purposes of simplification.</param> + /// <param name="obstacleId">The ID of the obstacle to write into the outputObstacles array.</param> + /// <param name="outputObstacles">Array to write the obstacle to.</param> + /// <param name="verticesAllocator">Allocator to use for the vertices of the obstacle.</param> + /// <param name="obstaclesAllocator">Allocator to use for the obstacle metadata.</param> + /// <param name="spinLock">Lock to use when allocating from the allocators.</param> + /// <param name="simplifyObstacles">If true, the obstacle will be simplified. This means that colinear vertices (when projected onto the movement plane) will be removed.</param> + [BurstCompile] + public static unsafe void TraceContours (ref UnsafeSpan<ObstacleSegment> obstaclesSpan, ref NativeMovementPlane movementPlane, int obstacleId, UnmanagedObstacle* outputObstacles, ref SlabAllocator<float3> verticesAllocator, ref SlabAllocator<ObstacleVertexGroup> obstaclesAllocator, ref SpinLock spinLock, bool simplifyObstacles) { + var obstacles = obstaclesSpan; + if (obstacles.Length == 0) { + outputObstacles[obstacleId] = new UnmanagedObstacle { + verticesAllocation = SlabAllocator<float3>.ZeroLengthArray, + groupsAllocation = SlabAllocator<ObstacleVertexGroup>.ZeroLengthArray, + }; + return; + } + + MarkerAllocate.Begin(); + var traceLookup = new NativeHashMapIntInt(obstacles.Length, Unity.Collections.Allocator.Temp); + // For each edge: Will be 0 if the segment should be ignored or if it has been visited, 1 if it has not been visited and has an ingoing edge, and 2 if it has not been visited and has no ingoing edge. + var priority = new NativeArray<byte>(obstacles.Length, Unity.Collections.Allocator.Temp, Unity.Collections.NativeArrayOptions.UninitializedMemory); + MarkerAllocate.End(); + for (int i = 0; i < obstacles.Length; i++) { + // var obstacle = obstacles[i]; + // Add the edge to the lookup. But if it already exists, ignore it. + // That it already exists is very much a special case that can happen if there is + // overlapping geometry (typically added by a NavmeshAdd component). + // In that case the "outer edge" that we want to trace is kinda undefined, but we will + // do our best with it. + if (traceLookup.TryAdd(obstacles[i].vertex1LinkId, i)) { + priority[i] = 2; + } else { + priority[i] = 0; + } + } + for (int i = 0; i < obstacles.Length; i++) { + if (traceLookup.TryGetValue(obstacles[i].vertex2LinkId, out var other) && priority[other] > 0) { + // The other segment has an ingoing edge. This means it cannot be the start of a contour. + // Reduce the priority so that we only consider it when looking for cycles. + priority[other] = 1; + } + } + + var outputMetadata = new NativeList<ObstacleVertexGroup>(16, Allocator.Temp); + var outputVertices = new NativeList<float3>(16, Allocator.Temp); + // We now have a hashmap of directed edges (vertex1 -> vertex2) such that these edges are directed the same (cw or ccw), and "outer edges". + // We can now follow these directed edges to trace out the contours of the mesh. + var toPlaneMatrix = movementPlane.AsWorldToPlaneMatrix(); + for (int allowLoops = 0; allowLoops <= 1; allowLoops++) { + var minPriority = allowLoops == 1 ? 1 : 2; + for (int i = 0; i < obstacles.Length; i++) { + if (priority[i] >= minPriority) { + int startVertices = outputVertices.Length; + outputVertices.Add(obstacles[i].vertex1); + + var lastAdded = obstacles[i].vertex1; + var candidateVertex = obstacles[i].vertex2; + var index = i; + var obstacleType = ObstacleType.Chain; + var boundsMn = lastAdded; + var boundsMx = lastAdded; + while (true) { + if (priority[index] == 0) { + // This should not happen for a regular navmesh. + // But it can happen if there are degenerate edges or overlapping triangles. + // In that case we will just stop here + break; + } + priority[index] = 0; + + float3 nextVertex; + if (traceLookup.TryGetValue(obstacles[index].vertex2LinkId, out int nextIndex)) { + nextVertex = 0.5f * (obstacles[index].vertex2 + obstacles[nextIndex].vertex1); + } else { + nextVertex = obstacles[index].vertex2; + nextIndex = -1; + } + + // Try to simplify and see if we even need to add the vertex C. + var p1 = lastAdded; + var p2 = nextVertex; + var p3 = candidateVertex; + var d1 = toPlaneMatrix.ToPlane(p2 - p1); + var d2 = toPlaneMatrix.ToPlane(p3 - p1); + var det = VectorMath.Determinant(d1, d2); + if (math.abs(det) < 0.01f && simplifyObstacles) { + // We don't need to add candidateVertex. It's just a straight line (p1,p2,p3 are colinear). + } else { + outputVertices.Add(candidateVertex); + boundsMn = math.min(boundsMn, candidateVertex); + boundsMx = math.max(boundsMx, candidateVertex); + lastAdded = p3; + } + + if (nextIndex == i) { + // Loop + outputVertices[startVertices] = nextVertex; + obstacleType = ObstacleType.Loop; + break; + } else if (nextIndex == -1) { + // End of chain + outputVertices.Add(nextVertex); + boundsMn = math.min(boundsMn, nextVertex); + boundsMx = math.max(boundsMx, nextVertex); + break; + } + + index = nextIndex; + candidateVertex = nextVertex; + } + + outputMetadata.Add(new ObstacleVertexGroup { + type = obstacleType, + vertexCount = outputVertices.Length - startVertices, + boundsMn = boundsMn, + boundsMx = boundsMx, + }); + } + } + } + + int obstacleSet, vertexSet; + if (outputMetadata.Length > 0) { + spinLock.Lock(); + obstacleSet = obstaclesAllocator.Allocate(outputMetadata); + vertexSet = verticesAllocator.Allocate(outputVertices); + spinLock.Unlock(); + } else { + obstacleSet = SlabAllocator<ObstacleVertexGroup>.ZeroLengthArray; + vertexSet = SlabAllocator<float3>.ZeroLengthArray; + } + outputObstacles[obstacleId] = new UnmanagedObstacle { + verticesAllocation = vertexSet, + groupsAllocation = obstacleSet, + }; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOObstacleCache.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOObstacleCache.cs.meta new file mode 100644 index 0000000..344ea02 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOObstacleCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8a3a9a0945d2b43f5b8cb871ffbc1b2b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtree.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtree.cs new file mode 100644 index 0000000..3b6ed49 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtree.cs @@ -0,0 +1,4 @@ + +// This file has been removed from the project. Since UnityPackages cannot +// delete files, only replace them, this message is left here to prevent old +// files from causing compiler errors diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtree.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtree.cs.meta new file mode 100644 index 0000000..f18d4ad --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtree.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4fadcced8ad4d40d59f3eea45426359d +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtreeBurst.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtreeBurst.cs new file mode 100644 index 0000000..f0222dc --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtreeBurst.cs @@ -0,0 +1,698 @@ +namespace Pathfinding.RVO { + using UnityEngine; + using Pathfinding.ECS.RVO; + using Unity.Burst; + using Unity.Jobs; + using Unity.Mathematics; + using Unity.Collections; + using Pathfinding.Drawing; + + /// <summary> + /// Quadtree for quick nearest neighbour search of rvo agents. + /// See: Pathfinding.RVO.Simulator + /// </summary> + public struct RVOQuadtreeBurst { + const int LeafSize = 16; + const int MaxDepth = 10; + + NativeArray<int> agents; + NativeArray<int> childPointers; + NativeArray<float3> boundingBoxBuffer; + NativeArray<int> agentCountBuffer; + NativeArray<float3> agentPositions; + NativeArray<float> agentRadii; + NativeArray<float> maxSpeeds; + NativeArray<float> maxRadius; + NativeArray<float> nodeAreas; + MovementPlane movementPlane; + + const int LeafNodeBit = 1 << 30; + const int BitPackingShift = 15; + const int BitPackingMask = (1 << BitPackingShift) - 1; + const int MaxAgents = BitPackingMask; + + /// <summary> + /// For a given number, contains the index of the first non-zero bit. + /// Only the values 0 through 15 are used when movementPlane is XZ or XY. + /// + /// Use bytes instead of ints to save some precious L1 cache memory. + /// </summary> + static readonly byte[] ChildLookup = new byte[256]; + + static RVOQuadtreeBurst() { + for (int v = 0; v < 256; v++) { + for (int i = 0; i < 8; i++) { + if (((v >> i) & 0x1) != 0) { + ChildLookup[v] = (byte)i; + break; + } + } + } + } + + public Rect bounds { + get { + return boundingBoxBuffer.IsCreated ? Rect.MinMaxRect(boundingBoxBuffer[0].x, boundingBoxBuffer[0].y, boundingBoxBuffer[1].x, boundingBoxBuffer[1].y) : new Rect(); + } + } + + static int InnerNodeCountUpperBound (int numAgents, MovementPlane movementPlane) { + // Every LeafSize number of nodes can cause a split at most MaxDepth + // number of times. Each split needs 4 (or 8) units of space. + // Round the value up by adding LeafSize-1 to the numerator. + // This is an upper bound. Most likely the tree will contain significantly fewer nodes. + return ((movementPlane == MovementPlane.Arbitrary ? 8 : 4) * MaxDepth * numAgents + LeafSize-1)/LeafSize; + } + + public void Dispose () { + agents.Dispose(); + childPointers.Dispose(); + boundingBoxBuffer.Dispose(); + agentCountBuffer.Dispose(); + maxSpeeds.Dispose(); + maxRadius.Dispose(); + nodeAreas.Dispose(); + agentPositions.Dispose(); + agentRadii.Dispose(); + } + + void Reserve (int minSize) { + if (!boundingBoxBuffer.IsCreated) { + boundingBoxBuffer = new NativeArray<float3>(4, Allocator.Persistent); + agentCountBuffer = new NativeArray<int>(1, Allocator.Persistent); + } + // Create a new agent's array. Round up to nearest multiple multiple of 2 to avoid re-allocating often if the agent count slowly increases + int roundedAgents = math.ceilpow2(minSize); + Util.Memory.Realloc(ref agents, roundedAgents, Allocator.Persistent, NativeArrayOptions.ClearMemory); + Util.Memory.Realloc(ref agentPositions, roundedAgents, Allocator.Persistent, NativeArrayOptions.ClearMemory); + Util.Memory.Realloc(ref agentRadii, roundedAgents, Allocator.Persistent, NativeArrayOptions.ClearMemory); + Util.Memory.Realloc(ref childPointers, InnerNodeCountUpperBound(roundedAgents, movementPlane), Allocator.Persistent, NativeArrayOptions.ClearMemory); + Util.Memory.Realloc(ref maxSpeeds, childPointers.Length, Allocator.Persistent, NativeArrayOptions.ClearMemory); + Util.Memory.Realloc(ref nodeAreas, childPointers.Length, Allocator.Persistent, NativeArrayOptions.ClearMemory); + Util.Memory.Realloc(ref maxRadius, childPointers.Length, Allocator.Persistent, NativeArrayOptions.ClearMemory); + } + + public JobBuild BuildJob (NativeArray<float3> agentPositions, NativeArray<AgentIndex> agentVersions, NativeArray<float> agentSpeeds, NativeArray<float> agentRadii, int numAgents, MovementPlane movementPlane) { + if (numAgents >= MaxAgents) throw new System.Exception("Too many agents. Cannot have more than " + MaxAgents); + Reserve(numAgents); + + this.movementPlane = movementPlane; + + return new JobBuild { + agents = agents, + agentVersions = agentVersions, + agentPositions = agentPositions, + agentSpeeds = agentSpeeds, + agentRadii = agentRadii, + outMaxSpeeds = maxSpeeds, + outMaxRadius = maxRadius, + outArea = nodeAreas, + outAgentRadii = this.agentRadii, // Will be copied. These are copied so that the quadtree remains in a valid state even after new agents have been added/removed. This is important for the QueryArea method which may be called at any time. + outAgentPositions = this.agentPositions, // Will be copied + outBoundingBox = boundingBoxBuffer, + outAgentCount = agentCountBuffer, + outChildPointers = childPointers, + numAgents = numAgents, + movementPlane = movementPlane, + }; + } + + [BurstCompile(CompileSynchronously = true, FloatMode = FloatMode.Fast)] + public struct JobBuild : IJob { + /// <summary>Length should be greater or equal to agentPositions.Length</summary> + public NativeArray<int> agents; + + [ReadOnly] + public NativeArray<float3> agentPositions; + + [ReadOnly] + public NativeArray<AgentIndex> agentVersions; + + [ReadOnly] + public NativeArray<float> agentSpeeds; + + [ReadOnly] + public NativeArray<float> agentRadii; + + /// <summary>Should have size 2</summary> + [WriteOnly] + public NativeArray<float3> outBoundingBox; + + /// <summary>Should have size 1</summary> + [WriteOnly] + public NativeArray<int> outAgentCount; + + /// <summary>Should have size: InnerNodeCountUpperBound(numAgents)</summary> + public NativeArray<int> outChildPointers; + + /// <summary>Should have size: InnerNodeCountUpperBound(numAgents)</summary> + public NativeArray<float> outMaxSpeeds; + + /// <summary>Should have size: InnerNodeCountUpperBound(numAgents)</summary> + public NativeArray<float> outMaxRadius; + + /// <summary>Should have size: InnerNodeCountUpperBound(numAgents)</summary> + public NativeArray<float> outArea; + + [WriteOnly] + public NativeArray<float3> outAgentPositions; + + [WriteOnly] + public NativeArray<float> outAgentRadii; + + public int numAgents; + + public MovementPlane movementPlane; + + static int Partition (NativeSlice<int> indices, int startIndex, int endIndex, NativeSlice<float> coordinates, float splitPoint) { + for (int i = startIndex; i < endIndex; i++) { + if (coordinates[indices[i]] > splitPoint) { + endIndex--; + var tmp = indices[i]; + indices[i] = indices[endIndex]; + indices[endIndex] = tmp; + i--; + } + } + return endIndex; + } + + void BuildNode (float3 boundsMin, float3 boundsMax, int depth, int agentsStart, int agentsEnd, int nodeOffset, ref int firstFreeChild) { + if (agentsEnd - agentsStart > LeafSize && depth < MaxDepth) { + if (movementPlane == MovementPlane.Arbitrary) { + // Split the node into 8 equally sized (by volume) child nodes + var xs = new NativeSlice<float3>(agentPositions).SliceWithStride<float>(0); + var ys = new NativeSlice<float3>(agentPositions).SliceWithStride<float>(4); + var zs = new NativeSlice<float3>(agentPositions).SliceWithStride<float>(8); + + float3 boundsMid = (boundsMin + boundsMax) * 0.5f; + int s0 = agentsStart; + int s8 = agentsEnd; + int s4 = Partition(agents, s0, s8, xs, boundsMid.x); + int s2 = Partition(agents, s0, s4, ys, boundsMid.y); + int s6 = Partition(agents, s4, s8, ys, boundsMid.y); + int s1 = Partition(agents, s0, s2, zs, boundsMid.z); + int s3 = Partition(agents, s2, s4, zs, boundsMid.z); + int s5 = Partition(agents, s4, s6, zs, boundsMid.z); + int s7 = Partition(agents, s6, s8, zs, boundsMid.z); + + // Note: guaranteed to be large enough + int childIndex = firstFreeChild; + outChildPointers[nodeOffset] = childIndex; + firstFreeChild += 8; + + // x y z + // low low low + // low low high + // low high low + // low high high + // high low low + // high low high + // high high low + // high high high + var min = boundsMin; + var mid = boundsMid; + var max = boundsMax; + BuildNode(new float3(min.x, min.y, min.z), new float3(mid.x, mid.y, mid.z), depth + 1, s0, s1, childIndex + 0, ref firstFreeChild); + BuildNode(new float3(min.x, min.y, mid.z), new float3(mid.x, mid.y, max.z), depth + 1, s1, s2, childIndex + 1, ref firstFreeChild); + BuildNode(new float3(min.x, mid.y, min.z), new float3(mid.x, max.y, mid.z), depth + 1, s2, s3, childIndex + 2, ref firstFreeChild); + BuildNode(new float3(min.x, mid.y, mid.z), new float3(mid.x, max.y, max.z), depth + 1, s3, s4, childIndex + 3, ref firstFreeChild); + BuildNode(new float3(mid.x, min.y, min.z), new float3(max.x, mid.y, mid.z), depth + 1, s4, s5, childIndex + 4, ref firstFreeChild); + BuildNode(new float3(mid.x, min.y, mid.z), new float3(max.x, mid.y, max.z), depth + 1, s5, s6, childIndex + 5, ref firstFreeChild); + BuildNode(new float3(mid.x, mid.y, min.z), new float3(max.x, max.y, mid.z), depth + 1, s6, s7, childIndex + 6, ref firstFreeChild); + BuildNode(new float3(mid.x, mid.y, mid.z), new float3(max.x, max.y, max.z), depth + 1, s7, s8, childIndex + 7, ref firstFreeChild); + } else if (movementPlane == MovementPlane.XY) { + // Split the node into 4 equally sized (by area) child nodes + var xs = new NativeSlice<float3>(agentPositions).SliceWithStride<float>(0); + var ys = new NativeSlice<float3>(agentPositions).SliceWithStride<float>(4); + + float3 boundsMid = (boundsMin + boundsMax) * 0.5f; + int s0 = agentsStart; + int s4 = agentsEnd; + int s2 = Partition(agents, s0, s4, xs, boundsMid.x); + int s1 = Partition(agents, s0, s2, ys, boundsMid.y); + int s3 = Partition(agents, s2, s4, ys, boundsMid.y); + + // Note: guaranteed to be large enough + int childIndex = firstFreeChild; + outChildPointers[nodeOffset] = childIndex; + firstFreeChild += 4; + + // x y + // low low + // low high + // high low + // high high + BuildNode(new float3(boundsMin.x, boundsMin.y, boundsMin.z), new float3(boundsMid.x, boundsMid.y, boundsMax.z), depth + 1, s0, s1, childIndex + 0, ref firstFreeChild); + BuildNode(new float3(boundsMin.x, boundsMid.y, boundsMin.z), new float3(boundsMid.x, boundsMax.y, boundsMax.z), depth + 1, s1, s2, childIndex + 1, ref firstFreeChild); + BuildNode(new float3(boundsMid.x, boundsMin.y, boundsMin.z), new float3(boundsMax.x, boundsMid.y, boundsMax.z), depth + 1, s2, s3, childIndex + 2, ref firstFreeChild); + BuildNode(new float3(boundsMid.x, boundsMid.y, boundsMin.z), new float3(boundsMax.x, boundsMax.y, boundsMax.z), depth + 1, s3, s4, childIndex + 3, ref firstFreeChild); + } else { + // Split the node into 4 equally sized (by area) child nodes + var xs = new NativeSlice<float3>(agentPositions).SliceWithStride<float>(0); + var zs = new NativeSlice<float3>(agentPositions).SliceWithStride<float>(8); + + float3 boundsMid = (boundsMin + boundsMax) * 0.5f; + int s0 = agentsStart; + int s4 = agentsEnd; + int s2 = Partition(agents, s0, s4, xs, boundsMid.x); + int s1 = Partition(agents, s0, s2, zs, boundsMid.z); + int s3 = Partition(agents, s2, s4, zs, boundsMid.z); + + // Note: guaranteed to be large enough + int childIndex = firstFreeChild; + outChildPointers[nodeOffset] = childIndex; + firstFreeChild += 4; + + // x z + // low low + // low high + // high low + // high high + BuildNode(new float3(boundsMin.x, boundsMin.y, boundsMin.z), new float3(boundsMid.x, boundsMax.y, boundsMid.z), depth + 1, s0, s1, childIndex + 0, ref firstFreeChild); + BuildNode(new float3(boundsMin.x, boundsMin.y, boundsMid.z), new float3(boundsMid.x, boundsMax.y, boundsMax.z), depth + 1, s1, s2, childIndex + 1, ref firstFreeChild); + BuildNode(new float3(boundsMid.x, boundsMin.y, boundsMin.z), new float3(boundsMax.x, boundsMax.y, boundsMid.z), depth + 1, s2, s3, childIndex + 2, ref firstFreeChild); + BuildNode(new float3(boundsMid.x, boundsMin.y, boundsMid.z), new float3(boundsMax.x, boundsMax.y, boundsMax.z), depth + 1, s3, s4, childIndex + 3, ref firstFreeChild); + } + } else { + // Bitpack the start and end indices + outChildPointers[nodeOffset] = agentsStart | (agentsEnd << BitPackingShift) | LeafNodeBit; + } + } + + void CalculateSpeeds (int nodeCount) { + for (int i = nodeCount - 1; i >= 0; i--) { + if ((outChildPointers[i] & LeafNodeBit) != 0) { + int startIndex = outChildPointers[i] & BitPackingMask; + int endIndex = (outChildPointers[i] >> BitPackingShift) & BitPackingMask; + float speed = 0; + for (int j = startIndex; j < endIndex; j++) speed = math.max(speed, agentSpeeds[agents[j]]); + outMaxSpeeds[i] = speed; + + float radius = 0; + for (int j = startIndex; j < endIndex; j++) radius = math.max(radius, agentRadii[agents[j]]); + outMaxRadius[i] = radius; + + float area = 0; + for (int j = startIndex; j < endIndex; j++) area += agentRadii[agents[j]]*agentRadii[agents[j]]; + outArea[i] = area; + } else { + // Take the maximum of all child speeds + // This is guaranteed to have been calculated already because we do the loop in reverse and child indices are always greater than the current index + int childIndex = outChildPointers[i]; + if (movementPlane == MovementPlane.Arbitrary) { + // 8 children + float maxSpeed = 0; + float maxRadius = 0; + float area = 0; + for (int j = 0; j < 8; j++) { + maxSpeed = math.max(maxSpeed, outMaxSpeeds[childIndex + j]); + maxRadius = math.max(maxRadius, outMaxSpeeds[childIndex + j]); + area += outArea[childIndex + j]; + } + outMaxSpeeds[i] = maxSpeed; + outMaxRadius[i] = maxRadius; + outArea[i] = area; + } else { + // 4 children + outMaxSpeeds[i] = math.max(math.max(outMaxSpeeds[childIndex], outMaxSpeeds[childIndex+1]), math.max(outMaxSpeeds[childIndex+2], outMaxSpeeds[childIndex+3])); + outMaxRadius[i] = math.max(math.max(outMaxRadius[childIndex], outMaxRadius[childIndex+1]), math.max(outMaxRadius[childIndex+2], outMaxRadius[childIndex+3])); + + // Sum of child areas + outArea[i] = outArea[childIndex] + outArea[childIndex+1] + outArea[childIndex+2] + outArea[childIndex+3]; + } + } + } + } + + public void Execute () { + float3 mn = float.PositiveInfinity; + float3 mx = float.NegativeInfinity; + int existingAgentCount = 0; + for (int i = 0; i < numAgents; i++) { + if (agentVersions[i].Valid) { + agents[existingAgentCount++] = i; + mn = math.min(mn, agentPositions[i]); + mx = math.max(mx, agentPositions[i]); + } + } + + outAgentCount[0] = existingAgentCount; + + if (existingAgentCount == 0) { + outBoundingBox[0] = outBoundingBox[1] = float3.zero; + return; + } + + outBoundingBox[0] = mn; + outBoundingBox[1] = mx; + + int firstFreeChild = 1; + BuildNode(mn, mx, 0, 0, existingAgentCount, 0, ref firstFreeChild); + + CalculateSpeeds(firstFreeChild); + + NativeArray<float3>.Copy(agentPositions, outAgentPositions, numAgents); + NativeArray<float>.Copy(agentRadii, outAgentRadii, numAgents); + } + } + + public struct QuadtreeQuery { + public float3 position; + public float speed, timeHorizon, agentRadius; + public int outputStartIndex, maxCount; + public NativeArray<int> result; + public NativeArray<float> resultDistances; + } + + public void QueryKNearest (QuadtreeQuery query) { + if (!agents.IsCreated) return; + float maxRadius = float.PositiveInfinity; + + for (int i = 0; i < query.maxCount; i++) query.result[query.outputStartIndex + i] = -1; + for (int i = 0; i < query.maxCount; i++) query.resultDistances[i] = float.PositiveInfinity; + + QueryRec(ref query, 0, boundingBoxBuffer[0], boundingBoxBuffer[1], ref maxRadius); + } + + void QueryRec (ref QuadtreeQuery query, int treeNodeIndex, float3 nodeMin, float3 nodeMax, ref float maxRadius) { + // Note: the second agentRadius usage should actually be the radius of the other agents, not this agent + // Determine the radius that we need to search to take all agents into account + // but for performance reasons and for simplicity we assume that agents have approximately the same radius. + // Thus an agent with a very small radius may in some cases detect an agent with a very large radius too late + // however this effect should be minor. + var radius = math.min(math.max((maxSpeeds[treeNodeIndex] + query.speed)*query.timeHorizon, query.agentRadius) + query.agentRadius, maxRadius); + float3 p = query.position; + + if ((childPointers[treeNodeIndex] & LeafNodeBit) != 0) { + // Leaf node + int maxCount = query.maxCount; + int startIndex = childPointers[treeNodeIndex] & BitPackingMask; + int endIndex = (childPointers[treeNodeIndex] >> BitPackingShift) & BitPackingMask; + + var result = query.result; + var resultDistances = query.resultDistances; + for (int j = startIndex; j < endIndex; j++) { + var agent = agents[j]; + float sqrDistance = math.lengthsq(p - agentPositions[agent]); + if (sqrDistance < radius*radius) { + // Close enough + + // Insert the agent into the results list using insertion sort + for (int k = 0; k < maxCount; k++) { + if (sqrDistance < resultDistances[k]) { + // Move the remaining items one step in the array + for (int q = maxCount - 1; q > k; q--) { + result[query.outputStartIndex + q] = result[query.outputStartIndex + q-1]; + resultDistances[q] = resultDistances[q-1]; + } + result[query.outputStartIndex + k] = agent; + resultDistances[k] = sqrDistance; + + if (k == maxCount - 1) { + // We reached the end of the array. This means that we just updated the largest distance. + // We can use this to restrict the future search. We know that no other agent distance we find can be larger than this value. + maxRadius = math.min(maxRadius, math.sqrt(sqrDistance)); + radius = math.min(radius, maxRadius); + } + break; + } + } + } + } + } else { + // Not a leaf node + int childrenStartIndex = childPointers[treeNodeIndex]; + + float3 nodeMid = (nodeMin + nodeMax) * 0.5f; + if (movementPlane == MovementPlane.Arbitrary) { + // First visit the child that overlaps the query position. + // This is important to do first as it usually reduces the maxRadius significantly + // and thus reduces the number of children we have to search later. + var mainChildIndex = (p.x < nodeMid.x ? 0 : 4) | (p.y < nodeMid.y ? 0 : 2) | (p.z < nodeMid.z ? 0 : 1); + { + var selector = new bool3((mainChildIndex & 0x4) != 0, (mainChildIndex & 0x2) != 0, (mainChildIndex & 0x1) != 0); + + var mn = math.select(nodeMin, nodeMid, selector); + var mx = math.select(nodeMid, nodeMax, selector); + QueryRec(ref query, childrenStartIndex + mainChildIndex, mn, mx, ref maxRadius); + radius = math.min(radius, maxRadius); + } + + // Visit a child if a cube with sides of length 2*radius (centered at p) touches the child. + // We calculate this info for all 8 children at the same time. + // Each child contains three checks, one for each axis. + // For example for the child which is lower than mid on the x-axis and z-axis, but higher than mid on the y axis + // the check we want to do looks like: (p.x - radius < nodeMid.x && p.y + radius > nodeMid.y && p.z - radius < nodeMid.z) + var lessThanMid = p - radius < nodeMid; + var greaterThanMid = p + radius > nodeMid; + // If for example lessThanMid.x is false, then we can exclude all 4 children that require that check + var branch1 = math.select(new int3(0b11110000, 0b11001100, 0b10101010), new int3(0xFF, 0xFF, 0xFF), lessThanMid); + var branch2 = math.select(new int3(0b00001111, 0b00110011, 0b01010101), new int3(0xFF, 0xFF, 0xFF), greaterThanMid); + var toVisitByAxis = branch1 & branch2; + // Combine the checks for each axis + // Bitmask of which children we want to visit (1 = visit, 0 = don't visit) + var childrenToVisit = toVisitByAxis.x & toVisitByAxis.y & toVisitByAxis.z; + + childrenToVisit &= ~(1 << mainChildIndex); + + // Loop over all children that we will visit. + // It's nice with a loop because we will usually only have a single branch. + while (childrenToVisit != 0) { + var childIndex = ChildLookup[childrenToVisit]; + var selector = new bool3((childIndex & 0x4) != 0, (childIndex & 0x2) != 0, (childIndex & 0x1) != 0); + + var mn = math.select(nodeMin, nodeMid, selector); + var mx = math.select(nodeMid, nodeMax, selector); + QueryRec(ref query, childrenStartIndex + childIndex, mn, mx, ref maxRadius); + radius = math.min(radius, maxRadius); + childrenToVisit &= ~(1 << childIndex); + } + } else if (movementPlane == MovementPlane.XY) { + var mainChildIndex = (p.x < nodeMid.x ? 0 : 2) | (p.y < nodeMid.y ? 0 : 1); + { + // Note: mx.z will become nodeMid.z which is technically incorrect, but we don't care about the Z coordinate here anyway + var selector = new bool3((mainChildIndex & 0x2) != 0, (mainChildIndex & 0x1) != 0, false); + + var mn = math.select(nodeMin, nodeMid, selector); + var mx = math.select(nodeMid, nodeMax, selector); + QueryRec(ref query, childrenStartIndex + mainChildIndex, mn, mx, ref maxRadius); + radius = math.min(radius, maxRadius); + } + + var lessThanMid = p.xy - radius < nodeMid.xy; + var greaterThanMid = p.xy + radius > nodeMid.xy; + + var v = new bool4(lessThanMid.x & lessThanMid.y, lessThanMid.x & greaterThanMid.y, greaterThanMid.x & lessThanMid.y, greaterThanMid.x & greaterThanMid.y); + // Build a bitmask of which children to visit + var childrenToVisit = (v.x ? 1 : 0) | (v.y ? 2 : 0) | (v.z ? 4 : 0) | (v.w ? 8 : 0); + childrenToVisit &= ~(1 << mainChildIndex); + + // Loop over all children that we will visit. + // It's nice with a loop because we will usually only have a single branch. + while (childrenToVisit != 0) { + var childIndex = ChildLookup[childrenToVisit]; + // Note: mx.z will become nodeMid.z which is technically incorrect, but we don't care about the Z coordinate here anyway + var selector = new bool3((childIndex & 0x2) != 0, (childIndex & 0x1) != 0, false); + + var mn = math.select(nodeMin, nodeMid, selector); + var mx = math.select(nodeMid, nodeMax, selector); + QueryRec(ref query, childrenStartIndex + childIndex, mn, mx, ref maxRadius); + radius = math.min(radius, maxRadius); + childrenToVisit &= ~(1 << childIndex); + } + } else { + var mainChildIndex = (p.x < nodeMid.x ? 0 : 2) | (p.z < nodeMid.z ? 0 : 1); + { + // Note: mx.y will become nodeMid.y which is technically incorrect, but we don't care about the Y coordinate here anyway + var selector = new bool3((mainChildIndex & 0x2) != 0, false, (mainChildIndex & 0x1) != 0); + + var mn = math.select(nodeMin, nodeMid, selector); + var mx = math.select(nodeMid, nodeMax, selector); + QueryRec(ref query, childrenStartIndex + mainChildIndex, mn, mx, ref maxRadius); + radius = math.min(radius, maxRadius); + } + + var lessThanMid = p.xz - radius < nodeMid.xz; + var greaterThanMid = p.xz + radius > nodeMid.xz; + + var v = new bool4(lessThanMid.x & lessThanMid.y, lessThanMid.x & greaterThanMid.y, greaterThanMid.x & lessThanMid.y, greaterThanMid.x & greaterThanMid.y); + var childrenToVisit = (v.x ? 1 : 0) | (v.y ? 2 : 0) | (v.z ? 4 : 0) | (v.w ? 8 : 0); + childrenToVisit &= ~(1 << mainChildIndex); + + while (childrenToVisit != 0) { + var childIndex = ChildLookup[childrenToVisit]; + // Note: mx.y will become nodeMid.y which is technically incorrect, but we don't care about the Y coordinate here anyway + var selector = new bool3((childIndex & 0x2) != 0, false, (childIndex & 0x1) != 0); + + var mn = math.select(nodeMin, nodeMid, selector); + var mx = math.select(nodeMid, nodeMax, selector); + QueryRec(ref query, childrenStartIndex + childIndex, mn, mx, ref maxRadius); + radius = math.min(radius, maxRadius); + childrenToVisit &= ~(1 << childIndex); + } + } + } + } + + /// <summary>Find the total agent area inside the circle at position with the given radius</summary> + public float QueryArea (float3 position, float radius) { + if (!agents.IsCreated || agentCountBuffer[0] == 0) return 0f; + return math.PI * QueryAreaRec(0, position, radius, boundingBoxBuffer[0], boundingBoxBuffer[1]); + } + + float QueryAreaRec (int treeNodeIndex, float3 p, float radius, float3 nodeMin, float3 nodeMax) { + float3 nodeMid = (nodeMin + nodeMax) * 0.5f; + // Radius of a circle that is guaranteed to cover the entire node + float nodeRadius = math.length(nodeMax - nodeMid); + float dist = math.lengthsq(nodeMid - p); + var maxAgentRadius = maxRadius[treeNodeIndex]; + var thresholdDistance = radius - (nodeRadius + maxAgentRadius); + + if (thresholdDistance > 0 && dist < thresholdDistance*thresholdDistance) { + // Node is completely inside the circle. Return the precalculated area of all agents inside the node. + return nodeAreas[treeNodeIndex]; + } + + if (dist > (radius + (nodeRadius + maxAgentRadius))*(radius + (nodeRadius + maxAgentRadius))) { + return 0; + } + + if ((childPointers[treeNodeIndex] & LeafNodeBit) != 0) { + // Leaf node + // Node is partially inside the circle + + int startIndex = childPointers[treeNodeIndex] & BitPackingMask; + int endIndex = (childPointers[treeNodeIndex] >> BitPackingShift) & BitPackingMask; + + float k = 0; + float area = 0; + for (int j = startIndex; j < endIndex; j++) { + var agent = agents[j]; + k += agentRadii[agent]*agentRadii[agent]; + float sqrDistance = math.lengthsq(p - agentPositions[agent]); + float agentRadius = agentRadii[agent]; + if (sqrDistance < (radius + agentRadius)*(radius + agentRadius)) { + float innerRadius = radius - agentRadius; + // Slight approximation at the edge of the circle. + // This is the approximate fraction of the agent that is inside the circle. + float fractionInside = sqrDistance < innerRadius*innerRadius ? 1.0f : 1.0f - (math.sqrt(sqrDistance) - innerRadius) / (2*agentRadius); + area += agentRadius*agentRadius * fractionInside; + } + } + return area; + } else { + float area = 0; + // Not a leaf node + int childIndex = childPointers[treeNodeIndex]; + float radiusWithMargin = radius + maxAgentRadius; + if (movementPlane == MovementPlane.Arbitrary) { + bool3 lower = (p - radiusWithMargin) < nodeMid; + bool3 upper = (p + radiusWithMargin) > nodeMid; + if (lower[0]) { + if (lower[1]) { + if (lower[2]) area += QueryAreaRec(childIndex + 0, p, radius, new float3(nodeMin.x, nodeMin.y, nodeMin.z), new float3(nodeMid.x, nodeMid.y, nodeMid.z)); + if (upper[2]) area += QueryAreaRec(childIndex + 1, p, radius, new float3(nodeMin.x, nodeMin.y, nodeMid.z), new float3(nodeMid.x, nodeMid.y, nodeMax.z)); + } + if (upper[1]) { + if (lower[2]) area += QueryAreaRec(childIndex + 2, p, radius, new float3(nodeMin.x, nodeMid.y, nodeMin.z), new float3(nodeMid.x, nodeMax.y, nodeMid.z)); + if (upper[2]) area += QueryAreaRec(childIndex + 3, p, radius, new float3(nodeMin.x, nodeMid.y, nodeMid.z), new float3(nodeMid.x, nodeMax.y, nodeMax.z)); + } + } + if (upper[0]) { + if (lower[1]) { + if (lower[2]) area += QueryAreaRec(childIndex + 4, p, radius, new float3(nodeMid.x, nodeMin.y, nodeMin.z), new float3(nodeMax.x, nodeMid.y, nodeMid.z)); + if (upper[2]) area += QueryAreaRec(childIndex + 5, p, radius, new float3(nodeMid.x, nodeMin.y, nodeMid.z), new float3(nodeMax.x, nodeMid.y, nodeMax.z)); + } + if (upper[1]) { + if (lower[2]) area += QueryAreaRec(childIndex + 6, p, radius, new float3(nodeMid.x, nodeMid.y, nodeMin.z), new float3(nodeMax.x, nodeMax.y, nodeMid.z)); + if (upper[2]) area += QueryAreaRec(childIndex + 7, p, radius, new float3(nodeMid.x, nodeMid.y, nodeMid.z), new float3(nodeMax.x, nodeMax.y, nodeMax.z)); + } + } + } else if (movementPlane == MovementPlane.XY) { + bool2 lower = (p - radiusWithMargin).xy < nodeMid.xy; + bool2 upper = (p + radiusWithMargin).xy > nodeMid.xy; + if (lower[0]) { + if (lower[1]) area += QueryAreaRec(childIndex + 0, p, radius, new float3(nodeMin.x, nodeMin.y, nodeMin.z), new float3(nodeMid.x, nodeMid.y, nodeMax.z)); + if (upper[1]) area += QueryAreaRec(childIndex + 1, p, radius, new float3(nodeMin.x, nodeMid.y, nodeMin.z), new float3(nodeMid.x, nodeMax.y, nodeMax.z)); + } + if (upper[0]) { + if (lower[1]) area += QueryAreaRec(childIndex + 2, p, radius, new float3(nodeMid.x, nodeMin.y, nodeMin.z), new float3(nodeMax.x, nodeMid.y, nodeMax.z)); + if (upper[1]) area += QueryAreaRec(childIndex + 3, p, radius, new float3(nodeMid.x, nodeMid.y, nodeMin.z), new float3(nodeMax.x, nodeMax.y, nodeMax.z)); + } + } else { + bool2 lower = (p - radiusWithMargin).xz < nodeMid.xz; + bool2 upper = (p + radiusWithMargin).xz > nodeMid.xz; + if (lower[0]) { + if (lower[1]) area += QueryAreaRec(childIndex + 0, p, radius, new float3(nodeMin.x, nodeMin.y, nodeMin.z), new float3(nodeMid.x, nodeMax.y, nodeMid.z)); + if (upper[1]) area += QueryAreaRec(childIndex + 1, p, radius, new float3(nodeMin.x, nodeMin.y, nodeMid.z), new float3(nodeMid.x, nodeMax.y, nodeMax.z)); + } + if (upper[0]) { + if (lower[1]) area += QueryAreaRec(childIndex + 2, p, radius, new float3(nodeMid.x, nodeMin.y, nodeMin.z), new float3(nodeMax.x, nodeMax.y, nodeMid.z)); + if (upper[1]) area += QueryAreaRec(childIndex + 3, p, radius, new float3(nodeMid.x, nodeMin.y, nodeMid.z), new float3(nodeMax.x, nodeMax.y, nodeMax.z)); + } + } + return area; + } + } + + [BurstCompile] + public struct DebugDrawJob : IJob { + public CommandBuilder draw; + [ReadOnly] + public RVOQuadtreeBurst quadtree; + + public void Execute () { + quadtree.DebugDraw(draw); + } + } + + public void DebugDraw (CommandBuilder draw) { + if (!agentCountBuffer.IsCreated) return; + var numAgents = agentCountBuffer[0]; + if (numAgents == 0) return; + + DebugDraw(0, boundingBoxBuffer[0], boundingBoxBuffer[1], draw); + for (int i = 0; i < numAgents; i++) { + draw.Cross(agentPositions[agents[i]], 0.5f, Palette.Colorbrewer.Set1.Red); + } + } + + void DebugDraw (int nodeIndex, float3 nodeMin, float3 nodeMax, CommandBuilder draw) { + float3 nodeMid = (nodeMin + nodeMax) * 0.5f; + + draw.WireBox(nodeMid, nodeMax - nodeMin, Palette.Colorbrewer.Set1.Orange); + + if ((childPointers[nodeIndex] & LeafNodeBit) != 0) { + int startIndex = childPointers[nodeIndex] & BitPackingMask; + int endIndex = (childPointers[nodeIndex] >> BitPackingShift) & BitPackingMask; + + for (int j = startIndex; j < endIndex; j++) { + draw.Line(nodeMid, agentPositions[agents[j]], Color.black); + } + } else { + int childIndex = childPointers[nodeIndex]; + if (movementPlane == MovementPlane.Arbitrary) { + DebugDraw(childIndex + 0, new float3(nodeMin.x, nodeMin.y, nodeMin.z), new float3(nodeMid.x, nodeMid.y, nodeMid.z), draw); + DebugDraw(childIndex + 1, new float3(nodeMin.x, nodeMin.y, nodeMid.z), new float3(nodeMid.x, nodeMid.y, nodeMax.z), draw); + DebugDraw(childIndex + 2, new float3(nodeMin.x, nodeMid.y, nodeMin.z), new float3(nodeMid.x, nodeMax.y, nodeMid.z), draw); + DebugDraw(childIndex + 3, new float3(nodeMin.x, nodeMid.y, nodeMid.z), new float3(nodeMid.x, nodeMax.y, nodeMax.z), draw); + DebugDraw(childIndex + 4, new float3(nodeMid.x, nodeMin.y, nodeMin.z), new float3(nodeMax.x, nodeMid.y, nodeMid.z), draw); + DebugDraw(childIndex + 5, new float3(nodeMid.x, nodeMin.y, nodeMid.z), new float3(nodeMax.x, nodeMid.y, nodeMax.z), draw); + DebugDraw(childIndex + 6, new float3(nodeMid.x, nodeMid.y, nodeMin.z), new float3(nodeMax.x, nodeMax.y, nodeMid.z), draw); + DebugDraw(childIndex + 7, new float3(nodeMid.x, nodeMid.y, nodeMid.z), new float3(nodeMax.x, nodeMax.y, nodeMax.z), draw); + } else if (movementPlane == MovementPlane.XY) { + DebugDraw(childIndex + 0, new float3(nodeMin.x, nodeMin.y, nodeMin.z), new float3(nodeMid.x, nodeMid.y, nodeMax.z), draw); + DebugDraw(childIndex + 1, new float3(nodeMin.x, nodeMid.y, nodeMin.z), new float3(nodeMid.x, nodeMax.y, nodeMax.z), draw); + DebugDraw(childIndex + 2, new float3(nodeMid.x, nodeMin.y, nodeMin.z), new float3(nodeMax.x, nodeMid.y, nodeMax.z), draw); + DebugDraw(childIndex + 3, new float3(nodeMid.x, nodeMid.y, nodeMin.z), new float3(nodeMax.x, nodeMax.y, nodeMax.z), draw); + } else { + DebugDraw(childIndex + 0, new float3(nodeMin.x, nodeMin.y, nodeMin.z), new float3(nodeMid.x, nodeMax.y, nodeMid.z), draw); + DebugDraw(childIndex + 1, new float3(nodeMin.x, nodeMin.y, nodeMid.z), new float3(nodeMid.x, nodeMax.y, nodeMax.z), draw); + DebugDraw(childIndex + 2, new float3(nodeMid.x, nodeMin.y, nodeMin.z), new float3(nodeMax.x, nodeMax.y, nodeMid.z), draw); + DebugDraw(childIndex + 3, new float3(nodeMid.x, nodeMin.y, nodeMid.z), new float3(nodeMax.x, nodeMax.y, nodeMax.z), draw); + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtreeBurst.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtreeBurst.cs.meta new file mode 100644 index 0000000..0482032 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOQuadtreeBurst.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a4ff665235b244238a6c694d7d38b83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization.meta new file mode 100644 index 0000000..568309f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fb135586fe44a402c8ded728d001a26c diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/JsonSerializer.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/JsonSerializer.cs new file mode 100644 index 0000000..1b55ceb --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/JsonSerializer.cs @@ -0,0 +1,1004 @@ +using System; +using System.IO; +using System.Collections.Generic; +using UnityEngine; +using Pathfinding.Util; + + +#if ASTAR_NO_ZIP +using Pathfinding.Serialization.Zip; +#elif NETFX_CORE +// For Universal Windows Platform +using ZipEntry = System.IO.Compression.ZipArchiveEntry; +using ZipFile = System.IO.Compression.ZipArchive; +#else +using Pathfinding.Ionic.Zip; +#endif + +namespace Pathfinding.Serialization { + /// <summary>Holds information passed to custom graph serializers</summary> + public class GraphSerializationContext { + private readonly GraphNode[] id2NodeMapping; + + /// <summary> + /// Deserialization stream. + /// Will only be set when deserializing + /// </summary> + public readonly BinaryReader reader; + + /// <summary> + /// Serialization stream. + /// Will only be set when serializing + /// </summary> + public readonly BinaryWriter writer; + + /// <summary> + /// Index of the graph which is currently being processed. + /// Version: uint instead of int after 3.7.5 + /// </summary> + public readonly uint graphIndex; + + /// <summary>Metadata about graphs being deserialized</summary> + public readonly GraphMeta meta; + + public bool[] persistentGraphs; + + public GraphSerializationContext (BinaryReader reader, GraphNode[] id2NodeMapping, uint graphIndex, GraphMeta meta) { + this.reader = reader; + this.id2NodeMapping = id2NodeMapping; + this.graphIndex = graphIndex; + this.meta = meta; + } + + public GraphSerializationContext (BinaryWriter writer, bool[] persistentGraphs) { + this.writer = writer; + this.persistentGraphs = persistentGraphs; + } + + public void SerializeNodeReference (GraphNode node) { + writer.Write(node == null ? -1 : (int)node.NodeIndex); + } + + public void SerializeConnections (Connection[] connections, bool serializeMetadata) { + if (connections == null) { + writer.Write(-1); + } else { + int persistentConnections = 0; + for (int i = 0; i < connections.Length; i++) persistentConnections += persistentGraphs[connections[i].node.GraphIndex] ? 1 : 0; + writer.Write(persistentConnections); + for (int i = 0; i < connections.Length; i++) { + // Ignore connections to nodes in graphs which are not serialized + if (!persistentGraphs[connections[i].node.GraphIndex]) continue; + + SerializeNodeReference(connections[i].node); + writer.Write(connections[i].cost); + if (serializeMetadata) writer.Write(connections[i].shapeEdgeInfo); + } + } + } + + public Connection[] DeserializeConnections (bool deserializeMetadata) { + int count = reader.ReadInt32(); + + if (count == -1) { + return null; + } else { + var connections = ArrayPool<Connection>.ClaimWithExactLength(count); + + for (int i = 0; i < count; i++) { + var target = DeserializeNodeReference(); + var cost = reader.ReadUInt32(); + if (deserializeMetadata) { + byte shapeEdgeInfo = Connection.NoSharedEdge; + if (meta.version < AstarSerializer.V4_1_0) { + // Read nothing + } else if (meta.version < AstarSerializer.V4_3_68) { + // Read, but discard data + reader.ReadByte(); + } else { + shapeEdgeInfo = reader.ReadByte(); + } + if (meta.version < AstarSerializer.V4_3_85) { + // Previously some additional bits were set to 1 + shapeEdgeInfo &= 0b1111 | (1 << 6); + } + if (meta.version < AstarSerializer.V4_3_87) { + shapeEdgeInfo |= Connection.IncomingConnection | Connection.OutgoingConnection; + } + + connections[i] = new Connection( + target, + cost, + shapeEdgeInfo + ); + } else { + connections[i] = new Connection(target, cost, true, true); + } + } + return connections; + } + + // TODO: Do we need to patch one way connections after deserializing? + } + + public GraphNode DeserializeNodeReference () { + var id = reader.ReadInt32(); + + if (id2NodeMapping == null) throw new Exception("Calling DeserializeNodeReference when not deserializing node references"); + + if (id == -1) return null; + GraphNode node = id2NodeMapping[id]; + if (node == null) throw new Exception("Invalid id ("+id+")"); + return node; + } + + /// <summary>Write a Vector3</summary> + public void SerializeVector3 (Vector3 v) { + writer.Write(v.x); + writer.Write(v.y); + writer.Write(v.z); + } + + /// <summary>Read a Vector3</summary> + public Vector3 DeserializeVector3 () { + return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + } + + /// <summary>Write an Int3</summary> + public void SerializeInt3 (Int3 v) { + writer.Write(v.x); + writer.Write(v.y); + writer.Write(v.z); + } + + /// <summary>Read an Int3</summary> + public Int3 DeserializeInt3 () { + return new Int3(reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32()); + } + + public int DeserializeInt (int defaultValue) { + if (reader.BaseStream.Position <= reader.BaseStream.Length-4) { + return reader.ReadInt32(); + } else { + return defaultValue; + } + } + + public float DeserializeFloat (float defaultValue) { + if (reader.BaseStream.Position <= reader.BaseStream.Length-4) { + return reader.ReadSingle(); + } else { + return defaultValue; + } + } + } + + /// <summary> + /// Handles low level serialization and deserialization of graph settings and data. + /// Mostly for internal use. You can use the methods in the AstarData class for + /// higher level serialization and deserialization. + /// + /// See: AstarData + /// </summary> + public class AstarSerializer { + private AstarData data; + + /// <summary>Zip which the data is loaded from</summary> + private ZipFile zip; + + /// <summary>Memory stream with the zip data</summary> + private MemoryStream zipStream; + + /// <summary>Graph metadata</summary> + private GraphMeta meta; + + /// <summary>Settings for serialization</summary> + private SerializeSettings settings; + + /// <summary> + /// Root GameObject used for deserialization. + /// This should be the GameObject which holds the AstarPath component. + /// Important when deserializing when the component is on a prefab. + /// </summary> + private GameObject contextRoot; + + /// <summary>Graphs that are being serialized or deserialized</summary> + private NavGraph[] graphs; + bool[] persistentGraphs; + + /// <summary> + /// Index used for the graph in the file. + /// If some graphs were null in the file then graphIndexInZip[graphs[i]] may not equal i. + /// Used for deserialization. + /// </summary> + private Dictionary<NavGraph, int> graphIndexInZip; + + private int graphIndexOffset; + + /// <summary>Extension to use for binary files</summary> + const string binaryExt = ".binary"; + + /// <summary>Extension to use for json files</summary> + const string jsonExt = ".json"; + + /// <summary> + /// Checksum for the serialized data. + /// Used to provide a quick equality check in editor code + /// </summary> + private uint checksum = 0xffffffff; + + System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); + + /// <summary>Cached StringBuilder to avoid excessive allocations</summary> + static System.Text.StringBuilder _stringBuilder = new System.Text.StringBuilder(); + + /// <summary> + /// Returns a cached StringBuilder. + /// This function only has one string builder cached and should + /// thus only be called from a single thread and should not be called while using an earlier got string builder. + /// </summary> + static System.Text.StringBuilder GetStringBuilder () { _stringBuilder.Length = 0; return _stringBuilder; } + + /// <summary>Cached version object for 3.8.3</summary> + public static readonly System.Version V3_8_3 = new System.Version(3, 8, 3); + + /// <summary>Cached version object for 3.9.0</summary> + public static readonly System.Version V3_9_0 = new System.Version(3, 9, 0); + + /// <summary>Cached version object for 4.1.0</summary> + public static readonly System.Version V4_1_0 = new System.Version(4, 1, 0); + + /// <summary>Cached version object for 4.3.2</summary> + public static readonly System.Version V4_3_2 = new System.Version(4, 3, 2); + + /// <summary>Cached version object for 4.3.6</summary> + public static readonly System.Version V4_3_6 = new System.Version(4, 3, 6); + + /// <summary>Cached version object for 4.3.37</summary> + public static readonly System.Version V4_3_37 = new System.Version(4, 3, 37); + + /// <summary>Cached version object for 4.3.12</summary> + public static readonly System.Version V4_3_12 = new System.Version(4, 3, 12); + + /// <summary>Cached version object for 4.3.68</summary> + public static readonly System.Version V4_3_68 = new System.Version(4, 3, 68); + + /// <summary>Cached version object for 4.3.74</summary> + public static readonly System.Version V4_3_74 = new System.Version(4, 3, 74); + + /// <summary>Cached version object for 4.3.80</summary> + public static readonly System.Version V4_3_80 = new System.Version(4, 3, 80); + + /// <summary>Cached version object for 4.3.83</summary> + public static readonly System.Version V4_3_83 = new System.Version(4, 3, 83); + + /// <summary>Cached version object for 4.3.85</summary> + public static readonly System.Version V4_3_85 = new System.Version(4, 3, 85); + + /// <summary>Cached version object for 4.3.87</summary> + public static readonly System.Version V4_3_87 = new System.Version(4, 3, 87); + + /// <summary>Cached version object for 5.1.0</summary> + public static readonly System.Version V5_1_0 = new System.Version(5, 1, 0); + + public AstarSerializer (AstarData data, GameObject contextRoot) : this(data, SerializeSettings.Settings, contextRoot) { + } + + public AstarSerializer (AstarData data, SerializeSettings settings, GameObject contextRoot) { + this.data = data; + this.contextRoot = contextRoot; + this.settings = settings; + } + + public void SetGraphIndexOffset (int offset) { + graphIndexOffset = offset; + } + + void AddChecksum (byte[] bytes) { + checksum = Checksum.GetChecksum(bytes, checksum); + } + + void AddEntry (string name, byte[] bytes) { +#if NETFX_CORE + var entry = zip.CreateEntry(name); + using (var stream = entry.Open()) { + stream.Write(bytes, 0, bytes.Length); + } +#else + zip.AddEntry(name, bytes); +#endif + } + + public uint GetChecksum () { return checksum; } + + #region Serialize + + public void OpenSerialize () { + // Create a new zip file, here we will store all the data + zipStream = new MemoryStream(); +#if NETFX_CORE + zip = new ZipFile(zipStream, System.IO.Compression.ZipArchiveMode.Create); +#else + zip = new ZipFile(); + zip.AlternateEncoding = System.Text.Encoding.UTF8; + zip.AlternateEncodingUsage = ZipOption.Always; + // Don't use parallel defate + zip.ParallelDeflateThreshold = -1; +#endif + meta = new GraphMeta(); + } + + public byte[] CloseSerialize () { + // As the last step, serialize metadata + byte[] bytes = SerializeMeta(); + AddChecksum(bytes); + AddEntry("meta"+jsonExt, bytes); + +#if !ASTAR_NO_ZIP && !NETFX_CORE + // Set dummy dates on every file to prevent the binary data to change + // for identical settings and graphs. + // Prevents the scene from being marked as dirty in the editor + // If ASTAR_NO_ZIP is defined this is not relevant since the replacement zip + // implementation does not even store dates + var dummy = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + foreach (var entry in zip.Entries) { + entry.AccessedTime = dummy; + entry.CreationTime = dummy; + entry.LastModified = dummy; + entry.ModifiedTime = dummy; + } +#endif + + // Save all entries to a single byte array +#if !NETFX_CORE + zip.Save(zipStream); +#endif + zip.Dispose(); + bytes = zipStream.ToArray(); + + zip = null; + zipStream = null; + return bytes; + } + + public void SerializeGraphs (NavGraph[] _graphs) { + if (graphs != null) throw new InvalidOperationException("Cannot serialize graphs multiple times."); + graphs = _graphs; + + if (zip == null) throw new NullReferenceException("You must not call CloseSerialize before a call to this function"); + + if (graphs == null) graphs = new NavGraph[0]; + + persistentGraphs = new bool[graphs.Length]; + for (int i = 0; i < graphs.Length; i++) { + //Ignore graph if null or if it should not persist + persistentGraphs[i] = graphs[i] != null && graphs[i].persistent; + + if (!persistentGraphs[i]) continue; + + // Serialize the graph to a byte array + byte[] bytes = Serialize(graphs[i]); + + AddChecksum(bytes); + AddEntry("graph"+i+jsonExt, bytes); + } + } + + /// <summary>Serialize metadata about all graphs</summary> + byte[] SerializeMeta () { + if (graphs == null) throw new System.Exception("No call to SerializeGraphs has been done"); + + meta.version = AstarPath.Version; + meta.graphs = graphs.Length; + meta.guids = new List<string>(); + meta.typeNames = new List<string>(); + + // For each graph, save the guid + // of the graph and the type of it + for (int i = 0; i < graphs.Length; i++) { + if (persistentGraphs[i]) { + meta.guids.Add(graphs[i].guid.ToString()); + meta.typeNames.Add(graphs[i].GetType().FullName); + } else { + meta.guids.Add(null); + meta.typeNames.Add(null); + } + } + + // Grab a cached string builder to avoid allocations + var output = GetStringBuilder(); + TinyJsonSerializer.Serialize(meta, output); + return encoding.GetBytes(output.ToString()); + } + + /// <summary>Serializes the graph settings to JSON and returns the data</summary> + public byte[] Serialize (NavGraph graph) { + // Grab a cached string builder to avoid allocations + var output = GetStringBuilder(); + + TinyJsonSerializer.Serialize(graph, output); + return encoding.GetBytes(output.ToString()); + } + + /// <summary> + /// Deprecated method to serialize node data. + /// Deprecated: Not used anymore + /// </summary> + [System.Obsolete("Not used anymore. You can safely remove the call to this function.")] + public void SerializeNodes () { + } + + static int GetMaxNodeIndexInAllGraphs (NavGraph[] graphs) { + int maxIndex = 0; + + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] == null || !graphs[i].persistent) continue; + graphs[i].GetNodes(node => { + maxIndex = Math.Max((int)node.NodeIndex, maxIndex); + if (node.Destroyed) { + Debug.LogError("Graph contains destroyed nodes. This is a bug."); + } + }); + } + return maxIndex; + } + + static byte[] SerializeNodeIndices (NavGraph[] graphs) { + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + + int maxNodeIndex = GetMaxNodeIndexInAllGraphs(graphs); + + writer.Write(maxNodeIndex); + + // While writing node indices, verify that the max node index is the same + // (user written graphs might have gotten it wrong) + int maxNodeIndex2 = 0; + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] == null || !graphs[i].persistent) continue; + graphs[i].GetNodes(node => { + maxNodeIndex2 = Math.Max((int)node.NodeIndex, maxNodeIndex2); + writer.Write(node.NodeIndex); + }); + } + + // Nice to verify if users are writing their own graph types + if (maxNodeIndex2 != maxNodeIndex) throw new Exception("Some graphs are not consistent in their GetNodes calls, sequential calls give different results."); + + byte[] bytes = stream.ToArray(); + writer.Close(); + + return bytes; + } + + /// <summary>Serializes info returned by NavGraph.SerializeExtraInfo</summary> + static byte[] SerializeGraphExtraInfo (NavGraph graph, bool[] persistentGraphs) { + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + var ctx = new GraphSerializationContext(writer, persistentGraphs); + + ((IGraphInternals)graph).SerializeExtraInfo(ctx); + byte[] bytes = stream.ToArray(); + writer.Close(); + + return bytes; + } + + /// <summary> + /// Used to serialize references to other nodes e.g connections. + /// Nodes use the GraphSerializationContext.GetNodeIdentifier and + /// GraphSerializationContext.GetNodeFromIdentifier methods + /// for serialization and deserialization respectively. + /// </summary> + static byte[] SerializeGraphNodeReferences (NavGraph graph, bool[] persistentGraphs) { + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + var ctx = new GraphSerializationContext(writer, persistentGraphs); + + graph.GetNodes(node => node.SerializeReferences(ctx)); + writer.Close(); + + var bytes = stream.ToArray(); + return bytes; + } + + public void SerializeExtraInfo () { + if (!settings.nodes) return; + if (graphs == null) throw new InvalidOperationException("Cannot serialize extra info with no serialized graphs (call SerializeGraphs first)"); + + var bytes = SerializeNodeIndices(graphs); + AddChecksum(bytes); + AddEntry("graph_references"+binaryExt, bytes); + + for (int i = 0; i < graphs.Length; i++) { + if (graphs[i] == null || !graphs[i].persistent) continue; + + bytes = SerializeGraphExtraInfo(graphs[i], persistentGraphs); + AddChecksum(bytes); + AddEntry("graph"+i+"_extra"+binaryExt, bytes); + + bytes = SerializeGraphNodeReferences(graphs[i], persistentGraphs); + AddChecksum(bytes); + AddEntry("graph"+i+"_references"+binaryExt, bytes); + } + } + + #endregion + + #region Deserialize + + ZipEntry GetEntry (string name) { +#if NETFX_CORE + return zip.GetEntry(name); +#else + return zip[name]; +#endif + } + + bool ContainsEntry (string name) { + return GetEntry(name) != null; + } + + public bool OpenDeserialize (byte[] bytes) { + // Copy the bytes to a stream + zipStream = new MemoryStream(); + zipStream.Write(bytes, 0, bytes.Length); + zipStream.Position = 0; + try { +#if NETFX_CORE + zip = new ZipFile(zipStream); +#else + zip = ZipFile.Read(zipStream); + // Don't use parallel defate + zip.ParallelDeflateThreshold = -1; +#endif + } catch (Exception e) { + // Catches exceptions when an invalid zip file is found + Debug.LogError("Caught exception when loading from zip\n"+e); + + zipStream.Dispose(); + return false; + } + + if (ContainsEntry("meta" + jsonExt)) { + meta = DeserializeMeta(GetEntry("meta" + jsonExt)); + } else if (ContainsEntry("meta" + binaryExt)) { + meta = DeserializeBinaryMeta(GetEntry("meta" + binaryExt)); + } else { + throw new Exception("No metadata found in serialized data."); + } + + if (FullyDefinedVersion(meta.version) > FullyDefinedVersion(AstarPath.Version)) { + Debug.LogWarning("Trying to load data from a newer version of the A* Pathfinding Project\nCurrent version: "+AstarPath.Version+" Data version: "+meta.version + + "\nThis is usually fine as the stored data is usually backwards and forwards compatible." + + "\nHowever node data (not settings) can get corrupted between versions (even though I try my best to keep compatibility), so it is recommended " + + "to recalculate any caches (those for faster startup) and resave any files. Even if it seems to load fine, it might cause subtle bugs.\n"); + } + return true; + } + + /// <summary> + /// Returns a version with all fields fully defined. + /// This is used because by default new Version(3,0,0) > new Version(3,0). + /// This is not the desired behaviour so we make sure that all fields are defined here + /// </summary> + static System.Version FullyDefinedVersion (System.Version v) { + return new System.Version(Mathf.Max(v.Major, 0), Mathf.Max(v.Minor, 0), Mathf.Max(v.Build, 0), Mathf.Max(v.Revision, 0)); + } + + public void CloseDeserialize () { + zipStream.Dispose(); + zip.Dispose(); + zip = null; + zipStream = null; + } + + NavGraph DeserializeGraph (int zipIndex, int graphIndex, System.Type[] availableGraphTypes) { + // Get the graph type from the metadata we deserialized earlier + var graphType = meta.GetGraphType(zipIndex, availableGraphTypes); + + // Graph was null when saving, ignore + if (System.Type.Equals(graphType, null)) return null; + + // Create a new graph of the right type + NavGraph graph = data.CreateGraph(graphType); + graph.graphIndex = (uint)(graphIndex); + + var jsonName = "graph" + zipIndex + jsonExt; + + if (ContainsEntry(jsonName)) { + // Read the graph settings + TinyJsonDeserializer.Deserialize(GetString(GetEntry(jsonName)), graphType, graph, contextRoot); + } else { + throw new FileNotFoundException("Could not find data for graph " + zipIndex + " in zip. Entry 'graph" + zipIndex + jsonExt + "' does not exist"); + } + + if (graph.guid.ToString() != meta.guids[zipIndex]) + throw new Exception("Guid in graph file not equal to guid defined in meta file. Have you edited the data manually?\n"+graph.guid+" != "+meta.guids[zipIndex]); + + return graph; + } + + /// <summary> + /// Deserializes graph settings. + /// Note: Stored in files named "graph<see cref=".json"/>" where # is the graph number. + /// </summary> + public NavGraph[] DeserializeGraphs (System.Type[] availableGraphTypes) { + // Allocate a list of graphs to be deserialized + var graphList = new List<NavGraph>(); + + graphIndexInZip = new Dictionary<NavGraph, int>(); + + for (int i = 0; i < meta.graphs; i++) { + var newIndex = graphList.Count + graphIndexOffset; + var graph = DeserializeGraph(i, newIndex, availableGraphTypes); + if (graph != null) { + graphList.Add(graph); + graphIndexInZip[graph] = i; + } + } + + graphs = graphList.ToArray(); + + DeserializeEditorSettingsCompatibility(); + DeserializeExtraInfo(); + + return graphs; + } + + bool DeserializeExtraInfo (NavGraph graph) { + var zipIndex = graphIndexInZip[graph]; + var entry = GetEntry("graph"+zipIndex+"_extra"+binaryExt); + + if (entry == null) + return false; + + var reader = GetBinaryReader(entry); + + var ctx = new GraphSerializationContext(reader, null, graph.graphIndex, meta); + + // Call the graph to process the data + ((IGraphInternals)graph).DeserializeExtraInfo(ctx); + return true; + } + + bool AnyDestroyedNodesInGraphs () { + bool result = false; + + for (int i = 0; i < graphs.Length; i++) { + graphs[i].GetNodes(node => { + if (node.Destroyed) { + result = true; + } + }); + } + return result; + } + + GraphNode[] DeserializeNodeReferenceMap () { + // Get the file containing the list of all node indices + // This is correlated with the new indices of the nodes and a mapping from old to new + // is done so that references can be resolved + var entry = GetEntry("graph_references"+binaryExt); + + if (entry == null) throw new Exception("Node references not found in the data. Was this loaded from an older version of the A* Pathfinding Project?"); + + var reader = GetBinaryReader(entry); + int maxNodeIndex = reader.ReadInt32(); + var int2Node = new GraphNode[maxNodeIndex+1]; + + try { + for (int i = 0; i < graphs.Length; i++) { + graphs[i].GetNodes(node => { + var index = reader.ReadInt32(); + int2Node[index] = node; + }); + } + } catch (Exception e) { + throw new Exception("Some graph(s) has thrown an exception during GetNodes, or some graph(s) have deserialized more or fewer nodes than were serialized", e); + } + +#if !NETFX_CORE + // For Windows Store apps the BaseStream.Position property is not supported + // so we have to disable this error check on that platform + if (reader.BaseStream.Position != reader.BaseStream.Length) { + throw new Exception((reader.BaseStream.Length / 4) + " nodes were serialized, but only data for " + (reader.BaseStream.Position / 4) + " nodes was found. The data looks corrupt."); + } +#endif + + reader.Close(); + return int2Node; + } + + void DeserializeNodeReferences (NavGraph graph, GraphNode[] int2Node) { + var zipIndex = graphIndexInZip[graph]; + var entry = GetEntry("graph"+zipIndex+"_references"+binaryExt); + + if (entry == null) throw new Exception("Node references for graph " + zipIndex + " not found in the data. Was this loaded from an older version of the A* Pathfinding Project?"); + + var reader = GetBinaryReader(entry); + var ctx = new GraphSerializationContext(reader, int2Node, graph.graphIndex, meta); + + graph.GetNodes(node => node.DeserializeReferences(ctx)); + } + + void DeserializeAndRemoveOldNodeLinks (GraphSerializationContext ctx) { + var count = ctx.reader.ReadInt32(); + for (int i = 0; i < count; i++) { + var linkID = ctx.reader.ReadUInt64(); + var startNode = ctx.DeserializeNodeReference(); + var endNode = ctx.DeserializeNodeReference(); + var connectedNode1 = ctx.DeserializeNodeReference(); + var connectedNode2 = ctx.DeserializeNodeReference(); + var clamped1 = ctx.DeserializeVector3(); + var clamped2 = ctx.DeserializeVector3(); + var postScanCalled = ctx.reader.ReadBoolean(); + + startNode.ClearConnections(true); + endNode.ClearConnections(true); + startNode.Walkable = false; + endNode.Walkable = false; + // In case of one-way links + GraphNode.Disconnect(connectedNode1, startNode); + GraphNode.Disconnect(connectedNode2, endNode); + } + + bool graphRemoved = false; + for (int i = 0; i < graphs.Length && !graphRemoved; i++) { + if (graphs[i] != null && graphs[i] is PointGraph pointGraph) { + bool anyWalkable = false; + int count2 = 0; + pointGraph.GetNodes(node => { + anyWalkable |= node.Walkable; + count2++; + }); + if (!anyWalkable && pointGraph.root == null && 2*count == count2 && (count2 > 0 || pointGraph.name.Contains("used for node links"))) { + // This is very likely an off-mesh link graph that was automatically created + // by the system in an earlier version + // It is not used anymore and should be removed + ((IGraphInternals)graphs[i]).DestroyAllNodes(); + var ls = new List<NavGraph>(graphs); + ls.RemoveAt(i); + graphs = ls.ToArray(); + graphRemoved = true; + } + if (pointGraph.name == "PointGraph (used for node links)") { + pointGraph.name = "PointGraph"; + } + } + } + + if (!graphRemoved && count > 0) { + Debug.LogWarning("Old off-mesh links were present in the serialized graph data. Not everything could be cleaned up properly. It is recommended that you re-scan all graphs and save the cache or graph file again. An attempt to migrate the old links was made, but a stray point graph may have been left behind."); + } + } + + + + /// <summary> + /// Deserializes extra graph info. + /// Extra graph info is specified by the graph types. + /// See: Pathfinding.NavGraph.DeserializeExtraInfo + /// Note: Stored in files named "graph<see cref="_extra.binary"/>" where # is the graph number. + /// </summary> + void DeserializeExtraInfo () { + bool anyDeserialized = false; + + // Loop through all graphs and deserialize the extra info + // if there is any such info in the zip file + for (int i = 0; i < graphs.Length; i++) { + anyDeserialized |= DeserializeExtraInfo(graphs[i]); + } + + if (!anyDeserialized) { + return; + } + + // Sanity check + // Make sure the graphs don't contain destroyed nodes + if (AnyDestroyedNodesInGraphs()) { + Debug.LogError("Graph contains destroyed nodes. This is a bug."); + } + + // Deserialize map from old node indices to new nodes + var int2Node = DeserializeNodeReferenceMap(); + + // Deserialize node references + for (int i = 0; i < graphs.Length; i++) { + DeserializeNodeReferences(graphs[i], int2Node); + } + + if (meta.version < V4_3_85) { + var entry = GetEntry("node_link2"+binaryExt); + + if (entry != null) { + var reader = GetBinaryReader(entry); + var ctx = new GraphSerializationContext(reader, int2Node, 0, meta); + DeserializeAndRemoveOldNodeLinks(ctx); + } + } + } + + /// <summary>Calls PostDeserialization on all loaded graphs</summary> + public void PostDeserialization () { + for (int i = 0; i < graphs.Length; i++) { + var ctx = new GraphSerializationContext(null, null, 0, meta); + ((IGraphInternals)graphs[i]).PostDeserialization(ctx); + } + } + + /// <summary> + /// Deserializes graph editor settings. + /// For future compatibility this method does not assume that the graphEditors array matches the <see cref="graphs"/> array in order and/or count. + /// It searches for a matching graph (matching if graphEditor.target == graph) for every graph editor. + /// Multiple graph editors should not refer to the same graph. + /// Note: Stored in files named "graph<see cref="_editor.json"/>" where # is the graph number. + /// + /// Note: This method is only used for compatibility, newer versions store everything in the graph.serializedEditorSettings field which is already serialized. + /// </summary> + void DeserializeEditorSettingsCompatibility () { + for (int i = 0; i < graphs.Length; i++) { + var zipIndex = graphIndexInZip[graphs[i]]; + ZipEntry entry = GetEntry("graph"+zipIndex+"_editor"+jsonExt); + if (entry == null) continue; + + (graphs[i] as IGraphInternals).SerializedEditorSettings = GetString(entry); + } + } + + /// <summary>Returns a binary reader for the data in the zip entry</summary> + private static BinaryReader GetBinaryReader (ZipEntry entry) { +#if NETFX_CORE + return new BinaryReader(entry.Open()); +#else + var stream = new System.IO.MemoryStream(); + + entry.Extract(stream); + stream.Position = 0; + return new System.IO.BinaryReader(stream); +#endif + } + + /// <summary>Returns the data in the zip entry as a string</summary> + private static string GetString (ZipEntry entry) { +#if NETFX_CORE + var reader = new StreamReader(entry.Open()); +#else + var buffer = new MemoryStream(); + + entry.Extract(buffer); + buffer.Position = 0; + var reader = new StreamReader(buffer); +#endif + string s = reader.ReadToEnd(); + reader.Dispose(); + return s; + } + + private GraphMeta DeserializeMeta (ZipEntry entry) { + return TinyJsonDeserializer.Deserialize(GetString(entry), typeof(GraphMeta)) as GraphMeta; + } + + private GraphMeta DeserializeBinaryMeta (ZipEntry entry) { + var meta = new GraphMeta(); + + var reader = GetBinaryReader(entry); + + if (reader.ReadString() != "A*") throw new System.Exception("Invalid magic number in saved data"); + int major = reader.ReadInt32(); + int minor = reader.ReadInt32(); + int build = reader.ReadInt32(); + int revision = reader.ReadInt32(); + + // Required because when saving a version with a field not set, it will save it as -1 + // and then the Version constructor will throw an exception (which we do not want) + if (major < 0) meta.version = new Version(0, 0); + else if (minor < 0) meta.version = new Version(major, 0); + else if (build < 0) meta.version = new Version(major, minor); + else if (revision < 0) meta.version = new Version(major, minor, build); + else meta.version = new Version(major, minor, build, revision); + + meta.graphs = reader.ReadInt32(); + + meta.guids = new List<string>(); + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) meta.guids.Add(reader.ReadString()); + + meta.typeNames = new List<string>(); + count = reader.ReadInt32(); + for (int i = 0; i < count; i++) meta.typeNames.Add(reader.ReadString()); + reader.Close(); + + return meta; + } + + + #endregion + + #region Utils + + /// <summary>Save the specified data at the specified path</summary> + public static void SaveToFile (string path, byte[] data) { +#if NETFX_CORE + throw new System.NotSupportedException("Cannot save to file on this platform"); +#else + using (var stream = new FileStream(path, FileMode.Create)) { + stream.Write(data, 0, data.Length); + } +#endif + } + + /// <summary>Load the specified data from the specified path</summary> + public static byte[] LoadFromFile (string path) { +#if NETFX_CORE + throw new System.NotSupportedException("Cannot load from file on this platform"); +#else + using (var stream = new FileStream(path, FileMode.Open)) { + var bytes = new byte[(int)stream.Length]; + stream.Read(bytes, 0, (int)stream.Length); + return bytes; + } +#endif + } + + #endregion + } + + /// <summary>Metadata for all graphs included in serialization</summary> + public class GraphMeta { + /// <summary>Project version it was saved with</summary> + public Version version; + + /// <summary>Number of graphs serialized</summary> + public int graphs; + + /// <summary>Guids for all graphs</summary> + public List<string> guids; + + /// <summary>Type names for all graphs</summary> + public List<string> typeNames; + + /// <summary>Returns the Type of graph number index</summary> + public Type GetGraphType (int index, System.Type[] availableGraphTypes) { + // The graph was null when saving. Ignore it + if (String.IsNullOrEmpty(typeNames[index])) return null; + + for (int j = 0; j < availableGraphTypes.Length; j++) { + if (availableGraphTypes[j].FullName == typeNames[index]) return availableGraphTypes[j]; + } + + throw new Exception("No graph of type '" + typeNames[index] + "' could be created, type does not exist"); + } + } + + /// <summary>Holds settings for how graphs should be serialized</summary> + public class SerializeSettings { + /// <summary> + /// Enable to include node data. + /// If false, only settings will be saved + /// </summary> + public bool nodes = true; + + /// <summary> + /// Use pretty printing for the json data. + /// Good if you want to open up the saved data and edit it manually + /// </summary> + [System.Obsolete("There is no support for pretty printing the json anymore")] + public bool prettyPrint; + + /// <summary> + /// Save editor settings. + /// Warning: Only applicable when saving from the editor using the AstarPathEditor methods + /// </summary> + public bool editorSettings; + + /// <summary>Serialization settings for only saving graph settings</summary> + public static SerializeSettings Settings { + get { + return new SerializeSettings { + nodes = false + }; + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/JsonSerializer.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/JsonSerializer.cs.meta new file mode 100644 index 0000000..4c4b58f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/JsonSerializer.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: edd6db33319e94141bb849f4f58261c3 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/SimpleZipReplacement.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/SimpleZipReplacement.cs new file mode 100644 index 0000000..28f69b2 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/SimpleZipReplacement.cs @@ -0,0 +1,80 @@ +#if ASTAR_NO_ZIP +using UnityEngine; +using System.Collections; +using System.Collections.Generic; + +namespace Pathfinding.Serialization.Zip { + public enum ZipOption { + Always + } + + public class ZipFile { + public System.Text.Encoding AlternateEncoding; + public ZipOption AlternateEncodingUsage = ZipOption.Always; + public int ParallelDeflateThreshold = 0; + + Dictionary<string, ZipEntry> dict = new Dictionary<string, ZipEntry>(); + + public void AddEntry (string name, byte[] bytes) { + dict[name] = new ZipEntry(name, bytes); + } + + public bool ContainsEntry (string name) { + return dict.ContainsKey(name); + } + + public void Save (System.IO.Stream stream) { + var writer = new System.IO.BinaryWriter(stream); + + writer.Write(dict.Count); + foreach (KeyValuePair<string, ZipEntry> pair in dict) { + writer.Write(pair.Key); + writer.Write(pair.Value.bytes.Length); + writer.Write(pair.Value.bytes); + } + } + + public static ZipFile Read (System.IO.Stream stream) { + ZipFile file = new ZipFile(); + + var reader = new System.IO.BinaryReader(stream); + int count = reader.ReadInt32(); + + for (int i = 0; i < count; i++) { + var name = reader.ReadString(); + var length = reader.ReadInt32(); + var bytes = reader.ReadBytes(length); + + file.dict[name] = new ZipEntry(name, bytes); + } + + return file; + } + + public ZipEntry this[string index] { + get { + ZipEntry v; + dict.TryGetValue(index, out v); + return v; + } + } + + public void Dispose () { + } + } + + public class ZipEntry { + internal string name; + internal byte[] bytes; + + public ZipEntry (string name, byte[] bytes) { + this.name = name; + this.bytes = bytes; + } + + public void Extract (System.IO.Stream stream) { + stream.Write(bytes, 0, bytes.Length); + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/SimpleZipReplacement.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/SimpleZipReplacement.cs.meta new file mode 100644 index 0000000..55b4b94 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/SimpleZipReplacement.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e4ab3c0aea7564a9c9af3bf72ed95858 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/TinyJson.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/TinyJson.cs new file mode 100644 index 0000000..b8fcfcb --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/TinyJson.cs @@ -0,0 +1,527 @@ +using UnityEngine; +using System.Collections.Generic; +using Pathfinding.Util; +using Pathfinding.WindowsStore; +using System; +using System.Linq; +#if NETFX_CORE +using WinRTLegacy; +#endif + +namespace Pathfinding.Serialization { + public class JsonMemberAttribute : System.Attribute { + } + public class JsonOptInAttribute : System.Attribute { + } + /// <summary>Indicates that the full type of the instance will always be serialized. This allows inheritance to work properly.</summary> + public class JsonDynamicTypeAttribute : System.Attribute { + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class JsonDynamicTypeAliasAttribute : System.Attribute { + public string alias; + public Type type; + + public JsonDynamicTypeAliasAttribute (string alias, Type type) { + this.alias = alias; + this.type = type; + } + } + + // Make sure the class is not stripped out when using code stripping (see https://docs.unity3d.com/Manual/ManagedCodeStripping.html) + [Pathfinding.Util.Preserve] + class SerializableAnimationCurve { + public WrapMode preWrapMode, postWrapMode; + public Keyframe[] keys; + } + + /// <summary> + /// A very tiny json serializer. + /// It is not supposed to have lots of features, it is only intended to be able to serialize graph settings + /// well enough. + /// </summary> + public class TinyJsonSerializer { + System.Text.StringBuilder output = new System.Text.StringBuilder(); + + Dictionary<Type, Action<System.Object> > serializers = new Dictionary<Type, Action<object> >(); + + static readonly System.Globalization.CultureInfo invariantCulture = System.Globalization.CultureInfo.InvariantCulture; + + public static void Serialize (System.Object obj, System.Text.StringBuilder output) { + new TinyJsonSerializer() { + output = output + }.Serialize(obj); + } + + TinyJsonSerializer () { + serializers[typeof(float)] = v => output.Append(((float)v).ToString("R", invariantCulture)); + serializers[typeof(bool)] = v => output.Append((bool)v ? "true" : "false"); + serializers[typeof(Version)] = serializers[typeof(uint)] = serializers[typeof(int)] = v => output.Append(v.ToString()); + serializers[typeof(string)] = v => output.AppendFormat("\"{0}\"", v.ToString().Replace("\"", "\\\"")); + serializers[typeof(Vector2)] = v => output.AppendFormat("{{ \"x\": {0}, \"y\": {1} }}", ((Vector2)v).x.ToString("R", invariantCulture), ((Vector2)v).y.ToString("R", invariantCulture)); + serializers[typeof(Vector3)] = v => output.AppendFormat("{{ \"x\": {0}, \"y\": {1}, \"z\": {2} }}", ((Vector3)v).x.ToString("R", invariantCulture), ((Vector3)v).y.ToString("R", invariantCulture), ((Vector3)v).z.ToString("R", invariantCulture)); + serializers[typeof(Pathfinding.Util.Guid)] = v => output.AppendFormat("{{ \"value\": \"{0}\" }}", v.ToString()); + serializers[typeof(LayerMask)] = v => output.AppendFormat("{{ \"value\": {0} }}", ((int)(LayerMask)v).ToString()); + } + + void Serialize (System.Object obj, bool serializePrivateFieldsByDefault = false) { + if (obj == null) { + output.Append("null"); + return; + } + + var type = obj.GetType(); + var typeInfo = WindowsStoreCompatibility.GetTypeInfo(type); + if (serializers.ContainsKey(type)) { + serializers[type] (obj); + } else if (typeInfo.IsEnum) { + output.Append('"' + obj.ToString() + '"'); + } else if (obj is System.Collections.IList) { + output.Append("["); + var arr = obj as System.Collections.IList; + for (int i = 0; i < arr.Count; i++) { + if (i != 0) + output.Append(", "); + Serialize(arr[i], serializePrivateFieldsByDefault); + } + output.Append("]"); + } else if (obj is AnimationCurve) { + var curve = obj as AnimationCurve; + Serialize(new SerializableAnimationCurve { preWrapMode = curve.preWrapMode, postWrapMode = curve.postWrapMode, keys = curve.keys }, true); + } else if (obj is UnityEngine.Object) { + SerializeUnityObject(obj as UnityEngine.Object); + } else { +#if NETFX_CORE + var optIn = typeInfo.CustomAttributes.Any(attr => attr.GetType() == typeof(JsonOptInAttribute)); +#else + var optIn = typeInfo.GetCustomAttributes(typeof(JsonOptInAttribute), true).Length > 0; +#endif + output.Append("{"); + bool earlier = false; + + if (typeInfo.GetCustomAttributes(typeof(JsonDynamicTypeAttribute), true).Length > 0) { + output.AppendFormat("\"@type\": \"{0}\"", typeInfo.AssemblyQualifiedName); + earlier = true; + } + + while (true) { +#if NETFX_CORE + var fields = typeInfo.DeclaredFields.Where(f => !f.IsStatic).ToArray(); +#else + var fields = type.GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); +#endif + foreach (var field in fields) { + if (field.DeclaringType != type) continue; + if ((!optIn && (field.IsPublic || serializePrivateFieldsByDefault)) || +#if NETFX_CORE + field.CustomAttributes.Any(attr => attr.GetType() == typeof(JsonMemberAttribute)) +#else + field.GetCustomAttributes(typeof(JsonMemberAttribute), true).Length > 0 +#endif + ) { + if (earlier) { + output.Append(", "); + } + + earlier = true; + output.AppendFormat("\"{0}\": ", field.Name); + Serialize(field.GetValue(obj), serializePrivateFieldsByDefault); + } + } + +#if NETFX_CORE + typeInfo = typeInfo.BaseType; + if (typeInfo == null) break; +#else + type = type.BaseType; + if (type == null) break; +#endif + } + output.Append("}"); + } + } + + void QuotedField (string name, string contents) { + output.AppendFormat("\"{0}\": \"{1}\"", name, contents); + } + + void SerializeUnityObject (UnityEngine.Object obj) { + // Note that a unityengine can be destroyed as well + if (obj == null) { + Serialize(null); + return; + } + + output.Append("{"); + var path = obj.name; +#if UNITY_EDITOR + // Figure out the path of the object relative to a Resources folder. + // In a standalone player this cannot be done unfortunately, so we will assume it is at the top level in the Resources folder. + // Fortunately it should be extremely rare to have to serialize references to unity objects in a standalone player. + var realPath = UnityEditor.AssetDatabase.GetAssetPath(obj); + var match = System.Text.RegularExpressions.Regex.Match(realPath, @"Resources/(.*?)(\.\w+)?$"); + if (match.Success) path = match.Groups[1].Value; +#endif + QuotedField("Name", path); + output.Append(", "); + QuotedField("Type", obj.GetType().FullName); + + //Write scene path if the object is a Component or GameObject + var component = obj as Component; + var go = obj as GameObject; + + if (component != null || go != null) { + if (component != null) { + go = component.gameObject; + } + + var helper = go.GetComponent<UnityReferenceHelper>(); + + if (helper == null) { + Debug.Log("Adding UnityReferenceHelper to Unity Reference '"+obj.name+"'"); + helper = go.AddComponent<UnityReferenceHelper>(); + } + + //Make sure it has a unique GUID + helper.Reset(); + output.Append(", "); + QuotedField("GUID", helper.GetGUID().ToString()); + } + output.Append("}"); + } + } + + /// <summary> + /// A very tiny json deserializer. + /// It is not supposed to have lots of features, it is only intended to be able to deserialize graph settings + /// well enough. Not much validation of the input is done. + /// </summary> + public class TinyJsonDeserializer { + System.IO.TextReader reader; + string fullTextDebug; + GameObject contextRoot; + + static readonly System.Globalization.NumberFormatInfo numberFormat = System.Globalization.NumberFormatInfo.InvariantInfo; + + /// <summary> + /// Deserializes an object of the specified type. + /// Will load all fields into the populate object if it is set (only works for classes). + /// </summary> + public static System.Object Deserialize (string text, Type type, System.Object populate = null, GameObject contextRoot = null) { + return new TinyJsonDeserializer() { + reader = new System.IO.StringReader(text), + fullTextDebug = text, + contextRoot = contextRoot, + }.Deserialize(type, populate); + } + + /// <summary> + /// Deserializes an object of type tp. + /// Will load all fields into the populate object if it is set (only works for classes). + /// </summary> + System.Object Deserialize (Type tp, System.Object populate = null) { + var tpInfo = WindowsStoreCompatibility.GetTypeInfo(tp); + + if (tpInfo.IsEnum) { + return Enum.Parse(tp, EatField()); + } else if (TryEat('n')) { + Eat("ull"); + TryEat(','); + return null; + } else if (Type.Equals(tp, typeof(float))) { + return float.Parse(EatField(), numberFormat); + } else if (Type.Equals(tp, typeof(int))) { + return int.Parse(EatField(), numberFormat); + } else if (Type.Equals(tp, typeof(uint))) { + return uint.Parse(EatField(), numberFormat); + } else if (Type.Equals(tp, typeof(bool))) { + return bool.Parse(EatField()); + } else if (Type.Equals(tp, typeof(string))) { + return EatField(); + } else if (Type.Equals(tp, typeof(Version))) { + return new Version(EatField()); + } else if (Type.Equals(tp, typeof(Vector2))) { + Eat("{"); + var result = new Vector2(); + EatField(); + result.x = float.Parse(EatField(), numberFormat); + EatField(); + result.y = float.Parse(EatField(), numberFormat); + Eat("}"); + return result; + } else if (Type.Equals(tp, typeof(Vector3))) { + Eat("{"); + var result = new Vector3(); + EatField(); + result.x = float.Parse(EatField(), numberFormat); + EatField(); + result.y = float.Parse(EatField(), numberFormat); + EatField(); + result.z = float.Parse(EatField(), numberFormat); + Eat("}"); + return result; + } else if (Type.Equals(tp, typeof(Pathfinding.Util.Guid))) { + Eat("{"); + EatField(); + var result = Pathfinding.Util.Guid.Parse(EatField()); + Eat("}"); + return result; + } else if (Type.Equals(tp, typeof(LayerMask))) { + Eat("{"); + EatField(); + var result = (LayerMask)int.Parse(EatField()); + Eat("}"); + return result; + } else if (tp.IsGenericType && Type.Equals(tp.GetGenericTypeDefinition(), typeof(List<>))) { + System.Collections.IList result = (System.Collections.IList)System.Activator.CreateInstance(tp); + var elementType = tp.GetGenericArguments()[0]; + + Eat("["); + while (!TryEat(']')) { + result.Add(Deserialize(elementType)); + TryEat(','); + } + return result; + } else if (tpInfo.IsArray) { + List<System.Object> ls = new List<System.Object>(); + Eat("["); + while (!TryEat(']')) { + ls.Add(Deserialize(tp.GetElementType())); + TryEat(','); + } + var arr = Array.CreateInstance(tp.GetElementType(), ls.Count); + ls.ToArray().CopyTo(arr, 0); + return arr; + } else if (typeof(UnityEngine.Object).IsAssignableFrom(tp)) { + return DeserializeUnityObject(); + } else { + Eat("{"); + + if (tpInfo.GetCustomAttributes(typeof(JsonDynamicTypeAttribute), true).Length > 0) { + string name = EatField(); + if (name != "@type") { + throw new System.Exception("Expected field '@type' but found '" + name + "'" + "\n\nWhen trying to deserialize: " + fullTextDebug); + } + + string typeName = EatField(); + + var aliases = tpInfo.GetCustomAttributes(typeof(JsonDynamicTypeAliasAttribute), true) as JsonDynamicTypeAliasAttribute[]; + var simpleTypeName = typeName.Split(',')[0]; + Type newType = null; + foreach (var alias in aliases) { + if (alias.alias == simpleTypeName) newType = alias.type; + } + + if (newType == null) newType = Type.GetType(typeName); + tp = newType ?? throw new System.Exception("Could not find a type with the name '" + typeName + "'" + "\n\nWhen trying to deserialize: " + fullTextDebug); + tpInfo = WindowsStoreCompatibility.GetTypeInfo(tp); + } + + var obj = populate ?? Activator.CreateInstance(tp); + while (!TryEat('}')) { + var name = EatField(); + var tmpType = tp; + System.Reflection.FieldInfo field = null; + while (field == null && tmpType != null) { + field = tmpType.GetField(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + tmpType = tmpType.BaseType; + } + + if (field == null) { + // Try a property instead + System.Reflection.PropertyInfo prop = null; + tmpType = tp; + while (prop == null && tmpType != null) { + prop = tmpType.GetProperty(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + tmpType = tmpType.BaseType; + } + + if (prop == null) { + SkipFieldData(); + } else { + prop.SetValue(obj, Deserialize(prop.PropertyType)); + } + } else { + field.SetValue(obj, Deserialize(field.FieldType)); + } + TryEat(','); + } + return obj; + } + } + + UnityEngine.Object DeserializeUnityObject () { + Eat("{"); + var result = DeserializeUnityObjectInner(); + Eat("}"); + return result; + } + + UnityEngine.Object DeserializeUnityObjectInner () { + // Ignore InstanceID field (compatibility only) + var fieldName = EatField(); + + if (fieldName == "InstanceID") { + EatField(); + fieldName = EatField(); + } + + if (fieldName != "Name") throw new Exception("Expected 'Name' field"); + string name = EatField(); + + if (name == null) return null; + + if (EatField() != "Type") throw new Exception("Expected 'Type' field"); + string typename = EatField(); + + // Remove assembly information + if (typename.IndexOf(',') != -1) { + typename = typename.Substring(0, typename.IndexOf(',')); + } + + // Note calling through assembly is more stable on e.g WebGL + var type = WindowsStoreCompatibility.GetTypeInfo(typeof(AstarPath)).Assembly.GetType(typename); + type = type ?? WindowsStoreCompatibility.GetTypeInfo(typeof(Transform)).Assembly.GetType(typename); + + if (Type.Equals(type, null)) { + Debug.LogError("Could not find type '"+typename+"'. Cannot deserialize Unity reference"); + return null; + } + + // Check if there is another field there + EatWhitespace(); + if ((char)reader.Peek() == '"') { + if (EatField() != "GUID") throw new Exception("Expected 'GUID' field"); + string guid = EatField(); + + if (contextRoot != null) { + foreach (var helper in contextRoot.GetComponentsInChildren<UnityReferenceHelper>(true)) { + if (helper.GetGUID() == guid) { + if (Type.Equals(type, typeof(GameObject))) { + return helper.gameObject; + } else { + return helper.GetComponent(type); + } + } + } + } + + foreach (var helper in UnityCompatibility.FindObjectsByTypeUnsortedWithInactive<UnityReferenceHelper>()) { + if (helper.GetGUID() == guid) { + if (Type.Equals(type, typeof(GameObject))) { + return helper.gameObject; + } else { + return helper.GetComponent(type); + } + } + } + } + + // Note: calling LoadAll with an empty string will make it load the whole resources folder, which is probably a bad idea. + if (!string.IsNullOrEmpty(name)) { + // Try to load from resources + UnityEngine.Object[] objs = Resources.LoadAll(name, type); + + for (int i = 0; i < objs.Length; i++) { + if (objs[i].name == name || objs.Length == 1) { + return objs[i]; + } + } + } + + return null; + } + + void EatWhitespace () { + while (char.IsWhiteSpace((char)reader.Peek())) + reader.Read(); + } + + void Eat (string s) { + EatWhitespace(); + for (int i = 0; i < s.Length; i++) { + var c = (char)reader.Read(); + if (c != s[i]) { + throw new Exception("Expected '" + s[i] + "' found '" + c + "'\n\n..." + reader.ReadLine() + "\n\nWhen trying to deserialize: " + fullTextDebug); + } + } + } + + System.Text.StringBuilder builder = new System.Text.StringBuilder(); + string EatUntil (string c, bool inString) { + builder.Length = 0; + bool escape = false; + while (true) { + var readInt = reader.Peek(); + if (!escape && (char)readInt == '"') { + inString = !inString; + } + + var readChar = (char)readInt; + if (readInt == -1) { + throw new Exception("Unexpected EOF" + "\n\nWhen trying to deserialize: " + fullTextDebug); + } else if (!escape && readChar == '\\') { + escape = true; + reader.Read(); + } else if (!inString && c.IndexOf(readChar) != -1) { + break; + } else { + builder.Append(readChar); + reader.Read(); + escape = false; + } + } + + return builder.ToString(); + } + + bool TryEat (char c) { + EatWhitespace(); + if ((char)reader.Peek() == c) { + reader.Read(); + return true; + } + return false; + } + + string EatField () { + var result = EatUntil("\",}]", TryEat('"')); + + TryEat('\"'); + TryEat(':'); + TryEat(','); + return result; + } + + void SkipFieldData () { + var indent = 0; + + while (true) { + EatUntil(",{}[]", false); + var last = (char)reader.Peek(); + + switch (last) { + case '{': + case '[': + indent++; + break; + case '}': + case ']': + indent--; + if (indent < 0) return; + break; + case ',': + if (indent == 0) { + reader.Read(); + return; + } + break; + default: + throw new System.Exception("Should not reach this part"); + } + + reader.Read(); + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/TinyJson.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/TinyJson.cs.meta new file mode 100644 index 0000000..84772f6 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Serialization/TinyJson.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 761fd44fdff0b40648c9b7ce7763a06f +timeCreated: 1463169419 +licenseType: Store +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/astarclasses.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/astarclasses.cs new file mode 100644 index 0000000..b5bd900 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/astarclasses.cs @@ -0,0 +1,1522 @@ +using UnityEngine; +using System.Collections.Generic; + +// Empty namespace declaration to avoid errors in the free version +// Which does not have any classes in the RVO namespace +namespace Pathfinding.RVO {} + +namespace Pathfinding { + using Pathfinding.Jobs; + using Pathfinding.Util; + using Unity.Burst; + using Unity.Collections; + using Unity.Jobs; + + [System.Serializable] + /// <summary>Stores editor colors</summary> + public class AstarColor : ISerializationCallbackReceiver { + public Color _SolidColor; + public Color _UnwalkableNode; + public Color _BoundsHandles; + + public Color _ConnectionLowLerp; + public Color _ConnectionHighLerp; + + public Color _MeshEdgeColor; + + /// <summary> + /// Holds user set area colors. + /// Use GetAreaColor to get an area color + /// </summary> + public Color[] _AreaColors; + + public static Color SolidColor = new Color(30/255f, 102/255f, 201/255f, 0.9F); + public static Color UnwalkableNode = new Color(1, 0, 0, 0.5F); + public static Color BoundsHandles = new Color(0.29F, 0.454F, 0.741F, 0.9F); + + public static Color ConnectionLowLerp = new Color(0, 1, 0, 0.5F); + public static Color ConnectionHighLerp = new Color(1, 0, 0, 0.5F); + + public static Color MeshEdgeColor = new Color(0, 0, 0, 0.5F); + + private static Color[] AreaColors = new Color[1]; + + public static int ColorHash () { + var hash = SolidColor.GetHashCode() ^ UnwalkableNode.GetHashCode() ^ BoundsHandles.GetHashCode() ^ ConnectionLowLerp.GetHashCode() ^ ConnectionHighLerp.GetHashCode() ^ MeshEdgeColor.GetHashCode(); + + for (int i = 0; i < AreaColors.Length; i++) hash = 7*hash ^ AreaColors[i].GetHashCode(); + return hash; + } + + /// <summary> + /// Returns an color for an area, uses both user set ones and calculated. + /// If the user has set a color for the area, it is used, but otherwise the color is calculated using AstarMath.IntToColor + /// </summary> + public static Color GetAreaColor (uint area) { + if (area >= AreaColors.Length) return AstarMath.IntToColor((int)area, 1F); + return AreaColors[(int)area]; + } + + /// <summary> + /// Returns an color for a tag, uses both user set ones and calculated. + /// If the user has set a color for the tag, it is used, but otherwise the color is calculated using AstarMath.IntToColor + /// See: <see cref="AreaColors"/> + /// </summary> + public static Color GetTagColor (uint tag) { + if (tag >= AreaColors.Length) return AstarMath.IntToColor((int)tag, 1F); + return AreaColors[(int)tag]; + } + + /// <summary> + /// Pushes all local variables out to static ones. + /// This is done because that makes it so much easier to access the colors during Gizmo rendering + /// and it has a positive performance impact as well (gizmo rendering is hot code). + /// It is a bit ugly though, but oh well. + /// </summary> + public void PushToStatic (AstarPath astar) { + _AreaColors = _AreaColors ?? new Color[0]; + + SolidColor = _SolidColor; + UnwalkableNode = _UnwalkableNode; + BoundsHandles = _BoundsHandles; + ConnectionLowLerp = _ConnectionLowLerp; + ConnectionHighLerp = _ConnectionHighLerp; + MeshEdgeColor = _MeshEdgeColor; + AreaColors = _AreaColors; + } + + public void OnBeforeSerialize () {} + + public void OnAfterDeserialize () { + // Patch bad initialization code in earlier versions that made it start out with a single transparent color. + if (_AreaColors != null && _AreaColors.Length == 1 && _AreaColors[0] == default) { + _AreaColors = new Color[0]; + } + } + + public AstarColor () { + // Set default colors + _SolidColor = new Color(30/255f, 102/255f, 201/255f, 0.9F); + _UnwalkableNode = new Color(1, 0, 0, 0.5F); + _BoundsHandles = new Color(0.29F, 0.454F, 0.741F, 0.9F); + _ConnectionLowLerp = new Color(0, 1, 0, 0.5F); + _ConnectionHighLerp = new Color(1, 0, 0, 0.5F); + _MeshEdgeColor = new Color(0, 0, 0, 0.5F); + } + } + + + /// <summary> + /// Info about what a ray- or linecasts hit. + /// + /// This is the return value of the <see cref="IRaycastableGraph.Linecast"/> methods. + /// Some members will also be initialized even if nothing was hit, see the individual member descriptions for more info. + /// + /// [Open online documentation to see images] + /// </summary> + public struct GraphHitInfo { + /// <summary> + /// Start of the segment/ray. + /// Note that the point passed to the Linecast method will be clamped to the surface on the navmesh, but it will be identical when seen from above. + /// </summary> + public Vector3 origin; + /// <summary> + /// Hit point. + /// + /// This is typically a point on the border of the navmesh. + /// + /// In case no obstacle was hit then this will be set to the endpoint of the segment. + /// + /// If the origin was inside an unwalkable node, then this will be set to the origin point. + /// </summary> + public Vector3 point; + /// <summary> + /// Node which contained the edge which was hit. + /// If the linecast did not hit anything then this will be set to the last node along the line's path (the one which contains the endpoint). + /// + /// For layered grid graphs the linecast will return true (i.e: no free line of sight) if, when walking the graph, we ended up at the right X,Z coordinate for the end node, + /// but the end node was on a different level (e.g the floor below or above in a building). In this case no node edge was really hit so this field will still be null. + /// + /// If the origin was inside an unwalkable node, then this field will be set to that node. + /// + /// If no node could be found which contained the origin, then this field will be set to null. + /// </summary> + public GraphNode node; + /// <summary> + /// Where the tangent starts. <see cref="tangentOrigin"/> and <see cref="tangent"/> together actually describes the edge which was hit. + /// [Open online documentation to see images] + /// + /// If nothing was hit, this will be Vector3.zero. + /// </summary> + public Vector3 tangentOrigin; + /// <summary> + /// Tangent of the edge which was hit. + /// [Open online documentation to see images] + /// + /// If nothing was hit, this will be Vector3.zero. + /// </summary> + public Vector3 tangent; + + /// <summary>Distance from <see cref="origin"/> to <see cref="point"/></summary> + public readonly float distance => (point-origin).magnitude; + } + + /// <summary> + /// Determines how to measure distances to the navmesh. + /// + /// The default is a euclidean distance, which works well for most things. + /// However, another option is to find nodes which are directly below a point. + /// This is very useful if you have a character, and you want to find the closest node to the character's feet. + /// Then projecting the distance so that we find the closest node as seen from above is a good idea. + /// + /// See <see cref="projectionAxis"/> for more info. + /// + /// [Open online documentation to see images] + /// The default distance metric is euclidean distance. This is the same as the distance you would measure in 3D space. + /// Shown in the image above are two parts of a navmesh in a side view. One part is colored blue, and one part colored red. Both are walkable. + /// In the background you can see if points are closer to the blue part, or to the red part. + /// You can also see a few query points in black, which show the closest point on the navmesh to that point. + /// The dashed circle around the first query point shows all points which are at the same distance from the query point. + /// + /// [Open online documentation to see images] + /// When using <see cref="ClosestAsSeenFromAbove"/>, the distance along the up direction is ignored. You can see this in the image + /// above. Note how all query points find their closest point directly below them. + /// Notice in particular the difference in behavior for the query point on the slope. + /// + /// [Open online documentation to see images] + /// When using <see cref="ClosestAsSeenFromAboveSoft"/>, the distance along the up direction is instead scaled by <see cref="distanceScaleAlongProjectionDirection"/> (set to 0.2 by default). + /// This makes it behave almost like <see cref="ClosestAsSeenFromAbove"/>, but it allows small deviations from just looking directly below the query point. + /// The dashed diamond in the image is similarly the set of points equidistant from the query point. + /// This mode is recommended for character movement since it finds points below the agent, but can better handle situations where the agent, for example, steps off a ledge. + /// </summary> + public struct DistanceMetric { + /// <summary> + /// Normal of the plane on which nodes will be projected before finding the closest point on them. + /// + /// When zero, this has no effect. + /// + /// When set to the special value (inf, inf, inf) then the graph's natural up direction will be used. + /// + /// Often you do not want to find the closest point on a node in 3D space, but rather + /// find for example the closest point on the node directly below the agent. + /// + /// This allows you to project the nodes onto a plane before finding the closest point on them. + /// For example, if you set this to Vector3.up, then the nodes will be projected onto the XZ plane. + /// Running a GetNearest query will then find the closest node as seen from above. + /// + /// [Open online documentation to see images] + /// + /// This is more flexible, however. You can set the <see cref="distanceScaleAlongProjectionDirection"/> to any value (though usually somewhere between 0 and 1). + /// With a value of 0, the closest node will be found as seen from above. + /// When the distance is greater than 0, moving along the projectionAxis from the query point will only cost <see cref="distanceScaleAlongProjectionDirection"/> times the regular distance, + /// but moving sideways will cost the normal amount. + /// + /// [Open online documentation to see images] + /// + /// A nice value to use is 0.2 for <see cref="distanceScaleAlongProjectionDirection"/>. This will make moving upwards or downwards (along the projection direction) + /// only appear like 20% the original distance to the nearest node search. + /// This allows you to find the closest position directly under the agent, if there is a navmesh directly under the agent, but also to search + /// not directly below the agent if that is necessary. + /// + /// Note: This is only supported by some graph types. The navmesh/recast graphs fully support this. + /// The grid graphs, however, only support this if the direction is parallel to the natural 'up' direction of the graph. + /// The grid graphs will search as if the direction is in the graph's up direction if this value is anything other than Vector3.zero. + /// Point graphs do not support this field at all, and will completely ignore it. + /// + /// Note: On recast/navmesh graphs there is a performance penalty when using this feature with a direction which is not parallel to the natural up direction of the graph. + /// Typically you should only set it to such values if you have a spherical or non-planar world (see spherical) (view in online documentation for working links). + /// </summary> + public Vector3 projectionAxis; + + /// <summary> + /// Distance scaling along the <see cref="projectionAxis"/>. + /// + /// See: <see cref="projectionAxis"/> for details + /// </summary> + public float distanceScaleAlongProjectionDirection; + + /// <summary>True when using the ClosestAsSeenFromAbove or ClosestAsSeenFromAboveSoft modes</summary> + public bool isProjectedDistance => projectionAxis != Vector3.zero; + + /// <summary> + /// Returns a DistanceMetric which will find the closest node in euclidean 3D space. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="projectionAxis"/> + /// </summary> + public static readonly DistanceMetric Euclidean = new DistanceMetric { projectionAxis = Vector3.zero, distanceScaleAlongProjectionDirection = 0 }; + + /// <summary> + /// Returns a DistanceMetric which will usually find the closest node as seen from above, but allows some sideways translation if that gives us a node which is significantly closer. + /// + /// The "upwards" direction will be set to the graph's natural up direction. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="projectionAxis"/> + /// + /// Note: This is only supported by some graph types. The navmesh/recast/grid graph fully support this. + /// Point graphs do not support this field at all, and will completely ignore it. + /// + /// See: <see cref="ClosestAsSeenFromAbove(Vector3)"/> + /// </summary> + public static DistanceMetric ClosestAsSeenFromAboveSoft () => new DistanceMetric { projectionAxis = Vector3.positiveInfinity, distanceScaleAlongProjectionDirection = 0.2f }; + + /// <summary> + /// Returns a DistanceMetric which will usually find the closest node as seen from above, but allows some sideways translation if that gives us a node which is significantly closer. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="projectionAxis"/> + /// + /// Note: This is only supported by some graph types. The navmesh/recast graphs fully support this. + /// The grid graphs, however, only support this if the direction is parallel to the natural 'up' direction of the graph. + /// The grid graphs will search as if the direction is in the graph's up direction if this value is anything other than Vector3.zero. + /// Point graphs do not support this field at all, and will completely ignore it. + /// + /// Note: On recast/navmesh graphs there is a performance penalty when using this feature with a direction which is not parallel to the natural up direction of the graph. + /// Typically you should only set it to such values if you have a spherical or non-planar world (see spherical) (view in online documentation for working links). + /// + /// Note: If you want to use the graph's natural up direction, use the overload which does not take any parameters. + /// </summary> + /// <param name="up">Defines what 'from above' means. Usually this should be the same as the natural up direction of the graph. Does not need to be normalized.</param> + public static DistanceMetric ClosestAsSeenFromAboveSoft (Vector3 up) => new DistanceMetric { projectionAxis = up, distanceScaleAlongProjectionDirection = 0.2f }; + + /// <summary> + /// Returns a DistanceMetric which will find the closest node as seen from above. + /// + /// The "upwards" direction will be set to the graph's natural up direction. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="projectionAxis"/> + /// + /// Note: This is only supported by some graph types. The navmesh/recast/grid graph fully support this. + /// Point graphs do not support this field at all, and will completely ignore it. + /// + /// See: <see cref="ClosestAsSeenFromAbove(Vector3)"/> + /// </summary> + public static DistanceMetric ClosestAsSeenFromAbove () => new DistanceMetric { projectionAxis = Vector3.positiveInfinity, distanceScaleAlongProjectionDirection = 0.0f }; + + /// <summary> + /// Returns a DistanceMetric which will find the closest node as seen from above. + /// + /// [Open online documentation to see images] + /// + /// See: <see cref="projectionAxis"/> + /// + /// Note: This is only supported by some graph types. The navmesh/recast graphs fully support this. + /// The grid graphs, however, only support this if the direction is parallel to the natural 'up' direction of the graph. + /// The grid graphs will search as if the direction is in the graph's up direction if this value is anything other than Vector3.zero. + /// Point graphs do not support this field at all, and will completely ignore it. + /// + /// Note: On recast/navmesh graphs there is a performance penalty when using this feature with a direction which is not parallel to the natural up direction of the graph. + /// Typically you should only set it to such values if you have a spherical or non-planar world (see spherical) (view in online documentation for working links). + /// + /// Note: If you want to use the graph's natural up direction, use the overload which does not take any parameters. + /// </summary> + /// <param name="up">Defines what 'from above' means. Usually this should be the same as the natural up direction of the graph. Does not need to be normalized.</param> + public static DistanceMetric ClosestAsSeenFromAbove (Vector3 up) => new DistanceMetric { projectionAxis = up, distanceScaleAlongProjectionDirection = 0.0f }; + } + + /// <summary>Nearest node constraint. Constrains which nodes will be returned by the <see cref="AstarPath.GetNearest"/> function</summary> + public class NNConstraint { + /// <summary> + /// Graphs treated as valid to search on. + /// This is a bitmask meaning that bit 0 specifies whether or not the first graph in the graphs list should be able to be included in the search, + /// bit 1 specifies whether or not the second graph should be included and so on. + /// <code> + /// // Enables the first and third graphs to be included, but not the rest + /// myNNConstraint.graphMask = (1 << 0) | (1 << 2); + /// </code> + /// <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> + /// + /// Note: This does only affect which nodes are returned from a <see cref="AstarPath.GetNearest"/> call, if a valid graph is connected to an invalid graph using a node link then it might be searched anyway. + /// + /// See: <see cref="AstarPath.GetNearest"/> + /// See: <see cref="SuitableGraph"/> + /// See: bitmasks (view in online documentation for working links) + /// </summary> + public GraphMask graphMask = -1; + + /// <summary>Only treat nodes in the area <see cref="area"/> as suitable. Does not affect anything if <see cref="area"/> is less than 0 (zero)</summary> + public bool constrainArea; + + /// <summary>Area ID to constrain to. Will not affect anything if less than 0 (zero) or if <see cref="constrainArea"/> is false</summary> + public int area = -1; + + /// <summary> + /// Determines how to measure distances to the navmesh. + /// + /// The default is a euclidean distance, which works well for most things. + /// + /// See: <see cref="DistanceMetric"/> + /// </summary> + public DistanceMetric distanceMetric; + + /// <summary>Constrain the search to only walkable or unwalkable nodes depending on <see cref="walkable"/>.</summary> + public bool constrainWalkability = true; + + /// <summary> + /// Only search for walkable or unwalkable nodes if <see cref="constrainWalkability"/> is enabled. + /// If true, only walkable nodes will be searched for, otherwise only unwalkable nodes will be searched for. + /// Does not affect anything if <see cref="constrainWalkability"/> if false. + /// </summary> + public bool walkable = true; + + /// <summary> + /// if available, do an XZ check instead of checking on all axes. + /// The navmesh/recast graph as well as the grid/layered grid graph supports this. + /// + /// This can be important on sloped surfaces. See the image below in which the closest point for each blue point is queried for: + /// [Open online documentation to see images] + /// + /// Deprecated: Use <see cref="distanceMetric"/> = DistanceMetric.ClosestAsSeenFromAbove() instead + /// </summary> + [System.Obsolete("Use distanceMetric = DistanceMetric.ClosestAsSeenFromAbove() instead")] + public bool distanceXZ { + get { + return distanceMetric.isProjectedDistance && distanceMetric.distanceScaleAlongProjectionDirection == 0; + } + set { + if (value) { + distanceMetric = DistanceMetric.ClosestAsSeenFromAbove(); + } else { + distanceMetric = DistanceMetric.Euclidean; + } + } + } + + /// <summary> + /// Sets if tags should be constrained. + /// See: <see cref="tags"/> + /// </summary> + public bool constrainTags = true; + + /// <summary> + /// Nodes which have any of these tags set are suitable. + /// This is a bitmask, i.e bit 0 indicates that tag 0 is good, bit 3 indicates tag 3 is good etc. + /// See: <see cref="constrainTags"/> + /// See: <see cref="graphMask"/> + /// See: bitmasks (view in online documentation for working links) + /// </summary> + public int tags = -1; + + /// <summary> + /// Constrain distance to node. + /// Uses distance from <see cref="AstarPath.maxNearestNodeDistance"/>. + /// If this is false, it will completely ignore the distance limit. + /// + /// If there are no suitable nodes within the distance limit then the search will terminate with a null node as a result. + /// Note: This value is not used in this class, it is used by the AstarPath.GetNearest function. + /// </summary> + public bool constrainDistance = true; + + /// <summary> + /// Returns whether or not the graph conforms to this NNConstraint's rules. + /// Note that only the first 31 graphs are considered using this function. + /// If the <see cref="graphMask"/> has bit 31 set (i.e the last graph possible to fit in the mask), all graphs + /// above index 31 will also be considered suitable. + /// </summary> + public virtual bool SuitableGraph (int graphIndex, NavGraph graph) { + return graphMask.Contains(graphIndex); + } + + /// <summary>Returns whether or not the node conforms to this NNConstraint's rules</summary> + public virtual bool Suitable (GraphNode node) { + if (constrainWalkability && node.Walkable != walkable) return false; + + if (constrainArea && area >= 0 && node.Area != area) return false; + + if (constrainTags && ((tags >> (int)node.Tag) & 0x1) == 0) return false; + + return true; + } + + /// <summary> + /// The default NNConstraint. + /// Equivalent to new NNConstraint (). + /// This NNConstraint has settings which works for most, it only finds walkable nodes + /// and it constrains distance set by A* Inspector -> Settings -> Max Nearest Node Distance + /// + /// Deprecated: Use <see cref="NNConstraint.Walkable"/> instead. It is equivalent, but the name is more descriptive. + /// </summary> + [System.Obsolete("Use NNConstraint.Walkable instead. It is equivalent, but the name is more descriptive")] + public static NNConstraint Default { + get { + return new NNConstraint(); + } + } + + /// <summary> + /// An NNConstraint which filters out unwalkable nodes. + /// This is the most commonly used NNConstraint. + /// + /// It also constrains the nearest node to be within the distance set by A* Inspector -> Settings -> Max Nearest Node Distance + /// </summary> + public static NNConstraint Walkable { + get { + return new NNConstraint(); + } + } + + /// <summary>Returns a constraint which does not filter the results</summary> + public static NNConstraint None { + get { + return new NNConstraint { + constrainWalkability = false, + constrainArea = false, + constrainTags = false, + constrainDistance = false, + graphMask = -1, + }; + } + } + + /// <summary>Default constructor. Equals to the property <see cref="Default"/></summary> + public NNConstraint () { + } + } + + /// <summary> + /// A special NNConstraint which can use different logic for the start node and end node in a path. + /// A PathNNConstraint can be assigned to the Path.nnConstraint field, the path will first search for the start node, then it will call <see cref="SetStart"/> and proceed with searching for the end node (nodes in the case of a MultiTargetPath). + /// The default PathNNConstraint will constrain the end point to lie inside the same area as the start point. + /// </summary> + public class PathNNConstraint : NNConstraint { + public static new PathNNConstraint Walkable { + get { + return new PathNNConstraint { + constrainArea = true + }; + } + } + + /// <summary>Called after the start node has been found. This is used to get different search logic for the start and end nodes in a path</summary> + public virtual void SetStart (GraphNode node) { + if (node != null) { + area = (int)node.Area; + } else { + constrainArea = false; + } + } + } + + /// <summary> + /// Result of a nearest node query. + /// + /// See: <see cref="AstarPath.GetNearest"/> + /// </summary> + public readonly struct NNInfo { + /// <summary> + /// Closest node. + /// May be null if there was no node which satisfied all constraints of the search. + /// </summary> + public readonly GraphNode node; + + /// <summary> + /// Closest point on the navmesh. + /// This is the query position clamped to the closest point on the <see cref="node"/>. + /// + /// If node is null, then this value is (+inf, +inf, +inf). + /// </summary> + public readonly Vector3 position; + + /// <summary> + /// Cost for picking this node as the closest node. + /// This is typically the squared distance from the query point to <see cref="position"/>. + /// + /// However, it may be different if the <see cref="NNConstraint"/> used a different cost function. + /// For example, if <see cref="NNConstraint.distanceMetric"/> is <see cref="DistanceMetric.ClosestAsSeenFromAbove()"/>, + /// then this value will be the squared distance in the XZ plane. + /// + /// This value is not guaranteed to be smaller or equal to the squared euclidean distance from the query point to <see cref="position"/>. + /// In particular for a navmesh/recast graph with a <see cref="DistanceMetric.ClosestAsSeenFromAboveSoft"/> NNConstraint it may + /// be slightly greater for some configurations. This is fine because we are only using this value for the rough + /// distance limit performanced by <see cref="AstarPath.maxNearestNodeDistance"/>, and it's not a problem if it is slightly inaccurate. + /// + /// See: <see cref="NNConstraint.distanceMetric"/> + /// + /// If <see cref="node"/> is null, then this value is positive infinity. + /// </summary> + public readonly float distanceCostSqr; + + /// <summary> + /// Closest point on the navmesh. + /// Deprecated: This field has been renamed to <see cref="position"/> + /// </summary> + [System.Obsolete("This field has been renamed to 'position'")] + public Vector3 clampedPosition { + get { + return position; + } + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public NNInfo (GraphNode node, Vector3 position, float distanceCostSqr) { + this.node = node; + if (node == null) { + // Guarantee that a null node always gives specific values for position and distanceCostSqr + this.position = Vector3.positiveInfinity; + this.distanceCostSqr = float.PositiveInfinity; + } else { + this.position = position; + this.distanceCostSqr = distanceCostSqr; + } + } + + public static readonly NNInfo Empty = new NNInfo(null, Vector3.positiveInfinity, float.PositiveInfinity); + + public static explicit operator Vector3(NNInfo ob) { + return ob.position; + } + + public static explicit operator GraphNode(NNInfo ob) { + return ob.node; + } + } + + /// <summary>Info about where in the scanning process a graph is</summary> + public enum ScanningStage { + PreProcessingGraphs, + PreProcessingGraph, + ScanningGraph, + PostProcessingGraph, + FinishingScans, + } + + /// <summary> + /// Progress info for e.g a progressbar. + /// Used by the scan functions in the project + /// See: <see cref="AstarPath.ScanAsync"/> + /// </summary> + public readonly struct Progress { + /// <summary>Current progress as a value between 0 and 1</summary> + public readonly float progress; + internal readonly ScanningStage stage; + internal readonly int graphIndex; + internal readonly int graphCount; + + public Progress (float progress, ScanningStage stage, int graphIndex = 0, int graphCount = 0) { + this.progress = progress; + this.stage = stage; + this.graphIndex = graphIndex; + this.graphCount = graphCount; + } + + public Progress MapTo (float min, float max) { + return new Progress(Mathf.Lerp(min, max, progress), stage, graphIndex, graphCount); + } + + public override string ToString () { + var s = progress.ToString("0%") + " "; + switch (stage) { + case ScanningStage.PreProcessingGraphs: + s += "Pre-processing graphs"; + break; + case ScanningStage.PreProcessingGraph: + s += "Pre-processing graph " + (graphIndex+1) + " of " + graphCount; + break; + case ScanningStage.ScanningGraph: + s += "Scanning graph " + (graphIndex+1) + " of " + graphCount; + break; + case ScanningStage.PostProcessingGraph: + s += "Post-processing graph " + (graphIndex+1) + " of " + graphCount; + break; + case ScanningStage.FinishingScans: + s += "Finalizing graph scans"; + break; + } + return s; + } + } + + /// <summary>Graphs which can be updated during runtime</summary> + public interface IUpdatableGraph { + /// <summary> + /// Schedules a number of graph updates. + /// + /// This method should return a promise. It should not execute the graph updates immediately. + /// The Apply method on the promise will be called when the graph updates should be applied. + /// In the meantime, the graph may perform calculations using e.g. the unity job system. + /// + /// Notes to implementators. + /// This function should (in order): + /// -# Call o.WillUpdateNode on the GUO for every node it will update, it is important that this is called BEFORE any changes are made to the nodes. + /// -# Update walkabilty using special settings such as the usePhysics flag used with the GridGraph. + /// -# Call Apply on the GUO for every node which should be updated with the GUO. + /// -# Update connectivity info if appropriate (GridGraphs updates connectivity, but most other graphs don't since then the connectivity cannot be recovered later). + /// + /// It is guaranteed that the Apply function will be called on the returned promise before this function is called again. + /// + /// Null may be returned if no updates need to be done to the graph. + /// </summary> + IGraphUpdatePromise ScheduleGraphUpdates(List<GraphUpdateObject> graphUpdates); + } + + /// <summary>Info about if a graph update has been applied or not</summary> + public enum GraphUpdateStage { + /// <summary> + /// The graph update object has been created, but not used for anything yet. + /// This is the default value. + /// </summary> + Created, + /// <summary>The graph update has been sent to the pathfinding system and is scheduled to be applied to the graphs</summary> + Pending, + /// <summary>The graph update has been applied to all graphs</summary> + Applied, + /// <summary> + /// The graph update has been aborted and will not be applied. + /// This can happen if the AstarPath component is destroyed while a graph update is queued to be applied. + /// </summary> + Aborted, + } + + /// <summary> + /// Represents a collection of settings used to update nodes in a specific region of a graph. + /// See: AstarPath.UpdateGraphs + /// See: graph-updates (view in online documentation for working links) + /// </summary> + public class GraphUpdateObject { + /// <summary> + /// The bounds to update nodes within. + /// Defined in world space. + /// </summary> + public Bounds bounds; + + /// <summary> + /// Use physics checks to update nodes. + /// When updating a grid graph and this is true, the nodes' position and walkability will be updated using physics checks + /// with settings from "Collision Testing" and "Height Testing". + /// + /// When updating a PointGraph, setting this to true will make it re-evaluate all connections in the graph which passes through the <see cref="bounds"/>. + /// + /// This has no effect when updating GridGraphs if <see cref="modifyWalkability"/> is turned on. + /// You should not combine <see cref="updatePhysics"/> and <see cref="modifyWalkability"/>. + /// + /// On RecastGraphs, having this enabled will trigger a complete recalculation of all tiles intersecting the bounds. + /// This is quite slow (but powerful). If you only want to update e.g penalty on existing nodes, leave it disabled. + /// </summary> + public bool updatePhysics = true; + + /// <summary> + /// Reset penalties to their initial values when updating grid graphs and <see cref="updatePhysics"/> is true. + /// If you want to keep old penalties even when you update the graph you may want to disable this option. + /// + /// The images below shows two overlapping graph update objects, the right one happened to be applied before the left one. They both have updatePhysics = true and are + /// set to increase the penalty of the nodes by some amount. + /// + /// The first image shows the result when resetPenaltyOnPhysics is false. Both penalties are added correctly. + /// [Open online documentation to see images] + /// + /// This second image shows when resetPenaltyOnPhysics is set to true. The first GUO is applied correctly, but then the second one (the left one) is applied + /// and during its updating, it resets the penalties first and then adds penalty to the nodes. The result is that the penalties from both GUOs are not added together. + /// The green patch in at the border is there because physics recalculation (recalculation of the position of the node, checking for obstacles etc.) affects a slightly larger + /// area than the original GUO bounds because of the Grid Graph -> Collision Testing -> Diameter setting (it is enlarged by that value). So some extra nodes have their penalties reset. + /// + /// [Open online documentation to see images] + /// + /// Bug: Not working with burst + /// </summary> + public bool resetPenaltyOnPhysics = true; + + /// <summary> + /// Update Erosion for GridGraphs. + /// When enabled, erosion will be recalculated for grid graphs + /// after the GUO has been applied. + /// + /// In the below image you can see the different effects you can get with the different values. + /// The first image shows the graph when no GUO has been applied. The blue box is not identified as an obstacle by the graph, the reason + /// there are unwalkable nodes around it is because there is a height difference (nodes are placed on top of the box) so erosion will be applied (an erosion value of 2 is used in this graph). + /// The orange box is identified as an obstacle, so the area of unwalkable nodes around it is a bit larger since both erosion and collision has made + /// nodes unwalkable. + /// The GUO used simply sets walkability to true, i.e making all nodes walkable. + /// + /// [Open online documentation to see images] + /// + /// When updateErosion=True, the reason the blue box still has unwalkable nodes around it is because there is still a height difference + /// so erosion will still be applied. The orange box on the other hand has no height difference and all nodes are set to walkable. + /// + /// When updateErosion=False, all nodes walkability are simply set to be walkable in this example. + /// + /// See: Pathfinding.GridGraph + /// + /// Bug: Not working with burst + /// </summary> + public bool updateErosion = true; + + /// <summary> + /// NNConstraint to use. + /// The Pathfinding.NNConstraint.SuitableGraph function will be called on the NNConstraint to enable filtering of which graphs to update. + /// Note: As the Pathfinding.NNConstraint.SuitableGraph function is A* Pathfinding Project Pro only, this variable doesn't really affect anything in the free version. + /// </summary> + public NNConstraint nnConstraint = NNConstraint.None; + + /// <summary> + /// Penalty to add to the nodes. + /// A penalty of 1000 is equivalent to the cost of moving 1 world unit. + /// </summary> + public int addPenalty; + + /// <summary> + /// If true, all nodes' walkable variable will be set to <see cref="setWalkability"/>. + /// It is not recommended to combine this with <see cref="updatePhysics"/> since then you will just overwrite + /// what <see cref="updatePhysics"/> calculated. + /// </summary> + public bool modifyWalkability; + + /// <summary>If <see cref="modifyWalkability"/> is true, the nodes' walkable variable will be set to this value</summary> + public bool setWalkability; + + /// <summary>If true, all nodes' tag will be set to <see cref="setTag"/></summary> + public bool modifyTag; + + /// <summary>If <see cref="modifyTag"/> is true, all nodes' tag will be set to this value</summary> + public PathfindingTag setTag; + + /// <summary> + /// Track which nodes are changed and save backup data. + /// Used internally to revert changes if needed. + /// + /// Deprecated: This field does not do anything anymore. Use <see cref="AstarPath.Snapshot"/> instead. + /// </summary> + [System.Obsolete("This field does not do anything anymore. Use AstarPath.Snapshot instead.")] + public bool trackChangedNodes; + + /// <summary> + /// A shape can be specified if a bounds object does not give enough precision. + /// Note that if you set this, you should set the bounds so that it encloses the shape + /// because the bounds will be used as an initial fast check for which nodes that should + /// be updated. + /// </summary> + public GraphUpdateShape shape; + + /// <summary> + /// Info about if a graph update has been applied or not. + /// Either an enum (see STAGE_CREATED and associated constants) + /// or a non-negative count of the number of graphs that are waiting to apply this graph update. + /// </summary> + internal int internalStage = STAGE_CREATED; + + internal const int STAGE_CREATED = -1; + internal const int STAGE_PENDING = -2; + internal const int STAGE_ABORTED = -3; + internal const int STAGE_APPLIED = 0; + + /// <summary>Info about if a graph update has been applied or not</summary> + public GraphUpdateStage stage { + get { + switch (internalStage) { + case STAGE_CREATED: + return GraphUpdateStage.Created; + case STAGE_APPLIED: + return GraphUpdateStage.Applied; + case STAGE_ABORTED: + return GraphUpdateStage.Aborted; + // Positive numbers means it is currently being applied, so it is also pending. + default: + case STAGE_PENDING: + return GraphUpdateStage.Pending; + } + } + } + + /// <summary>Should be called on every node which is updated with this GUO before it is updated.</summary> + /// <param name="node">The node to save fields for. If null, nothing will be done</param> + public virtual void WillUpdateNode (GraphNode node) { + } + + /// <summary> + /// Reverts penalties and flags (which includes walkability) on every node which was updated using this GUO. + /// Data for reversion is only saved if <see cref="trackChangedNodes"/> is true. + /// + /// See: blocking (view in online documentation for working links) + /// See: <see cref="GraphUpdateUtilities.UpdateGraphsNoBlock"/> + /// + /// Deprecated: Use <see cref="AstarPath.Snapshot"/> instead + /// </summary> + [System.Obsolete("Use AstarPath.Snapshot instead", true)] + public virtual void RevertFromBackup () {} + + /// <summary> + /// Updates the specified node using this GUO's settings. + /// + /// Note: Some graphs may call <see cref="ApplyJob"/> instead, for better performance. + /// </summary> + public virtual void Apply (GraphNode node) { + if (shape == null || shape.Contains(node)) { + // Update penalty and walkability + node.Penalty = (uint)(node.Penalty+addPenalty); + if (modifyWalkability) { + node.Walkable = setWalkability; + } + + // Update tags + if (modifyTag) node.Tag = (uint)setTag; + } + } + + /// <summary>Provides burst-readable data to a graph update job</summary> + public struct GraphUpdateData { + public NativeArray<Vector3> nodePositions; + public NativeArray<uint> nodePenalties; + public NativeArray<bool> nodeWalkable; + public NativeArray<int> nodeTags; + /// <summary> + /// Node indices to update. + /// Remaining nodes should be left alone. + /// </summary> + public NativeArray<int> nodeIndices; + }; + + /// <summary>Job for applying a graph update object</summary> + [BurstCompile] + public struct JobGraphUpdate : IJob { + public GraphUpdateShape.BurstShape shape; + public GraphUpdateData data; + + public Bounds bounds; + public int penaltyDelta; + public bool modifyWalkability; + public bool walkabilityValue; + public bool modifyTag; + public int tagValue; + + public void Execute () { + for (int i = 0; i < data.nodeIndices.Length; i++) { + var node = data.nodeIndices[i]; + if (bounds.Contains(data.nodePositions[node]) && shape.Contains(data.nodePositions[node])) { + data.nodePenalties[node] += (uint)penaltyDelta; + if (modifyWalkability) data.nodeWalkable[node] = walkabilityValue; + if (modifyTag) data.nodeTags[node] = tagValue; + } + } + } + }; + + /// <summary> + /// Update a set of nodes using this GUO's settings. + /// This is far more efficient since it can utilize the Burst compiler. + /// + /// This method may be called by graph generators instead of the <see cref="Apply"/> method to update the graph more efficiently. + /// </summary> + public virtual void ApplyJob (GraphUpdateData data, JobDependencyTracker dependencyTracker) { + if (addPenalty == 0 && !modifyWalkability && !modifyTag) return; + + new JobGraphUpdate { + shape = shape != null ? new GraphUpdateShape.BurstShape(shape, Allocator.Persistent) : GraphUpdateShape.BurstShape.Everything, + data = data, + bounds = bounds, + penaltyDelta = addPenalty, + modifyWalkability = modifyWalkability, + walkabilityValue = setWalkability, + modifyTag = modifyTag, + tagValue = (int)setTag.value, + }.Schedule(dependencyTracker); + } + + public GraphUpdateObject () { + } + + /// <summary>Creates a new GUO with the specified bounds</summary> + public GraphUpdateObject (Bounds b) { + bounds = b; + } + } + + /// <summary>Graph which has a well defined transformation from graph space to world space</summary> + public interface ITransformedGraph { + GraphTransform transform { get; } + } + + /// <summary>Graph which supports the Linecast method</summary> + public interface IRaycastableGraph { + /// <summary> + /// Checks if the straight line of sight between the two points on the graph is obstructed. + /// + /// Returns: True if an obstacle was hit, and false otherwise. + /// </summary> + /// <param name="start">The start point of the raycast.</param> + /// <param name="end">The end point of the raycast.</param> + bool Linecast(Vector3 start, Vector3 end); + /// <summary>Deprecated:</summary> + [System.Obsolete] + bool Linecast(Vector3 start, Vector3 end, GraphNode startNodeHint); + /// <summary>Deprecated:</summary> + [System.Obsolete] + bool Linecast(Vector3 start, Vector3 end, GraphNode startNodeHint, out GraphHitInfo hit); + /// <summary> + /// Checks if the straight line of sight between the two points on the graph is obstructed. + /// + /// Returns: True if an obstacle was hit, and false otherwise. + /// </summary> + /// <param name="start">The start point of the raycast.</param> + /// <param name="end">The end point of the raycast.</param> + /// <param name="startNodeHint">If you know which node contains the start point, you may pass it here to save a GetNearest call. Otherwise, pass null. If the start point is not actually inside the give node, you may get different behavior on different graph types. Some will clamp the start point to the surface of the hint node, and some will ignore the hint parameter completely.</param> + /// <param name="hit">Additional information about what was hit.</param> + /// <param name="trace">If you supply a list, it will be filled with all nodes that the linecast traversed. You may pass null if you don't care about this.</param> + /// <param name="filter">You may supply a callback to indicate which nodes should be considered unwalkable. Note that already unwalkable nodes cannot be made walkable in this way.</param> + bool Linecast(Vector3 start, Vector3 end, GraphNode startNodeHint, out GraphHitInfo hit, List<GraphNode> trace, System.Func<GraphNode, bool> filter = null); + /// <summary> + /// Checks if the straight line of sight between the two points on the graph is obstructed. + /// + /// Returns: True if an obstacle was hit, and false otherwise. + /// </summary> + /// <param name="start">The start point of the raycast.</param> + /// <param name="end">The end point of the raycast.</param> + /// <param name="hit">Additional information about what was hit.</param> + /// <param name="trace">If you supply a list, it will be filled with all nodes that the linecast traversed. You may pass null if you don't care about this.</param> + /// <param name="filter">You may supply a callback to indicate which nodes should be considered unwalkable. Note that already unwalkable nodes cannot be made walkable in this way.</param> + bool Linecast(Vector3 start, Vector3 end, out GraphHitInfo hit, List<GraphNode> trace = null, System.Func<GraphNode, bool> filter = null); + } + + /// <summary> + /// Integer Rectangle. + /// Uses an inclusive coordinate range. + /// + /// Works almost like UnityEngine.Rect but with integer coordinates + /// </summary> + [System.Serializable] + public struct IntRect { + public int xmin, ymin, xmax, ymax; + + public IntRect (int xmin, int ymin, int xmax, int ymax) { + this.xmin = xmin; + this.xmax = xmax; + this.ymin = ymin; + this.ymax = ymax; + } + + public bool Contains (int x, int y) { + return !(x < xmin || y < ymin || x > xmax || y > ymax); + } + + public bool Contains (IntRect other) { + return xmin <= other.xmin && xmax >= other.xmax && ymin <= other.ymin && ymax >= other.ymax; + } + + public Int2 Min { + get { + return new Int2(xmin, ymin); + } + } + + public Int2 Max { + get { + return new Int2(xmax, ymax); + } + } + + public int Width { + get { + return xmax-xmin+1; + } + } + + public int Height { + get { + return ymax-ymin+1; + } + } + + public int Area { + get { + return Width * Height; + } + } + + /// <summary> + /// Returns if this rectangle is valid. + /// An invalid rect could have e.g xmin > xmax. + /// Rectangles are valid iff they contain at least one point. + /// </summary> + // TODO: Make into property + public bool IsValid () { + return xmin <= xmax && ymin <= ymax; + } + + public static bool operator == (IntRect a, IntRect b) { + return a.xmin == b.xmin && a.xmax == b.xmax && a.ymin == b.ymin && a.ymax == b.ymax; + } + + public static bool operator != (IntRect a, IntRect b) { + return a.xmin != b.xmin || a.xmax != b.xmax || a.ymin != b.ymin || a.ymax != b.ymax; + } + + public static explicit operator Rect(IntRect r) => new Rect(r.xmin, r.ymin, r.Width, r.Height); + + public override bool Equals (System.Object obj) { + if (!(obj is IntRect)) return false; + var rect = (IntRect)obj; + + return xmin == rect.xmin && xmax == rect.xmax && ymin == rect.ymin && ymax == rect.ymax; + } + + public override int GetHashCode () { + return xmin*131071 ^ xmax*3571 ^ ymin*3109 ^ ymax*7; + } + + /// <summary> + /// Returns the intersection rect between the two rects. + /// The intersection rect is the area which is inside both rects. + /// If the rects do not have an intersection, an invalid rect is returned. + /// See: IsValid + /// </summary> + public static IntRect Intersection (IntRect a, IntRect b) { + return new IntRect( + System.Math.Max(a.xmin, b.xmin), + System.Math.Max(a.ymin, b.ymin), + System.Math.Min(a.xmax, b.xmax), + System.Math.Min(a.ymax, b.ymax) + ); + } + + /// <summary>Returns if the two rectangles intersect each other</summary> + public static bool Intersects (IntRect a, IntRect b) { + return !(a.xmin > b.xmax || a.ymin > b.ymax || a.xmax < b.xmin || a.ymax < b.ymin); + } + + /// <summary> + /// Returns a new rect which contains both input rects. + /// This rectangle may contain areas outside both input rects as well in some cases. + /// </summary> + public static IntRect Union (IntRect a, IntRect b) { + return new IntRect( + System.Math.Min(a.xmin, b.xmin), + System.Math.Min(a.ymin, b.ymin), + System.Math.Max(a.xmax, b.xmax), + System.Math.Max(a.ymax, b.ymax) + ); + } + + /// <summary> + /// Returns a new rect that contains all of a except for the parts covered by b. + /// + /// Throws: An exception if the difference is not a rectangle (e.g. if they only overlap in a corner). + /// + /// <code> + /// ┌───────────┐ + /// │ B │ + /// │ ┌─────┐ │ + /// │ │ │ │ ─► + /// └──┼─────┼──┘ ┌─────┐ + /// │ A │ │ A │ + /// └─────┘ └─────┘ + /// </code> + /// </summary> + public static IntRect Exclude (IntRect a, IntRect b) { + if (!b.IsValid() || !a.IsValid()) return a; + var intersection = Intersection(a, b); + if (!intersection.IsValid()) return a; + if (a.xmin == intersection.xmin && a.xmax == intersection.xmax) { + if (a.ymin == intersection.ymin) { + a.ymin = intersection.ymax + 1; + return a; + } else if (a.ymax == intersection.ymax) { + a.ymax = intersection.ymin - 1; + return a; + } else { + throw new System.ArgumentException("B splits A into two disjoint parts"); + } + } else if (a.ymin == intersection.ymin && a.ymax == intersection.ymax) { + if (a.xmin == intersection.xmin) { + a.xmin = intersection.xmax + 1; + return a; + } else if (a.xmax == intersection.xmax) { + a.xmax = intersection.xmin - 1; + return a; + } else { + throw new System.ArgumentException("B splits A into two disjoint parts"); + } + } else { + throw new System.ArgumentException("B covers either a corner of A, or does not touch the edges of A at all"); + } + } + + /// <summary>Returns a new IntRect which is expanded to contain the point</summary> + public IntRect ExpandToContain (int x, int y) { + return new IntRect( + System.Math.Min(xmin, x), + System.Math.Min(ymin, y), + System.Math.Max(xmax, x), + System.Math.Max(ymax, y) + ); + } + + /// <summary>Returns a new IntRect which has been moved by an offset</summary> + public IntRect Offset (Int2 offset) { + return new IntRect(xmin + offset.x, ymin + offset.y, xmax + offset.x, ymax + offset.y); + } + + /// <summary>Returns a new rect which is expanded by range in all directions.</summary> + /// <param name="range">How far to expand. Negative values are permitted.</param> + public IntRect Expand (int range) { + return new IntRect(xmin-range, + ymin-range, + xmax+range, + ymax+range + ); + } + + public override string ToString () { + return "[x: "+xmin+"..."+xmax+", y: " + ymin +"..."+ymax+"]"; + } + } + + /// <summary> + /// Holds a bitmask of graphs. + /// This bitmask can hold up to 32 graphs. + /// + /// The bitmask can be converted to and from integers implicitly. + /// + /// <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: bitmasks (view in online documentation for working links) + /// </summary> + [System.Serializable] + public struct GraphMask { + /// <summary>Bitmask representing the mask</summary> + public int value; + + /// <summary>A mask containing every graph</summary> + public static GraphMask everything => new GraphMask(-1); + + public GraphMask (int value) { + this.value = value; + } + + public static implicit operator int(GraphMask mask) { + return mask.value; + } + + public static implicit operator GraphMask (int mask) { + return new GraphMask(mask); + } + + /// <summary>Combines two masks to form the intersection between them</summary> + public static GraphMask operator & (GraphMask lhs, GraphMask rhs) { + return new GraphMask(lhs.value & rhs.value); + } + + /// <summary>Combines two masks to form the union of them</summary> + public static GraphMask operator | (GraphMask lhs, GraphMask rhs) { + return new GraphMask(lhs.value | rhs.value); + } + + /// <summary>Inverts the mask</summary> + public static GraphMask operator ~ (GraphMask lhs) { + return new GraphMask(~lhs.value); + } + + /// <summary>True if this mask contains the graph with the given graph index</summary> + public bool Contains (int graphIndex) { + return ((value >> graphIndex) & 1) != 0; + } + + /// <summary>A bitmask containing the given graph</summary> + public static GraphMask FromGraph (NavGraph graph) { + return 1 << (int)graph.graphIndex; + } + + public override string ToString () { + return value.ToString(); + } + + /// <summary>A bitmask containing the given graph index.</summary> + public static GraphMask FromGraphIndex(uint graphIndex) => new GraphMask(1 << (int)graphIndex); + + /// <summary> + /// A bitmask containing the first graph with the given name. + /// <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> + /// </summary> + public static GraphMask FromGraphName (string graphName) { + var graph = AstarPath.active.data.FindGraph(g => g.name == graphName); + + if (graph == null) throw new System.ArgumentException("Could not find any graph with the name '" + graphName + "'"); + return FromGraph(graph); + } + } + + #region Delegates + + /// <summary> + /// Delegate with on Path object as parameter. + /// This is used for callbacks for when a path has been calculated. + /// </summary> + public delegate void OnPathDelegate(Path p); + + public delegate void OnGraphDelegate(NavGraph graph); + + public delegate void OnScanDelegate(AstarPath script); + + #endregion + + #region Enums + + [System.Flags] + public enum GraphUpdateThreading { + /// <summary> + /// Call UpdateArea in the unity thread. + /// This is the default value. + /// </summary> + UnityThread = 0, + /// <summary>Calls UpdateAreaInit in the Unity thread before everything else</summary> + UnityInit = 1 << 1, + /// <summary>Calls UpdateAreaPost in the Unity thread after everything else</summary> + UnityPost = 1 << 2 + } + + /// <summary>How path results are logged by the system</summary> + public enum PathLog { + /// <summary>Does not log anything. This is recommended for release since logging path results has a performance overhead.</summary> + None, + /// <summary>Logs basic info about the paths</summary> + Normal, + /// <summary>Includes additional info</summary> + Heavy, + /// <summary>Same as heavy, but displays the info in-game using GUI</summary> + InGame, + /// <summary>Same as normal, but logs only paths which returned an error</summary> + OnlyErrors + } + + /// <summary> + /// How to estimate the cost of moving to the destination during pathfinding. + /// + /// The heuristic is the estimated cost from the current node to the target. + /// The different heuristics have roughly the same performance except not using any heuristic at all (<see cref="Heuristic.None"/>) + /// which is usually significantly slower. + /// + /// In the image below you can see a comparison of the different heuristic options for an 8-connected grid and + /// for a 4-connected grid. + /// Note that all paths within the green area will all have the same length. The only difference between the heuristics + /// is which of those paths of the same length that will be chosen. + /// Note that while the Diagonal Manhattan and Manhattan options seem to behave very differently on an 8-connected grid + /// they only do it in this case because of very small rounding errors. Usually they behave almost identically on 8-connected grids. + /// + /// [Open online documentation to see images] + /// + /// Generally for a 4-connected grid graph the Manhattan option should be used as it is the true distance on a 4-connected grid. + /// For an 8-connected grid graph the Diagonal Manhattan option is the mathematically most correct option, however the Euclidean option + /// is often preferred, especially if you are simplifying the path afterwards using modifiers. + /// + /// For any graph that is not grid based the Euclidean option is the best one to use. + /// + /// See: <a href="https://en.wikipedia.org/wiki/A*_search_algorithm">Wikipedia: A* search_algorithm</a> + /// </summary> + public enum Heuristic { + /// <summary>Manhattan distance. See: https://en.wikipedia.org/wiki/Taxicab_geometry</summary> + Manhattan, + /// <summary> + /// Manhattan distance, but allowing diagonal movement as well. + /// Note: This option is currently hard coded for the XZ plane. It will be equivalent to Manhattan distance if you try to use it in the XY plane (i.e for a 2D game). + /// </summary> + DiagonalManhattan, + /// <summary>Ordinary distance. See: https://en.wikipedia.org/wiki/Euclidean_distance</summary> + Euclidean, + /// <summary> + /// Use no heuristic at all. + /// This reduces the pathfinding algorithm to Dijkstra's algorithm. + /// This is usually significantly slower compared to using a heuristic, which is why the A* algorithm is usually preferred over Dijkstra's algorithm. + /// You may have to use this if you have a very non-standard graph. For example a world with a <a href="https://en.wikipedia.org/wiki/Wraparound_(video_games)">wraparound playfield</a> (think Civilization or Asteroids) and you have custom links + /// with a zero cost from one end of the map to the other end. Usually the A* algorithm wouldn't find the wraparound links because it wouldn't think to look in that direction. + /// See: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm + /// </summary> + None + } + + /// <summary>How to visualize the graphs in the editor</summary> + public enum GraphDebugMode { + /// <summary>Draw the graphs with a single solid color</summary> + SolidColor, + /// <summary> + /// Use the G score of the last calculated paths to color the graph. + /// The G score is the cost from the start node to the given node. + /// See: https://en.wikipedia.org/wiki/A*_search_algorithm + /// </summary> + G, + /// <summary> + /// Use the H score (heuristic) of the last calculated paths to color the graph. + /// The H score is the estimated cost from the current node to the target. + /// See: https://en.wikipedia.org/wiki/A*_search_algorithm + /// </summary> + H, + /// <summary> + /// Use the F score of the last calculated paths to color the graph. + /// The F score is the G score + the H score, or in other words the estimated cost total cost of the path. + /// See: https://en.wikipedia.org/wiki/A*_search_algorithm + /// </summary> + F, + /// <summary> + /// Use the penalty of each node to color the graph. + /// This does not show penalties added by tags. + /// See: graph-updates (view in online documentation for working links) + /// See: <see cref="Pathfinding.GraphNode.Penalty"/> + /// </summary> + Penalty, + /// <summary> + /// Visualize the connected components of the graph. + /// A node with a given color can reach any other node with the same color. + /// + /// See: <see cref="Pathfinding.HierarchicalGraph"/> + /// See: https://en.wikipedia.org/wiki/Connected_component_(graph_theory) + /// </summary> + Areas, + /// <summary> + /// Use the tag of each node to color the graph. + /// See: tags (view in online documentation for working links) + /// See: <see cref="Pathfinding.GraphNode.Tag"/> + /// </summary> + Tags, + /// <summary> + /// Visualize the hierarchical graph structure of the graph. + /// This is mostly for internal use. + /// See: <see cref="Pathfinding.HierarchicalGraph"/> + /// </summary> + HierarchicalNode, + /// <summary> + /// Visualize the obstacles generated from the navmesh border. + /// + /// These obstacles are used for local avoidance, as well as for the <see cref="FollowerEntity"/> in its internal navigation. + /// + /// The graph will be colored the same as for <see cref="GraphDebugMode.HierarchicalNode"/>. + /// </summary> + NavmeshBorderObstacles, + } + + /// <summary>Number of threads to use</summary> + public enum ThreadCount { + AutomaticLowLoad = -1, + AutomaticHighLoad = -2, + None = 0, + One = 1, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight + } + + /// <summary>Internal state of a path in the pipeline</summary> + public enum PathState { + /// <summary>Path has been created but not yet scheduled</summary> + Created = 0, + /// <summary>Path is waiting to be calculated</summary> + PathQueue = 1, + /// <summary>Path is being calculated</summary> + Processing = 2, + /// <summary>Path is calculated and is waiting to have its callback called</summary> + ReturnQueue = 3, + /// <summary>The path callback is being called right now (only set inside the callback itself)</summary> + Returning = 4, + /// <summary>The path has been calculated and its callback has been called</summary> + Returned = 5, + } + + + /// <summary>State of a path request</summary> + public enum PathCompleteState { + /// <summary> + /// The path has not been calculated yet. + /// See: <see cref="Pathfinding.Path.IsDone()"/> + /// </summary> + NotCalculated = 0, + /// <summary> + /// The path calculation is done, but it failed. + /// See: <see cref="Pathfinding.Path.error"/> + /// </summary> + Error = 1, + /// <summary>The path has been successfully calculated</summary> + Complete = 2, + /// <summary> + /// The path has been calculated, but only a partial path could be found. + /// See: <see cref="Pathfinding.ABPath.calculatePartial"/> + /// </summary> + Partial = 3, + } + + /// <summary>What to do when the character is close to the destination</summary> + public enum CloseToDestinationMode { + /// <summary>The character will stop as quickly as possible when within endReachedDistance (field that exist on most movement scripts) units from the destination</summary> + Stop, + /// <summary>The character will continue to the exact position of the destination</summary> + ContinueToExactDestination, + } + + /// <summary>Indicates the side of a line that a point lies on</summary> + public enum Side : byte { + /// <summary>The point lies exactly on the line</summary> + Colinear = 0, + /// <summary>The point lies on the left side of the line</summary> + Left = 1, + /// <summary>The point lies on the right side of the line</summary> + Right = 2 + } + + public enum InspectorGridHexagonNodeSize { + /// <summary>Value is the distance between two opposing sides in the hexagon</summary> + Width, + /// <summary>Value is the distance between two opposing vertices in the hexagon</summary> + Diameter, + /// <summary>Value is the raw node size of the grid</summary> + NodeSize + } + + public enum InspectorGridMode { + Grid, + IsometricGrid, + Hexagonal, + Advanced + } + + /// <summary> + /// Determines which direction the agent moves in. + /// For 3D games you most likely want the ZAxisIsForward option as that is the convention for 3D games. + /// For 2D games you most likely want the YAxisIsForward option as that is the convention for 2D games. + /// </summary> + public enum OrientationMode : byte { + ZAxisForward, + YAxisForward, + } + + #endregion +} + +namespace Pathfinding.Util { + /// <summary>Prevents code stripping. See: https://docs.unity3d.com/Manual/ManagedCodeStripping.html</summary> + public class PreserveAttribute : System.Attribute { + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/astarclasses.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/astarclasses.cs.meta new file mode 100644 index 0000000..63b1d90 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/astarclasses.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f499b0552bded40b6a9d474dfb94343a +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} |