using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding {
[System.Serializable]
///
/// Adjusts start and end points of a path.
///
/// This modifier is included in the component and is always used if you are using a Seeker.
/// When a path is calculated the resulting path will only be the positions of the nodes it passes through.
/// However often you may not want to navigate to the center of a specific node but instead to a point on the surface of a node.
/// This modifier will adjust the endpoints of the path.
///
/// [Open online documentation to see images]
///
/// \ingroup modifiers
///
public class StartEndModifier : PathModifier {
public override int Order { get { return 0; } }
///
/// Add points to the path instead of replacing them.
/// If for example is set to ClosestOnNode then the path will be modified so that
/// the path goes first to the center of the last node in the path and then goes to the closest point
/// on the node to the end point in the path request.
///
/// If this is false however then the relevant points in the path will simply be replaced.
/// In the above example the path would go directly to the closest point on the node without passing
/// through the center of the node.
///
public bool addPoints;
///
/// How the start point of the path will be determined.
/// See:
///
public Exactness exactStartPoint = Exactness.ClosestOnNode;
///
/// How the end point of the path will be determined.
/// See:
///
public Exactness exactEndPoint = Exactness.ClosestOnNode;
///
/// Will be called when a path is processed.
/// The value which is returned will be used as the start point of the path
/// and potentially clamped depending on the value of the field.
/// Only used for the Original, Interpolate and NodeConnection modes.
///
public System.Func adjustStartPoint;
///
/// Sets where the start and end points of a path should be placed.
///
/// Here is a legend showing what the different items in the above images represent.
/// The images above show a path coming in from the top left corner and ending at a node next to an obstacle as well as 2 different possible end points of the path and how they would be modified.
/// [Open online documentation to see images]
///
public enum Exactness {
///
/// The point is snapped to the position of the first/last node in the path.
/// Use this if your game is very tile based and you want your agents to stop precisely at the center of the nodes.
/// If you recalculate the path while the agent is moving you may want the start point snapping to be ClosestOnNode and the end point snapping to be SnapToNode however
/// as while an agent is moving it will likely not be right at the center of a node.
///
/// [Open online documentation to see images]
///
SnapToNode,
///
/// The point is set to the exact point which was passed when creating the path request.
/// Note that if a path was for example requested to a point inside an obstacle, then the last point of the path will be inside that obstacle, which is usually not what you want.
/// Consider using the option instead.
///
/// [Open online documentation to see images]
///
Original,
///
/// The point is set to the closest point on the line between either the two first points or the two last points.
/// Usually you will want to use the NodeConnection mode instead since that is usually the behaviour that you really want.
/// This mode exists mostly for compatibility reasons.
/// [Open online documentation to see images]
/// Deprecated: Use NodeConnection instead.
///
Interpolate,
///
/// The point is set to the closest point on the surface of the node. Note that some node types (point nodes) do not have a surface, so the "closest point" is simply the node's position which makes this identical to .
/// This is the mode that you almost always want to use in a free movement 3D world.
/// [Open online documentation to see images]
///
ClosestOnNode,
///
/// The point is set to the closest point on one of the connections from the start/end node.
/// This mode may be useful in a grid based or point graph based world when using the AILerp script.
///
/// Note: If you are using this mode with a you probably also want to use the for .
///
/// [Open online documentation to see images]
///
NodeConnection,
}
///
/// Do a straight line check from the node's center to the point determined by the .
/// There are very few cases where you will want to use this. It is mostly here for
/// backwards compatibility reasons.
///
/// Version: Since 4.1 this field only has an effect for the mode Original because that's the only one where it makes sense.
///
public bool useRaycasting;
public LayerMask mask = -1;
///
/// Do a straight line check from the node's center to the point determined by the .
/// See:
///
/// Version: Since 4.1 this field only has an effect for the mode Original because that's the only one where it makes sense.
///
public bool useGraphRaycasting;
List connectionBuffer;
System.Action connectionBufferAddDelegate;
public override void Apply (Path _p) {
var p = _p as ABPath;
// This modifier only supports ABPaths (doesn't make much sense for other paths anyway)
if (p == null || p.vectorPath.Count == 0) return;
bool singleNode = false;
if (p.vectorPath.Count == 1 && !addPoints) {
// Duplicate first point
p.vectorPath.Add(p.vectorPath[0]);
singleNode = true;
}
// Add instead of replacing points
bool forceAddStartPoint, forceAddEndPoint;
// Which connection the start/end point was on (only used for the Connection mode)
int closestStartConnection, closestEndConnection;
Vector3 pStart = Snap(p, exactStartPoint, true, out forceAddStartPoint, out closestStartConnection);
Vector3 pEnd = Snap(p, exactEndPoint, false, out forceAddEndPoint, out closestEndConnection);
// This is a special case when the path is only a single node and the Connection mode is used.
// (forceAddStartPoint/forceAddEndPoint is only used for the Connection mode)
// In this case the start and end points lie on the connections of the node.
// There are two cases:
// 1. If the start and end points lie on the same connection we do *not* want
// the path to pass through the node center but instead go directly from point to point.
// This is the case of closestStartConnection == closestEndConnection.
// 2. If the start and end points lie on different connections we *want*
// the path to pass through the node center as it goes from one connection to another one.
// However in any case we only want the node center to be added once to the path
// so we set forceAddStartPoint to false anyway.
if (singleNode) {
if (closestStartConnection == closestEndConnection) {
forceAddStartPoint = false;
forceAddEndPoint = false;
} else {
forceAddStartPoint = false;
}
}
// Add or replace the start point
// Disable adding of points if the mode is SnapToNode since then
// the first item in vectorPath will very likely be the same as the
// position of the first node
if ((forceAddStartPoint || addPoints) && exactStartPoint != Exactness.SnapToNode) {
p.vectorPath.Insert(0, pStart);
} else {
p.vectorPath[0] = pStart;
}
if ((forceAddEndPoint || addPoints) && exactEndPoint != Exactness.SnapToNode) {
p.vectorPath.Add(pEnd);
} else {
p.vectorPath[p.vectorPath.Count-1] = pEnd;
}
}
Vector3 Snap (ABPath path, Exactness mode, bool start, out bool forceAddPoint, out int closestConnectionIndex) {
var index = start ? 0 : path.path.Count - 1;
var node = path.path[index];
var nodePos = (Vector3)node.position;
closestConnectionIndex = 0;
forceAddPoint = false;
switch (mode) {
case Exactness.ClosestOnNode:
return start ? path.startPoint : path.endPoint;
case Exactness.SnapToNode:
return nodePos;
case Exactness.Original:
case Exactness.Interpolate:
case Exactness.NodeConnection:
Vector3 relevantPoint;
if (start) {
relevantPoint = adjustStartPoint != null? adjustStartPoint() : path.originalStartPoint;
} else {
relevantPoint = path.originalEndPoint;
}
switch (mode) {
case Exactness.Original:
return GetClampedPoint(nodePos, relevantPoint, node);
case Exactness.Interpolate:
// Adjacent node to either the start node or the end node in the path
var adjacentNode = path.path[Mathf.Clamp(index + (start ? 1 : -1), 0, path.path.Count-1)];
return VectorMath.ClosestPointOnSegment(nodePos, (Vector3)adjacentNode.position, relevantPoint);
case Exactness.NodeConnection:
// This code uses some tricks to avoid allocations
// even though it uses delegates heavily
// The connectionBufferAddDelegate delegate simply adds whatever node
// it is called with to the connectionBuffer
connectionBuffer = connectionBuffer ?? new List();
connectionBufferAddDelegate = connectionBufferAddDelegate ?? (System.Action)connectionBuffer.Add;
// Adjacent node to either the start node or the end node in the path
adjacentNode = path.path[Mathf.Clamp(index + (start ? 1 : -1), 0, path.path.Count-1)];
// Add all neighbours of #node to the connectionBuffer
node.GetConnections(connectionBufferAddDelegate);
var bestPos = nodePos;
var bestDist = float.PositiveInfinity;
// Loop through all neighbours
// Do it in reverse order because the length of the connectionBuffer
// will change during iteration
for (int i = connectionBuffer.Count - 1; i >= 0; i--) {
var neighbour = connectionBuffer[i];
if (!path.CanTraverse(neighbour)) continue;
// Find the closest point on the connection between the nodes
// and check if the distance to that point is lower than the previous best
var closest = VectorMath.ClosestPointOnSegment(nodePos, (Vector3)neighbour.position, relevantPoint);
var dist = (closest - relevantPoint).sqrMagnitude;
if (dist < bestDist) {
bestPos = closest;
bestDist = dist;
closestConnectionIndex = i;
// If this node is not the adjacent node
// then the path should go through the start node as well
forceAddPoint = neighbour != adjacentNode;
}
}
connectionBuffer.Clear();
return bestPos;
default:
throw new System.ArgumentException("Cannot reach this point, but the compiler is not smart enough to realize that.");
}
default:
throw new System.ArgumentException("Invalid mode");
}
}
protected Vector3 GetClampedPoint (Vector3 from, Vector3 to, GraphNode hint) {
Vector3 point = to;
RaycastHit hit;
if (useRaycasting && Physics.Linecast(from, to, out hit, mask)) {
point = hit.point;
}
if (useGraphRaycasting && hint != null) {
var rayGraph = AstarData.GetGraph(hint) as IRaycastableGraph;
if (rayGraph != null) {
GraphHitInfo graphHit;
if (rayGraph.Linecast(from, point, hint, out graphHit)) {
point = graphHit.point;
}
}
}
return point;
}
}
}