How to make A.I. Flee/Stay at distance?

Hey,

I’ve seen that A* Pathfinding has a possibility to make A.I. flee or, from my understanding, avoid the target while moving around it/staying at distance. I’m not sure where should I modify that path from. I’m using RichAI and a Recast graph.

I’d like two behaviors:

  • one A.I. that is always running away from you
  • one that stays X distance away (remains around you but doesn’t let you close in too much)

I usually don’t have many long chunks of uninterrupted time in my day and thus would really benefit from a nudge in the right direction : )

Thanks!

1 Like

Hi

The system has a path type called RandomPath and one called FleePath that may be used for this purpose. Since you are using a navmesh based graph I think it might be worth just trying to make them move toward a specific point instead because paths operate on nodes and nodes in navmesh graphs could be pretty large and it may not give you a good enough fleeing behavior.

So what I would do in these scenarios is to for the fleeing path just pick a point in the opposite direction of the enemy and make the character try to move there. This will work well if your environment is relatively open, however if it is more closed, like in a maze this may work very badly and you may want to use the RandomPath type instead.

For the staying X distance away path I would start with just picking a point closer or further away from the enemy based on the current distance, something like

var targetToMoveTo = enemy.position + (transform.position - enemy.position).normalized * desiredDistance;

A more advanced solution could use a ConstantPath or maybe a BFS (which is easier to use) to find a bunch of nodes around the enemy, then you could randomly pick points on those nodes while keeping a score for how good they are (for example how close to the desired distance they are and maybe other factors) and then pick the best point to move to. I think this might handle cases where the unit is cornered better, but it’s hard to say since I haven’t implemented it.

1 Like

Thanks for the suggestions Aaron.
I’ll look into these step by step and probably ask more questions as I try to implement them.

Before I dive into it though, I just wanted to make sure that the RecastGraph is the best option for my needs, since the solution for the behaviour I need depends on what type of graph I have.

I figured I’d describe to you the type of game in general and you’d be able to recommend the graph (my take on it was that Recast is best).

I’m making a First Person Shooter, where I generate a level randomly every time. These levels have areas that look like a maze and aren’t very open, as well as some open areas. So I need to consider both. So far they don’t have any elevation, neither platforms to jump up/down on, neither overlapping bridges. I might make them in the future so I’d like to hear what the limitations of the recommended solution would be for those as well.

Regarding my A.I. needs, I mainly need to make three types:

  • one that’s just chasing you, trying to touch you (this one I achieved relatively simply).
  • one that shoots at you and thus needs to stay at some distance, and if you try to get close will try to maintain that distance.
  • one that just runs away from you, where the A.I. strives to always get away from you until it’s a safe distance away. This distance should be preferably a traversible path instead of an absolute distance. I.e., if I’m on the other side of the wall, but to get to the A.I. I need to walk X units, the safe distance should be X units, not the distance between me and the A.I.

Sorry for the long description, I just want to make sure I’m using the right tool from the start.

Thanks!

1 Like

Hi

Ok.

So the problem with using a recast/navmesh graph for this is that all pathfinding is done on nodes and in recast/navmesh graphs those can be quite large. This means that the distance calculations will not be very accurate. You can work around this however. If you use a normal path (see docs) and generate a path to the player then you can post process it by removing X units at the end of the path. That will bring the unit to a stop X units from the player. It may be the case that the unit is too close to the player however and in that case you will need to use a FleePath (see docs) and do roughly the same thing (keep track of the actual distance to the player and cut off the path after X - actualDistanceToPlayer units). That will move the unit so that it is around X units away from the player.
So overall it requires a few tricks, but I don’t think it should be that complicated to implement.

1 Like

I haven’t implemented these yet but I think I understand what you mean:
If the A.I. is too far, I create a standard path so it’d move towards the player, then cut away some of the end part of that path so that the unit will be X absolute distance away from the player; once it’s ready I call seeker.StartPath and feed in that path. If it happens that A.I. moved closer than absolute X distance between the player and A.I. (or player moved closer to it in the meantime) then I call a FleePath, which will move the A.I. away from the player until it is a desired absolute X distance away.
Did I get it right?

Thank you, once I get some uninterrupted free time I’ll look into this as well as try to implement some kind of fleeing behavior using your suggestions of ConstanPath of BFS!

Meanwhile couple of questions:

  • could you point me to how would one remove a part of path so it ends at the desired distance away from the player?
  • would recast graph be the best solution for the type of game I described?
1 Like

That sounds about right.

In the movement script when the path has been calculated (normally in the OnPathComplete method) you can access the path using the ‘path.vectorPath’ list. You can do something like this to cut it up.

 public List<Vector3> LimitLength (List<Vector3> path, float desiredLength) {
       List<Vector3> result = new List<Vector3>();
       if (path.Count == 0) return result;
       float length = 0;
       result.Add(path[0]);
       for (int i = 0; i < path.Count - 1; i++) {
            var segLength = Vector3.Distance(path[i], path[i+1]);
            if (length + segLength > desiredLength) {
                  // Need to cut this segment
                 result.Add(Vector3.Lerp(path[i], path[i+1], Mathf.InverseLerp(length, length + segLength, desiredLength)));
                 break;
             } else {
                 // No cutting required
                 result.Add(path[i+1]);
             }
      }
      return result;
}

If you need overlapping regions, then a recast graph is probably the best way to go. Otherwise I would consider a grid graph because it can be calculated faster. It’s hard to give a definite answer.

1 Like

Hey Aron, first and foremost i’m terribly sorry for misspelling your name all this time, I just realized it has only one “a”.

Ok, I’m trying to implement a ConstantPath and having couple of problems.

First, there’s a bit of confusion regarding the seeker.pathCallback. I registered my own method to it (OnPathComplete) so that I could use that with ConstantPath (as it is explained in documentation). I’d like the callback to happen when I call seeker.StartPath method specifying a callback. It seems it’s being called on any path calculation, regardless whether specified or not. In my current code, I use richAIScript.UpdatePath() method to move the A.I. around when they’re idle. That method uses seeker.StartPath(tr.position, target.position). There’s no callback specified but it calls OnPathComplete anyway. I have two questions regarding this problem:

  1. Should I stop using UpdatePath and move my A.I. somehow else?
  2. Is there a way to use the callback on specific paths only (in my case, I need it for ConstantPath but not for other cases)

Second, below is the code with which I’m trying to use the ConstantPath, i.e. get the nodes around it, pick several random ones, see which one is the furthest from player, and start moving towards that (i want to use that for fleeing behavior for now).

void Flee() //this one's being called every tick if player is close enough
{
	if (readyToFlee)
	{
		ConstantPath cpath = ConstantPath.Construct(transform.position, 1000, null);
		seeker.StartPath(cpath, OnPathComplete);
		readyToFlee = false;
	}
}

then when seeker.StartPath(cpath, OnPathComplete) finishes its path calculation, I want to call GetEscapePath.

public override void OnPathComplete(Path p)
{
	base.OnPathComplete(p);
	GetEscapePath(p);
}

Then get escape path is supposed to get some nodes (in this case 10), choose the farthest one from the player, and start moving towards it.

void GetEscapePath(Path p)
{
	ConstantPath cpath = p as ConstantPath; //the problem seems to be here
	List<GraphNode> nodeList = cpath.allNodes;
	List<Vector3> pointList = PathUtilities.GetPointsOnNodes(nodeList, 10, 10);
	Vector3 farthestPoint = GameManager.instance.player.transform.position;
	for (int i = 0; i < pointList.Count; i++)
	{
		if (Vector3.Distance(pointList[i], GameManager.instance.player.transform.position) > Vector3.Distance(farthestPoint, GameManager.instance.player.transform.position))
			farthestPoint = pointList[i];
	}

	seeker.StartPath(transform.position, farthestPoint);

	if (MovedSinceLastFrame(GameManager.instance.player.transform.position))
		readyToFlee = true;
}

