Adding A* Algorythm and carpet roads

This commit is contained in:
Nicolas SANS
2023-04-24 15:49:03 +02:00
parent 75018ee6cd
commit 690e3fbd0b
685 changed files with 134125 additions and 19 deletions

View File

@ -0,0 +1,774 @@
using UnityEngine;
using System.Collections;
using UnityEngine.Serialization;
namespace Pathfinding {
using Pathfinding.RVO;
using Pathfinding.Util;
/// <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>\copydoc Pathfinding::IAstarAI::radius</summary>
public float radius = 0.5f;
/// <summary>\copydoc Pathfinding::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 \reflink{autoRepath.interval}.
/// See: \reflink{AutoRepathPolicy}
/// </summary>
public float repathRate {
get {
return this.autoRepath.interval;
}
set {
this.autoRepath.interval = value;
}
}
/// <summary>
/// \copydoc Pathfinding::IAstarAI::canSearch
/// Deprecated: This has been superseded by \reflink{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>\copydoc Pathfinding::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>
/// 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 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>
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>
/// 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 IMovementPlane movementPlane = GraphTransform.identityTransform;
/// <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>Last frame index when <see cref="prevPosition1"/> was updated</summary>
protected int prevFrame;
/// <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;
[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>
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 {
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::destination</summary>
public Vector3 destination { get; set; }
/// <summary>\copydoc Pathfinding::IAstarAI::velocity</summary>
public Vector3 velocity {
get {
return lastDeltaTime > 0.000001f ? (prevPosition1 - prevPosition2) / lastDeltaTime : Vector3.zero;
}
}
/// <summary>
/// Velocity that this agent wants to move with.
/// Includes gravity and local avoidance if applicable.
/// </summary>
public Vector3 desiredVelocity { get { return lastDeltaTime > 0.00001f ? movementPlane.ToWorld(lastDeltaPosition / lastDeltaTime, verticalVelocity) : Vector3.zero; } }
/// <summary>\copydoc Pathfinding::IAstarAI::isStopped</summary>
public bool isStopped { get; set; }
/// <summary>\copydoc Pathfinding::IAstarAI::onSearchPath</summary>
public System.Action onSearchPath { get; set; }
/// <summary>True if the path should be automatically recalculated as soon as possible</summary>
protected virtual bool shouldRecalculatePath {
get {
return !waitingForPathCalculation && autoRepath.ShouldRecalculatePath((IAstarAI)this);
}
}
protected AIBase () {
// 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>
/// 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;
seeker = GetComponent<Seeker>();
// Find attached movement components
controller = GetComponent<CharacterController>();
rigid = GetComponent<Rigidbody>();
rigid2D = GetComponent<Rigidbody2D>();
}
/// <summary>Called when the component is enabled</summary>
protected virtual void OnEnable () {
FindComponents();
// Make sure we receive callbacks when paths are calculated
seeker.pathCallback += OnPathComplete;
Init();
}
/// <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>\copydoc Pathfinding::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 (clearPath) SearchPath();
}
protected void CancelCurrentPathRequest () {
waitingForPathCalculation = false;
// Abort calculation of the current path
if (seeker != null) seeker.CancelCurrentPathRequest();
}
protected virtual void OnDisable () {
ClearPath();
// Make sure we no longer receive callbacks when paths complete
seeker.pathCallback -= OnPathComplete;
velocity2D = Vector3.zero;
accumulatedMovementDelta = Vector3.zero;
verticalVelocity = 0f;
lastDeltaTime = 0;
}
/// <summary>
/// Called every frame.
/// If no rigidbodies are used then all movement happens here.
/// </summary>
protected virtual void Update () {
if (shouldRecalculatePath) SearchPath();
// 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 (rigid == null && rigid2D == null && canMove) {
Vector3 nextPosition;
Quaternion nextRotation;
MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);
FinalizeMovement(nextPosition, nextRotation);
}
}
/// <summary>
/// Called every physics update.
/// If rigidbodies are used then all movement happens here.
/// </summary>
protected virtual void FixedUpdate () {
if (!(rigid == null && rigid2D == null) && canMove) {
Vector3 nextPosition;
Quaternion nextRotation;
MovementUpdate(Time.fixedDeltaTime, out nextPosition, out nextRotation);
FinalizeMovement(nextPosition, nextRotation);
}
}
/// <summary>\copydoc Pathfinding::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>\copydoc Pathfinding::IAstarAI::SearchPath</summary>
public virtual void SearchPath () {
if (float.IsPositiveInfinity(destination.x)) return;
if (onSearchPath != null) onSearchPath();
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);
}
/// <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 <see cref="SetPath(null)"/>
///
/// See: <see cref="SetPath"/>
/// See: <see cref="isStopped"/>
/// </summary>
protected abstract void ClearPath();
/// <summary>\copydoc Pathfinding::IAstarAI::SetPath</summary>
public void SetPath (Path path) {
if (path == null) {
CancelCurrentPathRequest();
ClearPath();
} else if (path.PipelineState == PathState.Created) {
// Path has not started calculation yet
waitingForPathCalculation = true;
seeker.CancelCurrentPathRequest();
seeker.StartPath(path);
autoRepath.DidRecalculatePath(destination);
} else if (path.PipelineState == PathState.Returned) {
// 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();
else throw new System.ArgumentException("If you calculate the path using seeker.StartPath then this script will pick up the calculated path anyway as it listens for all paths the Seeker finishes calculating. You should not call SetPath in that case.");
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 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 (Vector2 position, float distanceToEndOfPath, float 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);
}
/// <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="maxDegrees">Maximum number of degrees to rotate this frame.</param>
protected Quaternion SimulateRotationTowards (Vector2 direction, float maxDegrees) {
if (direction != Vector2.zero) {
Quaternion targetRotation = Quaternion.LookRotation(movementPlane.ToWorld(direction, 0), movementPlane.ToWorld(Vector2.zero, 1));
// 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, maxDegrees);
}
return simulatedRotation;
}
/// <summary>\copydoc Pathfinding::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 () {
var currentFrame = Time.frameCount;
if (currentFrame != prevFrame) prevPosition2 = prevPosition1;
prevPosition1 = position;
prevFrame = currentFrame;
}
/// <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>
/// 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) {
RaycastHit hit;
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 hit, 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 hit.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);
protected virtual void OnDrawGizmos () {
if (!Application.isPlaying || !enabled) FindComponents();
var color = ShapeGizmoColor;
if (orientation == OrientationMode.YAxisForward) {
Draw.Gizmos.Cylinder(position, Vector3.forward, 0, radius * tr.localScale.x, color);
} else {
Draw.Gizmos.Cylinder(position, rotation * Vector3.up, tr.localScale.y * height, radius * tr.localScale.x, color);
}
if (!float.IsPositiveInfinity(destination.x) && Application.isPlaying) Draw.Gizmos.CircleXZ(destination, 0.2f, Color.blue);
autoRepath.DrawGizmos((IAstarAI)this);
}
protected override void Reset () {
ResetShape();
base.Reset();
}
void ResetShape () {
var cc = GetComponent<CharacterController>();
if (cc != null) {
radius = cc.radius;
height = Mathf.Max(radius*2, cc.height);
}
}
protected override int OnUpgradeSerializedData (int version, bool unityThread) {
if (unityThread && !float.IsNaN(centerOffsetCompatibility)) {
height = centerOffsetCompatibility*2;
ResetShape();
centerOffsetCompatibility = float.NaN;
}
#pragma warning disable 618
if (unityThread && targetCompatibility != null) target = targetCompatibility;
#pragma warning restore 618
if (version <= 3) {
repathRate = repathRateCompatibility;
canSearch = canSearchCompability;
}
return 5;
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a08f67bbe580e4ddfaebd06363c9cc97
timeCreated: 1496932372
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 100
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,701 @@
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"/>.
///
/// \ingroup movementscripts
/// </summary>
[RequireComponent(typeof(Seeker))]
[AddComponentMenu("Pathfinding/AI/AILerp (2D,3D)")]
[HelpURL("http://arongranberg.com/astar/docs/class_pathfinding_1_1_a_i_lerp.php")]
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 \reflink{autoRepath.interval}.
/// See: \reflink{AutoRepathPolicy}
/// </summary>
public float repathRate {
get {
return this.autoRepath.interval;
}
set {
this.autoRepath.interval = value;
}
}
/// <summary>
/// \copydoc Pathfinding::IAstarAI::canSearch
/// Deprecated: This has been superseded by \reflink{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>
/// 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>
/// 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;
}
}
}
#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;
}
}
/// <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 Mathf.Max(interpolator.remainingDistance, 0);
}
set {
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 interpolator = 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"/>
/// See: <see cref="RepeatTrySearchPath"/>
/// </summary>
protected virtual void Start () {
startHasRun = true;
Init();
}
/// <summary>Called when the component is enabled</summary>
protected virtual void OnEnable () {
// Make sure we receive callbacks when paths complete
seeker.pathCallback += 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();
// Make sure we no longer receive callbacks when paths complete
seeker.pathCallback -= OnPathComplete;
}
/// <summary>\copydoc Pathfinding::IAstarAI::GetRemainingPath</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;
}
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((IAstarAI)this);
}
}
/// <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));
}
/// <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.
/// </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;
// 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;
interpolator.SetPath(null);
}
/// <summary>\copydoc Pathfinding::IAstarAI::SetPath</summary>
public void SetPath (Path path) {
if (path == null) {
ClearPath();
} else if (path.PipelineState == PathState.Created) {
// Path has not started calculation yet
canSearchAgain = false;
seeker.CancelCurrentPathRequest();
seeker.StartPath(path);
autoRepath.DidRecalculatePath(destination);
} else if (path.PipelineState == PathState.Returned) {
// 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();
else throw new System.ArgumentException("If you calculate the path using seeker.StartPath then this script will pick up the calculated path anyway as it listens for all paths the Seeker finishes calculating. You should not call SetPath in that case.");
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;
interpolator.SetPath(path.vectorPath);
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>
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 int OnUpgradeSerializedData (int version, bool unityThread) {
#pragma warning disable 618
if (unityThread && targetCompatibility != null) target = targetCompatibility;
#pragma warning restore 618
if (version <= 3) {
repathRate = repathRateCompatibility;
canSearch = canSearchCompability;
}
return 4;
}
public virtual void OnDrawGizmos () {
tr = transform;
autoRepath.DrawGizmos((IAstarAI)this);
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 847a14d4dc9cc43679ab34fc78e0182f
timeCreated: 1454879612
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,491 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace Pathfinding {
using Pathfinding.RVO;
using Pathfinding.Util;
/// <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 <see cref="Update"/> or <see cref="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 <see cref="Update"/> (or <see cref="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="Pathfinding.Seeker"/> component which should be attached to the same GameObject as this script.
/// Since this script will when waking up register to the <see cref="Pathfinding.Seeker.pathCallback"/> delegate this script will be notified every time a new path is calculated by the <see cref="OnPathComplete"/> method being called.
/// It may take one or sometimes multiple frames for the path to be calculated, but finally the <see cref="OnPathComplete"/> method will be called and the current path that the AI is following will be replaced.
/// </summary>
[AddComponentMenu("Pathfinding/AI/AIPath (2D,3D)")]
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>
/// Distance to the end point to consider the end of path to be reached.
/// When the end is within this distance then <see cref="OnTargetReached"/> will be called and <see cref="reachedEndOfPath"/> will return true.
/// </summary>
public float endReachedDistance = 0.2F;
/// <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.
/// </summary>
public bool slowWhenNotFacingTarget = true;
/// <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="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>
/// 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>Helper which calculates points along the current path</summary>
protected PathInterpolator interpolator = 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 {
get {
return interpolator.valid ? interpolator.remainingDistance + movementPlane.ToPlane(interpolator.position - position).magnitude : float.PositiveInfinity;
}
}
/// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary>
public 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
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::reachedEndOfPath</summary>
public bool reachedEndOfPath { get; protected set; }
/// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary>
public bool hasPath {
get {
return interpolator.valid;
}
}
/// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary>
public bool pathPending {
get {
return waitingForPathCalculation;
}
}
/// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary>
public Vector3 steeringTarget {
get {
return interpolator.valid ? interpolator.position : position;
}
}
/// <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; } }
#endregion
/// <summary>\copydoc Pathfinding::IAstarAI::GetRemainingPath</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);
}
protected override void OnDisable () {
base.OnDisable();
// Release current path so that it can be pooled
if (path != null) path.Release(this);
path = null;
interpolator.SetPath(null);
reachedEndOfPath = 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.
///
/// 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.
/// </summary>
public virtual void OnTargetReached () {
}
/// <summary>
/// Called when a requested path has been calculated.
/// A path is first requested by <see cref="UpdatePath"/>, 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;
// Make sure the path contains at least 2 points
if (path.vectorPath.Count == 1) path.vectorPath.Add(path.vectorPath[0]);
interpolator.SetPath(path.vectorPath);
var graph = path.path.Count > 0 ? AstarData.GetGraph(path.path[0]) as ITransformedGraph : null;
movementPlane = graph != null ? graph.transform : (orientation == OrientationMode.YAxisForward ? new GraphTransform(Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(-90, 270, 90), Vector3.one)) : GraphTransform.identityTransform);
// 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();
if (path != null) path.Release(this);
path = null;
interpolator.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;
// 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
float distanceToEnd = dir.magnitude + Mathf.Max(0, interpolator.remainingDistance);
// Check if we have reached the target
var prevTargetReached = reachedEndOfPath;
reachedEndOfPath = distanceToEnd <= endReachedDistance && interpolator.valid;
if (!prevTargetReached && reachedEndOfPath) OnTargetReached();
float slowdown;
// 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 (interpolator.valid && !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.
slowdown = distanceToEnd < slowdownDistance? Mathf.Sqrt(distanceToEnd / slowdownDistance) : 1;
if (reachedEndOfPath && whenCloseToDestination == CloseToDestinationMode.Stop) {
// Slow down as quickly as possible
velocity2D -= Vector2.ClampMagnitude(velocity2D, currentAcceleration * deltaTime);
} else {
velocity2D += MovementUtilities.CalculateAccelerationToReachPoint(dir, dir.normalized*maxSpeed, velocity2D, currentAcceleration, rotationSpeed, maxSpeed, forwards) * deltaTime;
}
} else {
slowdown = 1;
// Slow down as quickly as possible
velocity2D -= Vector2.ClampMagnitude(velocity2D, currentAcceleration * deltaTime);
}
velocity2D = MovementUtilities.ClampVelocity(velocity2D, maxSpeed, slowdown, slowWhenNotFacingTarget && enableRotation, forwards);
ApplyGravity(deltaTime);
// Set how much the agent wants to move during this frame
var delta2D = lastDeltaPosition = CalculateDeltaToMoveThisFrame(movementPlane.ToPlane(currentPosition), distanceToEnd, deltaTime);
nextPosition = currentPosition + movementPlane.ToWorld(delta2D, verticalVelocity * lastDeltaTime);
CalculateNextRotation(slowdown, out nextRotation);
}
protected virtual void CalculateNextRotation (float slowdown, out Quaternion nextRotation) {
if (lastDeltaTime > 0.00001f && enableRotation) {
Vector2 desiredRotationDirection;
desiredRotationDirection = velocity2D;
// Rotate towards the direction we are moving in.
// Don't rotate when we are very close to the target.
var currentRotationSpeed = rotationSpeed * Mathf.Max(0, (slowdown - 0.3f) / 0.7f);
nextRotation = SimulateRotationTowards(desiredRotationDirection, currentRotationSpeed * lastDeltaTime);
} else {
// TODO: simulatedRotation
nextRotation = rotation;
}
}
static NNConstraint cachedNNConstraint = NNConstraint.Default;
protected override Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) {
if (constrainInsideGraph) {
cachedNNConstraint.tags = seeker.traversableTags;
cachedNNConstraint.graphMask = seeker.graphMask;
cachedNNConstraint.distanceXZ = true;
var clampedPosition = AstarPath.active.GetNearest(position, cachedNNConstraint).position;
// 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);
protected override void OnDrawGizmos () {
base.OnDrawGizmos();
if (alwaysDrawGizmos) OnDrawGizmosInternal();
}
protected override void OnDrawGizmosSelected () {
base.OnDrawGizmosSelected();
if (!alwaysDrawGizmos) OnDrawGizmosInternal();
}
void OnDrawGizmosInternal () {
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) * (UnityEditor.Selection.gameObjects.Length == 1 ? 1 : 0);
if (alpha > 0) {
// Make sure the scene view is repainted while the gizmos are visible
if (!alwaysDrawGizmos) UnityEditor.SceneView.RepaintAll();
Draw.Gizmos.Line(position, steeringTarget, GizmoColor * new Color(1, 1, 1, alpha));
Gizmos.matrix = Matrix4x4.TRS(position, transform.rotation * (orientation == OrientationMode.YAxisForward ? Quaternion.Euler(-90, 0, 0) : Quaternion.identity), Vector3.one);
Draw.Gizmos.CircleXZ(Vector3.zero, pickNextWaypointDist, GizmoColor * new Color(1, 1, 1, alpha));
Draw.Gizmos.CircleXZ(Vector3.zero, slowdownDistance, Color.Lerp(GizmoColor, Color.red, 0.5f) * new Color(1, 1, 1, alpha));
Draw.Gizmos.CircleXZ(Vector3.zero, endReachedDistance, Color.Lerp(GizmoColor, Color.red, 0.8f) * new Color(1, 1, 1, alpha));
}
}
#endif
protected override int OnUpgradeSerializedData (int version, bool unityThread) {
// Approximately convert from a damping value to a degrees per second value.
if (version < 1) rotationSpeed *= 90;
return base.OnUpgradeSerializedData(version, unityThread);
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f6eb1402c17e84a9282a7f0f62eb584f
timeCreated: 1491225739
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,392 @@
using UnityEngine;
using System.Collections.Generic;
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: 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"/>
/// </summary>
Vector3 desiredVelocity { get; }
/// <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="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>
/// 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>
/// 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 script 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>
/// 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.
/// Note that if you calculate the path using seeker.StartPath then this script will already pick it up because it is listening for
/// all paths that the Seeker finishes calculating. In that case you do not need to call this function.
///
/// 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.
///
/// <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);
///
/// // If you want to make use of properties like ai.reachedDestination or ai.remainingDistance or similar
/// // you should also set the destination property to something reasonable.
/// // Since the agent's own path recalculation is disabled, setting this will not affect how the paths are calculated.
/// // ai.destination = ...
/// </code>
/// </summary>
void SetPath(Path path);
/// <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.
///
/// 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 AILerp script) if it does not have
/// any concept of being moved externally.
///
/// 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>
/// </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);
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b7438f3f6b9404f05ab7f584f92aa7d5
timeCreated: 1495013922
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 20549335d45df4a329ece093b865221b
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@ -0,0 +1,507 @@
using UnityEngine;
using System.Collections.Generic;
#if UNITY_5_5_OR_NEWER
using UnityEngine.Profiling;
#endif
namespace Pathfinding {
/// <summary>
/// Handles path calls for a single unit.
/// \ingroup relevant
/// 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.
///
/// [Open online documentation to see images]
///
/// See: calling-pathfinding (view in online documentation for working links)
/// See: modifiers (view in online documentation for working links)
/// </summary>
[AddComponentMenu("Pathfinding/Seeker")]
[HelpURL("http://arongranberg.com/astar/docs/class_pathfinding_1_1_seeker.php")]
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.
///
/// Note: This array should always have a length of 32 otherwise the system will ignore it.
///
/// 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.Default;
///
/// 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>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.\n
/// A temporary callback can also be set when calling StartPath, but that delegate will only be called for that path
/// </summary>
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;
/// <summary>Used for drawing gizmos</summary>
[System.NonSerialized]
List<Vector3> lastCompletedVectorPath;
/// <summary>Used for drawing gizmos</summary>
[System.NonSerialized]
List<GraphNode> lastCompletedNodePath;
/// <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>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;
}
/// <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: pathCallback
/// </summary>
public Path GetCurrentPath () {
return 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>
public 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: RunModifiers
/// \since Added in 3.2
/// </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
/// since the path might be completely calculated, but it has not yet been processed by the Seeker.
///
/// \since Added in 3.0.8
/// Version: Behaviour changed in 3.2
/// </summary>
public bool IsDone () {
return path == null || path.PipelineState >= PathState.Returned;
}
/// <summary>
/// Called when a path has completed.
/// This should have been implemented as optional parameter values, but that didn't seem to work very well with delegates (the values weren't the default ones)
/// See: OnPathComplete(Path,bool,bool)
/// </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);
lastCompletedNodePath = p.path;
lastCompletedVectorPath = p.vectorPath;
// 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);
}
// 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>
/// Returns a new path instance.
/// The path will be taken from the path pool if path recycling is turned on.\n
/// This path can be sent to <see cref="StartPath(Path,OnPathDelegate,int)"/> with no change, but if no change is required <see cref="StartPath(Vector3,Vector3,OnPathDelegate)"/> does just that.
/// <code>
/// var seeker = GetComponent<Seeker>();
/// Path p = seeker.GetNewPath (transform.position, transform.position+transform.forward*100);
/// // Disable heuristics on just this path for example
/// p.heuristic = Heuristic.None;
/// seeker.StartPath (p, OnPathComplete);
/// </code>
/// Deprecated: Use ABPath.Construct(start, end, null) instead.
/// </summary>
[System.Obsolete("Use ABPath.Construct(start, end, null) instead")]
public ABPath GetNewPath (Vector3 start, Vector3 end) {
// Construct a path with start and end points
return ABPath.Construct(start, end, null);
}
/// <summary>
/// Call this function to start calculating a path.
/// Since this method does not take a callback parameter, you should set the <see cref="pathCallback"/> field before calling this method.
/// </summary>
/// <param name="start">The start point of the path</param>
/// <param name="end">The end point of the path</param>
public Path StartPath (Vector3 start, Vector3 end) {
return StartPath(start, end, null);
}
/// <summary>
/// Call this function to start calculating a path.
///
/// callback 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)
/// </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</param>
public Path StartPath (Vector3 start, Vector3 end, OnPathDelegate callback) {
return StartPath(ABPath.Construct(start, end, null), callback);
}
/// <summary>
/// Call this function to start calculating a path.
///
/// callback 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)
/// </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</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>
/// 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.
///
/// Version: Since 3.8.3 this method works properly if a MultiTargetPath is used.
/// It now behaves identically to the StartMultiTargetPath(MultiTargetPath) method.
///
/// 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).
/// </summary>
/// <param name="p">The path to start calculating</param>
/// <param name="callback">The function to call when the path has been calculated</param>
public Path StartPath (Path p, OnPathDelegate callback = null) {
// 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.
///
/// Version: Since 3.8.3 this method works properly if a MultiTargetPath is used.
/// It now behaves identically to the StartMultiTargetPath(MultiTargetPath) method.
/// </summary>
/// <param name="p">The path to start calculating</param>
/// <param name="callback">The function to call when the path has been calculated</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) {
p.callback += onPathDelegate;
p.enabledTags = traversableTags;
p.tagPenalties = tagPenalties;
// 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>Draws gizmos for the Seeker</summary>
public void OnDrawGizmos () {
if (lastCompletedNodePath == null || !drawGizmos) {
return;
}
if (detailedGizmos) {
Gizmos.color = new Color(0.7F, 0.5F, 0.1F, 0.5F);
if (lastCompletedNodePath != null) {
for (int i = 0; i < lastCompletedNodePath.Count-1; i++) {
Gizmos.DrawLine((Vector3)lastCompletedNodePath[i].position, (Vector3)lastCompletedNodePath[i+1].position);
}
}
}
Gizmos.color = new Color(0, 1F, 0, 1F);
if (lastCompletedVectorPath != null) {
for (int i = 0; i < lastCompletedVectorPath.Count-1; i++) {
Gizmos.DrawLine(lastCompletedVectorPath[i], lastCompletedVectorPath[i+1]);
}
}
}
protected override int OnUpgradeSerializedData (int version, bool unityThread) {
if (graphMaskCompatibility != -1) {
Debug.Log("Loaded " + graphMaskCompatibility + " " + graphMask.value);
graphMask = graphMaskCompatibility;
graphMaskCompatibility = -1;
}
return base.OnUpgradeSerializedData(version, unityThread);
}
}
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 373b52eb9bf8c40f785bb6947a1aee66
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@ -0,0 +1,25 @@
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding.Examples {
/// <summary>Helper script in the example scene 'Turn Based'</summary>
[HelpURL("http://arongranberg.com/astar/docs/class_pathfinding_1_1_examples_1_1_turn_based_a_i.php")]
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 });
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8f95b80c439d6408b9afac9d013922e4
timeCreated: 1453035991
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: