- Home /
How to calculate distance based on A*?
I've successfully set up my nodes everywhere for the graph, and I'm using the "AIPath" script included in the A* pathfinding package. Everything works great, except I need to somehow be able to access how far away the enemy is from the player on the path. I don't want the distance to be calculated in a straight line from the enemy to the player, I need it to take into account where the enemy is going to have to go before they can get to the player. So is there a way to calculate the distance based on the designated path the enemy's AI has created? Is it even possible to calculate the distance it will have to travel on the path until it reaches the player? I thought about figuring out how many nodes are between the two, but after trying some different things and still not being able to pull that off, I'm pretty stuck. I'm fine with JS, but A* is C#, and not to mention the fact that I've always been pretty bad at working with AI. If someone could help me out with this, I would be very much obliged.
Seeker.cs:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Pathfinding;
using System.Diagnostics;
[AddComponentMenu ("Pathfinding/Seeker")]
/** 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 it's pathfinding calls.
* It also handles post-processing of paths using modifiers.
* \see \ref calling-pathfinding
*/
public class Seeker : MonoBehaviour {
//====== SETTINGS ======
/* Recalculate last queried path when a graph changes. \see AstarPath.OnGraphsUpdated */
//public bool recalcOnGraphChange = true;
public bool drawGizmos = true;
public bool detailedGizmos = false;
/** Saves nearest nodes for previous path to enable faster Get Nearest Node calls.
* This variable basically does not a affect anything at the moment. So it is hidden in the inspector */
[HideInInspector]
public bool saveGetNearestHints = true;
public StartEndModifier startEndModifier = new StartEndModifier ();
[HideInInspector]
public TagMask traversableTags = new TagMask (-1,-1);
[HideInInspector]
/** 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.
* \see Pathfinding.Path.tagPenalties
*/
public int[] tagPenalties = new int[32];
public int totalPoints;
//====== SETTINGS ======
//public delegate Path PathReturn (Path p);
/** 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 */
public OnPathDelegate pathCallback;
/** Called before pathfinding is started */
public OnPathDelegate preProcessPath;
/** For anything which requires the original nodes (Node[]) (before modifiers) to work */
public OnPathDelegate postProcessOriginalPath;
/** Anything which only modifies the positions (Vector3[]) */
public OnPathDelegate postProcessPath;
//public GetNextTargetDelegate getNextTarget;
//DEBUG
//public Path lastCompletedPath;
[System.NonSerialized]
public List<Vector3> lastCompletedVectorPath;
[System.NonSerialized]
public List<Node> lastCompletedNodePath;
//END DEBUG
/** The current path */
[System.NonSerialized]
protected Path path;
/** Previous path. Used to draw gizmos */
private Path prevPath;
/** Returns #path */
public Path GetCurrentPath () {
return path;
}
private Node startHint;
private Node endHint;
private OnPathDelegate onPathDelegate;
private OnPathDelegate onPartialPathDelegate;
/** Temporary callback only called for the current path. This value is set by the StartPath functions */
private OnPathDelegate tmpPathCallback;
/** The path ID of the last path queried */
protected uint lastPathID = 0;
/** Initializes a few variables
*/
public void Awake () {
onPathDelegate = OnPathComplete;
startEndModifier.Awake (this);
}
/** Cleans up some variables.
* Releases any eventually claimed paths.
* Calls OnDestroy on the #startEndModifier.
*
* \see ReleaseClaimedPath
* \see startEndModifier
*/
public void OnDestroy () {
ReleaseClaimedPath ();
startEndModifier.OnDestroy (this);
}
/** Releases an eventual claimed path.
* 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.
*/
public void ReleaseClaimedPath () {
if (prevPath != null) {
prevPath.ReleaseSilent (this);
prevPath = null;
}
}
private List<IPathModifier> modifiers = new List<IPathModifier> ();
public void RegisterModifier (IPathModifier mod) {
if (modifiers == null) {
modifiers = new List<IPathModifier> (1);
}
modifiers.Add (mod);
}
public void DeregisterModifier (IPathModifier mod) {
if (modifiers == null) {
return;
}
modifiers.Remove (mod);
}
public enum ModifierPass {
PreProcess,
PostProcessOriginal,
PostProcess
}
/** 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
*/
public void PostProcess (Path p) {
RunModifiers (ModifierPass.PostProcess,p);
}
/** Runs modifiers on path \a p */
public void RunModifiers (ModifierPass pass, Path p) {
//Sort the modifiers based on priority (bubble sort (slow but since it's a small list, it works good))
bool changed = true;
while (changed) {
changed = false;
for (int i=0;i<modifiers.Count-1;i++) {
if (modifiers[i].Priority < modifiers[i+1].Priority) {
IPathModifier tmp = modifiers[i];
modifiers[i] = modifiers[i+1];
modifiers[i+1] = tmp;
changed = true;
}
}
}
//Call eventual delegates
switch (pass) {
case ModifierPass.PreProcess:
if (preProcessPath != null) preProcessPath (p);
break;
case ModifierPass.PostProcessOriginal:
if (postProcessOriginalPath != null) postProcessOriginalPath (p);
break;
case ModifierPass.PostProcess:
if (postProcessPath != null) postProcessPath (p);
break;
}
//No modifiers, then exit here
if (modifiers.Count == 0) return;
ModifierData prevOutput = ModifierData.All;
IPathModifier prevMod = modifiers[0];
//Loop through all modifiers and apply post processing
for (int i=0;i<modifiers.Count;i++) {
//Cast to MonoModifier, i.e modifiers attached as scripts to the game object
MonoModifier mMod = modifiers[i] as MonoModifier;
//Ignore modifiers which are not enabled
if (mMod != null && !mMod.enabled) continue;
switch (pass) {
case ModifierPass.PreProcess:
modifiers[i].PreProcess (p);
break;
case ModifierPass.PostProcessOriginal:
modifiers[i].ApplyOriginal (p);
break;
case ModifierPass.PostProcess:
//Convert the path if necessary to match the required input for the modifier
ModifierData newInput = ModifierConverter.Convert (p,prevOutput,modifiers[i].input);
if (newInput != ModifierData.None) {
modifiers[i].Apply (p,newInput);
prevOutput = modifiers[i].output;
} else {
UnityEngine.Debug.Log ("Error converting "+(i > 0 ? prevMod.GetType ().Name : "original")+"'s output to "+(modifiers[i].GetType ().Name)+"'s input.\nTry rearranging the modifier priorities on the Seeker.");
prevOutput = ModifierData.None;
}
prevMod = modifiers[i];
break;
}
if (prevOutput == ModifierData.None) {
break;
}
}
}
/** Is the current path done calculating.
* Returns true if the current #path has been returned or if the #path is null.
*
* \note Do not confuse this with Pathfinding.Path.IsDone. They do mostly return the same value, but not always.
*
* \since Added in 3.0.8
* \version Behaviour changed in 3.2
* */
public bool IsDone () {
return path == null || path.GetState() >= PathState.Returned;
}
/** 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) */
public void OnPathComplete (Path p) {
OnPathComplete (p,true,true);
}
/** Called when a path has completed.
* Will post process it and return it by calling #tmpPathCallback and #pathCallback */
public void OnPathComplete (Path p, bool runModifiers, bool sendCallbacks) {
AstarProfiler.StartProfile ("Seeker OnPathComplete");
if (p != null && p != path && sendCallbacks) {
return;
}
if (this == null || p == null || p != path)
return;
if (!path.error && runModifiers) {
AstarProfiler.StartProfile ("Seeker Modifiers");
//This will send the path for post processing to modifiers attached to this Seeker
RunModifiers (ModifierPass.PostProcessOriginal, path);
//This will send the path for post processing to modifiers attached to this Seeker
RunModifiers (ModifierPass.PostProcess, path);
AstarProfiler.EndProfile ();
}
if (sendCallbacks) {
p.Claim (this);
AstarProfiler.StartProfile ("Seeker Callbacks");
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);
}
//Recycle the previous path
if (prevPath != null) {
prevPath.ReleaseSilent (this);
}
prevPath = p;
//If not drawing gizmos, then storing prevPath is quite unecessary
//So clear it and set prevPath to null
if (!drawGizmos) ReleaseClaimedPath ();
AstarProfiler.EndProfile();
}
AstarProfiler.EndProfile ();
}
/*public void OnEnable () {
//AstarPath.OnGraphsUpdated += CheckPathValidity;
}
public void OnDisable () {
//AstarPath.OnGraphsUpdated -= CheckPathValidity;
}*/
/*public void CheckPathValidity (AstarPath active) {
/*if (!recalcOnGraphChange) {
return;
}
//Debug.Log ("Checking Path Validity");
//Debug.Break ();
if (lastCompletedPath != null && !lastCompletedPath.error) {
//Debug.Log ("Checking Path Validity");
StartPath (transform.position,lastCompletedPath.endPoint);
/*if (!lastCompletedPath.path[0].IsWalkable (lastCompletedPath)) {
StartPath (transform.position,lastCompletedPath.endPoint);
return;
}
for (int i=0;i<lastCompletedPath.path.Length-1;i++) {
if (!lastCompletedPath.path[i].ContainsConnection (lastCompletedPath.path[i+1],lastCompletedPath)) {
StartPath (transform.position,lastCompletedPath.endPoint);
return;
}
Debug.DrawLine (lastCompletedPath.path[i].position,lastCompletedPath.path[i+1].position,Color.cyan);
}*
}*
}*/
//The frame the last call was made from this Seeker
//private int lastPathCall = -1000;
/** 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 #StartPath(Path,OnPathDelegate,int) with no change, but if no change is required #StartPath(Vector3,Vector3,OnPathDelegate) does just that.
* \code Seeker seeker = GetComponent (typeof(Seeker)) as Seeker;
* Path p = seeker.GetNewPath (transform.position, transform.position+transform.forward*100);
* p.nnConstraint = NNConstraint.Default; \endcode */
public Path GetNewPath (Vector3 start, Vector3 end) {
//Construct a path with start and end points
Path p = ABPath.Construct (start, end, null);
return p;
}
/** Call this function to start calculating a path.
* \param start The start point of the path
* \param end The end point of the path
*/
public Path StartPath (Vector3 start, Vector3 end) {
return StartPath (start,end,null,-1);
}
/** Call this function to start calculating a path.
* \param start The start point of the path
* \param end The end point of the path
* \param callback The function to call when the path has been calculated
*
* \a callback will be called when the path has completed.
* \a Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed) */
public Path StartPath (Vector3 start, Vector3 end, OnPathDelegate callback) {
return StartPath (start,end,callback,-1);
}
/** Call this function to start calculating a path.
* \param start The start point of the path
* \param end The end point of the path
* \param callback The function to call when the path has been calculated
* \param graphMask Mask used to specify which graphs should be searched for close nodes. See Pathfinding.NNConstraint.graphMask.
*
* \a callback will be called when the path has completed.
* \a Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed) */
public Path StartPath (Vector3 start, Vector3 end, OnPathDelegate callback, int graphMask) {
Path p = GetNewPath (start,end);
return StartPath (p, callback, graphMask);
}
/** Call this function to start calculating a path.
* \param p The path to start calculating
* \param callback The function to call when the path has been calculated
* \param graphMask Mask used to specify which graphs should be searched for close nodes. See Pathfinding.NNConstraint.graphMask. \astarproParam
*
* \a callback will be called when the path has completed.
* \a Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed) */
public Path StartPath (Path p, OnPathDelegate callback = null, int graphMask = -1) {
p.enabledTags = traversableTags.tagsChange;
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.GetState() <= PathState.Processing && lastPathID == path.pathID) {
path.LogError ("Canceled path because a new one was requested\nGameObject: "+gameObject.name);
//No callback should be sent for the canceled path
}
path = p;
path.callback += onPathDelegate;
tmpPathCallback = callback;
//Set the Get Nearest Node hints if they have not already been set
/*if (path.startHint == null)
path.startHint = startHint;
if (path.endHint == null)
path.endHint = endHint;
*/
//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;
//Delay the path call by one frame if it was sent the same frame as the previous call
/*if (lastPathCall == Time.frameCount) {
StartCoroutine (DelayPathStart (path));
return path;
}*/
//lastPathCall = Time.frameCount;
//Pre process the path
RunModifiers (ModifierPass.PreProcess, path);
//Send the request to the pathfinder
AstarPath.StartPath (path);
return path;
}
public IEnumerator DelayPathStart (Path p) {
yield return 0;
//lastPathCall = Time.frameCount;
RunModifiers (ModifierPass.PreProcess, p);
AstarPath.StartPath (p);
}
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]);
}
}
}
}
I have never worked with the AIPath before but I did just make a program to create and find the closest objects around it. So I'm going to take some shots in the semi darkness and see if they hit.
few questions first. Is the AI always going after the player? Can you get the distance from AI to node? Are the nodes evenly placed or the distance between them vary?
Not always. I've created a system in which the AI wanders around the level until it catches sight of the player, then goes after them. I've done this by setting up an empty game object that picks a random transform object from somewhere in the level every 6 seconds, then moves the AIPath target to it's location. Once the enemy sees the player, the object stops picking random transforms, and just moves the AIPath target to follow the player's position. The nodes are definitely not evenly placed, they vary in distance in many different areas. I'm not quite sure what you mean about getting the distance from AI to node. Are you asking if I already have a script to do this?
Ok so When the AI sees the player he chases him down. And you at that point need the distance between him and the player? Or do you always need the distance between the two?
Well, I would prefer to always have the distance, but as I stated in my original post, I need the distance based on the AI's route. For instance, if the enemy is right next to the player, but there's a wall in between them, the standard distance check would calculate how far apart they are in world space, which would obviously be a very small distance, because they're right next to each other. But if it were calculated the way I need it to be, the distance would be greater because it involves the AI having to go AROUND the wall. I just want to make sure I've made that part clear, as I don't want there to be any confusion. Thanks again for replying.
I presume you keep a list or an array that represents the current path that the AI is taking, right? If so you can iterate through the nodes and find out which node is closest to the player.
Then you can iterate through the nodes, adding the distance between them, until you hit the node that's closest to the player. You can then add the distance between that node and the player, and that should give you your total distance. Here's a pseudo-code example:
Vector3[] positionList = CalculateAStarPath();
int nodeClosestToPlayer = 0;
float totalDistance = 0;
float currentDistance = $$anonymous$$athf.Infinity;
for(int i = 0; i < positionList.Length; i++)
{
//Check to see if stuff is between the node position and player position before allowing the node to be accepted as a smaller distance
if(!Physics.Linecast(player.position, positionList[i]) && Vector3.$$anonymous$$agnitude(player.position - positionList[i]) < currentDistance)
{
currentDistance = Vector3.$$anonymous$$agnitude(player.position - positionList[i]);
nodeClosestToPlayer = i;
}
}
//Assumes index 0 is the node closest to the enemy, and positionList.Length-1 is the target node
//start with distance from enemy to closest node
totalDistance = Vector3.$$anonymous$$agnitude(positionList[0] - transform.position);
for(int i = 1; i <= nodeClosestToPlayer; i++)
{
totalDistance = totalDistance + Vector3.$$anonymous$$agnitude(positionList[i] - positionList[i-1]);
}
totalDistance = totalDistance + Vector3.$$anonymous$$agnitude(player.position - positionList[nodeClosestToPlayer]);
You may need to do more than just a linecast to deter$$anonymous$$e whether it's valid to go from that node to the player, but you get the idea.
Answer by panbake · Nov 15, 2013 at 06:50 PM
A simple way to do this (also sort of pseudo code as the A* pathfinding project won't load for Unity 4.3) is just to iterate over the nodes like so
protected float GetDistanceToTarget()
{
List<Vector3> vPath = path.vectorPath;
float totalDistance = 0;
Vector3 current = transform.position;
//Iterate through vPath and find the distance between the nodes
for (int i = currentWaypointIndex; i < vPath.Count; i++)
{
totalDistance += (vPath[i] - current).magnitude;
current = vPath[i];
}
totalDistance += (target.position - current).magnitude;
return totalDistance;
}
You can place this in the AIPath class. If you want to store this for access by JS, make a public field in the AIPath class and update it with a call to GetDistanceToTarget().
You probably don't want to call this every frame (if you really do call it in Update) as iterating over every node every update is overkill and for lots of pathers will be a burden. Instead you could use another method to get the distance from the current position (transform.position) to the next waypoint and then maybe recalculate the whole thing once you've reached a new waypoint.
I tried exactly what you said, but I'm getting
error CS0236: A field initializer cannot reference the nonstatic field, method, or property
AIPath.GetDistanceToTarget()' I added a public float to the AIPath class
public float overallDistance = GetDistanceToTarget();`
Did I type something wrong? I can't figure out exactly what the root of the problem is. Thanks for your help, I appreciate it.
In order to call a function by classname, the function involved must be static.
protected static float GetDistanceToTarget()
Edit: actually, it needs to be public too if you're calling it from another class
public static float GetDistanceToTarget()
You can definitely make the method static, like LunaArgenteus suggested, but really I don't see a reason to call GetDistanceToTarget() on initialization. It is only ever valid after you have an actual path to follow, and from what I understood you want to call this so it updates. Basically in your Update() method you'll want to call.
void Update()
{
//Do other stuff
//Update distance to target
overallDistance = GetDistanceToTarget();
}
Furthermore if you do make it static it will have to take parameters for currentWaypointIndex, path.vectorPath and target. It's easier to just not call this before the class has been initialized.
O$$anonymous$$, doing what you've explained, I managed to get it working. I had to change the "public static float" to just "public float", but after that I finally stopped getting errors. However, there's just one problem. The distance being calculated is simply the regular distance between the enemy and the player in world space. This is what I was trying to distinguish in my question, as I was afraid I would appear to be asking something else. The distance float I'm trying to achieve is not the distance directly between the AI and the player, but rather the distance the AI is going to have to travel to reach the player. In this picture, if the AI's destination is Room B, then the AI is pretty much right next to it. However, "Distance 1" is not what I'm trying to calculate. I need "Distance 2", which is much greater than Distance 1 because it involves covering more ground. So it's not technically distance that I'm looking for, but I can't really think of another word to describe it. It's more like the length of the route ahead. I really do appreciate the help, and I'm sorry I didn't explain my question well enough before.
Assu$$anonymous$$g path.VectorPath returns a series of nodes that leads to the PLAYER, then the method to find the distance described by myself and pancake should give you distance 2. I'm guessing what's happening is that your target is not always the player (you mentioned the ai wanders around the level), so the vector path ends abruptly and the remaining linear distance (distance 1 from the end of the path) is added to the total distance.
I still think you might be able to use some method in the Seeker class to calculate a path to the player that you can use strictly for finding the distance. Try looking for the "StartPath" function in the Seeker class. If you're having trouble understanding the Seeker class, you can post it here and we can see if it will help with what you're looking to do.
Answer by cleitonw1 · Jun 04, 2020 at 11:54 PM
Try it:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Pathfinding; //ADD THIS
public class ZombieController : MonoBehaviour
{
public AIPath aIPath; //AIpath of the enemy
void Update()
{
Debug.Log("Distance: " + aIPath.remainingDistance);
}
}
Your answer
Follow this Question
Related Questions
Delete Astar Path gride graph around a particular Object 1 Answer
How do i get the IsPathPossible() function to ignore some nodes using Astar Pathfinding Project 0 Answers
A* PathFinding Radius sphere 1 Answer
How do i get the IsPathPossible() function to ignore some nodes using Astar Pathfinding Project 0 Answers