my cpath casting seems to be the problem since it’s throwing a null reference exception (my cpath is null), and i’m not sure how else to cast it. I tried also (ConstantPath)p but it didn’t work.
Also what is ConstantPath exactly. I thought “path” is a collection of nodes in a certain order so it can be traversible. ConstantPath finds all the nodes around a position within a given distance, meaning it’s just a list of nodes without any clear path to follow?

In case I don’t need overlaps is the initial build time the only thing that grid graph is better with? I’m not doing much building but an explosive barrel for example may be a reason to recalculate that part of the graph. I thought recast graph is faster in general.

1 Like

Np. That is surprisingly common actually since Aron spelled with just one A is not very common.

Seeker.pathCallback can be set if you want a particular method to be called for every path request that Seeker handles.
If you specify a path callback in the StartPath call (as in Seeker.StartPath(…, OnPathComplete)) then that method will be called as well.
In this case you likely want to bypass the Seeker altogether because you do not want to interrupt the regular path requests that your character does. I would recommend something like this

IEnumerator DetermineMovementMode () {
         ConstantPath cp = ConstantPath.Construct(...);
         AstarPath.StartPath(cp);
         // Wait for the path to be calculated. This may take multiple frames
         // No callback has been specified, so no callback will be called
         yield return StartCoroutine(cp.WaitForPath());
         // Use the result here
 }

A path in the regular sense is a sequence of nodes, the ConstantPath type was added later when the naming convention was already established. It does not calculate a path, but it finds all nodes which can be reached by a path with a cost lower than some constant.

Yes, usually recast graphs are better if the initial build time can be tolerated. There are various other trade-offs because the graphs are represented in very different ways but it would be hard to list them all here.

I don’t have my build with me right now but I’m wondering should I adopt one way to initiate movement?
I have a feeling that using RichAI.UpdatePath is somehow not the best way since it already has one seeker.StartPath which I don’t have any input for?
If yes, what should i replace it with? I looked through the code and it seemed like seeker.StartPath is making sure that this path is called immediately while AstarPath.StartPath only queues the path. In general I’d always want to start the path immediately wouldn’t I?
For example in this case, I’m not sure why would I use the coroutine when I want my A.I. character to start fleeing as soon as the player is in range?

EDIT: I tried your way and it works. Now i’m just trying to understand why that works and mine doesn’t. My code gives this error
“ArgumentException: Path traverses no nodes
Pathfinding.RichPath.Initialize (.Seeker s, Pathfinding.Path p, Boolean mergePartEndpoints, FunnelSimplification simplificationMode) (at Assets/Extrazz/AstarPathfindingProject/Core/AI/RichPath.cs:27)”

when I debug it, in List nodeList = cpath.allNodes, nodeList is always “1” or more. What am I doing wrong?

P.S. I drew some debug sphere’s and turns out the default 2000 Gscore is way too little for my game, would it be too heavy on performance to crank it up 10 or even 50 fold?

Seeker.StartPath is a very thin wrapper around AstarPath.StartPath, they both queue the path.

Because the path calculation might take a frame or so to complete. You can force the path to be calculated immediately by using AstarPath.WaitForPath(path) like

ABPath path = ABPath.Construct(...);
AstarPath.StartPath(path);
AstarPath.WaitForPath(path);
// The path has been calculated now

However if the path is long, this can have a negative impact on the game’s FPS.

A cost of 2000 corresponds roughly to a distance of 2 world units, so increasing that is probably a good idea.

Are you trying to feed a ConstantPath into the RichAI component? A ConstantPath does not have a definite start and end, it is just a collection of nodes, so the RichAI component would get very confused.

1 Like

for example, this code works:

IEnumerator StartEscaping()
{
	ConstantPath cpath = ConstantPath.Construct(gameObject.transform.position, GScore, null);
	AstarPath.StartPath(cpath);
	yield return StartCoroutine(cpath.WaitForPath());
	GetEscapePath(cpath);
}

If I replace “AstarPath.StartPath(cpath)” with “seeker.StartPath(cpath)”, I get the error mentioned before

(ArgumentException: Path traverses no nodes
Pathfinding.RichPath.Initialize (.Seeker s, Pathfinding.Path p, Boolean mergePartEndpoints, FunnelSimplification simplificationMode) (at Assets/Extrazz/AstarPathfindingProject/Core/AI/RichPath.cs:27))

And I don’t understand why that is happening. I looked at the code between seeker.StartPath and AstarPath.StartPath and couldn’t see anything that’d cause this error (because I’m not experienced enough I reckon). It says on AstarPath.StartPath method that you usually would want to call it from seeker rather than directly and I’d like to understand why in this case we’re making that exception. I’m asking all these questions to have a good understanding of the plugin so that I have less random questions later :smiley:

Here’s what I was trying to do:

  1. build a constant path

  2. “start” it with seeker.StartPath so that it’s built

  3. specify a callback in that seeker.StartPath which would call the GetEscapePath method once the path calculation is complete.
    It doesn’t work because of the previous point - seeker.StartPath results in “Path traverses no nodes” while AstarPath.StartPath does the job. If that means that calling StartPath through seeker feeds it into RichAI while calling it with AstarPath doesn’t then yeah, I guess I was trying to feed RichAI a constant path, but I don’t really understand why that is the case. Here’s the code:

    void Flee()
    {
    if (readyToFlee)
    {
    ConstantPath cpath = ConstantPath.Construct(transform.position, GScore, null);
    seeker.StartPath(cpath, GetEscapePath);
    readyToFlee = false;
    }
    }

Then this would in theory call GetEscapePath once the path has been calculated

void GetEscapePath(Path p)
{
	ConstantPath cpath = p as ConstantPath;
	List<GraphNode> nodeList = cpath.allNodes;
	List<Vector3> pointList = PathUtilities.GetPointsOnNodes(nodeList, 25, 10);
	Vector3 farthestPoint = player.transform.position;
	for (int i = 0; i < pointList.Count; i++)
	{
		if (Vector3.Distance(pointList[i], player.transform.position) > Vector3.Distance(farthestPoint, player.transform.position))
			farthestPoint = pointList[i];
	}

	seeker.StartPath(transform.position, farthestPoint);
}

This code still works, the A.I. is running away and all, it just throws that error on every new path creation. Why is that?

1 Like

Hi

The reason is that the RichAI component sets the seeker.pathCallback field. That means it will get notified about every path that the seeker calculates, including your ConstantPath.

1 Like

aha, and if we calculate it with AStar instead, it doesn’t get notified and thus doesn’t get confused. Then we make an alternative to the pathcallback through the coroutine and the day is saved thanks to power puff girls. I think I understand it more or less. Thank you for shedding some light on it Aron.

This thread is just pure gold! Thank you for all these explanations - I am interested in FleePath, and it’s like a blessing that it exists :slight_smile:

Should I specify the tags of the path after I invoke FleePath.Construct() , but before I invoke AstarPath.StartPath()? I would like to use NNConstrain on it, to prevent moving across “myUnwalkableTag”

To me, these two approaches are self-exclusive - how can I then subscribe for a callback to be called? (called as soon as my specific FleePath was computed and ready to be set into my RichAI for traversal)?

And, what is the purpose of the callback inside FleePath.Construct(..., OnPathDelegate)?
Thanks!

Hi

Yeah… That is an annoying design flaw that has unfortunately stuck due to backwards compatibility concerns.
What should happen is that the movement scripts only get called for the paths they start, and not for any other.

However in any case if you want to control the path queries of a movement script, I would recommend setting ai.canSearch = false and control everything yourself from a separate script (including the normal ABPath path requests). I think that will be much easier.

Yes. The Construct method is just like a constructor, except it also uses object pooling to reduce pressure on the GC. Set your tags before calling StartPath and all will be good.

The callback will be called when the path has been calculated. See https://arongranberg.com/astar/docs/callingpathfinding.html

1 Like