A* Pathfinding: Debugging Special Cases

December 19, 2019

A* Pathfinding

Debugging Special Cases

ISSUES

Getting an Index Out of Bounds Error in EnemyBasicMovement Script

Error happening on this line:

Vector3 direction = (path.lookPoints[0] – transform.position).normalized;

Suggests that the very first point in a path does not exist, so somehow empty paths are being passed through the system.

Test 1: Increase A* Grid Size to Cover Entire Play Area and Spawn Points

Some of the spawn points are a bit off of the normal play area, so I made sure to cover anywhere the units could possibly exist with the A* grid so they always had a node to latch onto (even though I believe the clamping should cover this error). This did not end up being the issue as the error persisted.

Test 2: Increase Size of Walkable Plane Over Entire Play Area

Similar to the logic of Test 1, I just wanted to make sure the nodes were not not being created because the units were missing some usual piece of information they would have to make them. Turns out this was not the issue either.

Test 3: Make Sure Target is Set Before Instantiating the Enemy

I thought maybe a path was being created before the unit had a target, which could make sense for creating a path with size 0. I did this by having the pathing script disabled initially, and making sure the spawner passed in the target information BEFORE it activated the script. The error continued though.

Test 4: Do Not Move the Target to See if Error Exists

It seemed like most spawns didn’t get errors if the player did not move, so I moved the one spawn that basically did not have to move to reach the player’s x position and tested what happened without the player moving (The X position counted as “very close” to the player at this time since I still had some of the issues with converting between 3D and 2D coordinates). There were no errors at all when not moving the target during the game.

DETERMINED ISSUE

Something about the paths updating later in the units’ lives was creating these size 0 paths.

Test 5: Is Spawning Causing the Issue

I was not sure if spawning the units was somehow causing the problems still, so I just placed 4 of the unit prefabs randomly about into the scene initially and stopped the spawners. This did not get rid of the error, and had the units return to moving in big circles still.

Further Info

This was giving me a lot of trouble, so I went back to following the path of logic to see why it would be producing these 0 count paths.

  • OnPathFound method in EnemyBasicMovement is being passed an empty array of Vector3[] waypoints
  • PathRequestManager class calls this method through its own FinishedProcessingPath method
  • OnPathFound is the method assigned to the callback Action for the PathRequest object
  • PathFindingHeap is creating an empty waypoints array somehow

The zero count path is being created in PathFindingHeap somehow. This should be impossible as every path should at the very least have the start node and the target node, which would be a count of 2.

    Within PathFindingHeap

  • RetracePath takes in a startNode and endNode to create the entire path of nodes between them
  • In the problem case, a new path has been created when the target is only a single node away from the pathfinding unit’s current position
  • there is a while loop that runs until it hits the startNode, so in this case, it only runs a single time (since the only nodes are the startNode and the endNode), creating a single waypoint
  • this creates a path with a count of 1
  • then the SimplifyPath method takes in this path
  • SimplifyPath creates a new List of Vector3 to fill with only necessary waypoints
  • it does this by taking points from the passed in path, but it looks through this with a for loop which starts at i = 1 and ends at i < path.Count, because it needs a previous point to compare to since it is doing a directional check
  • since the path.Count is only 1 here, the for loop never runs, and we return an empty set of waypoints
  • Can possibly solve with a special case since a path of 1 node is also a most simplified case

Test 6: Add Case In Simplify Path to Deal with Receiving One Node

I just added a check case before the normal logic that if the input path for SimplifyPath was only 1 node, it would just use that node as the path. This reduces the number of errors significantly, but there are cases where even the base path passed into SimplifyPath has 0 nodes.

DETERMINED ISSUE

The PathFindingHeap RetracePath method is creating paths with 0 nodes when Unit is close to target. As a result, SimplifyPath also makes 0 count paths. The only way this can be the case is if the startNode == endNode, so this must be happening when a new path is being created even though the unit and its target are already considered to be on the same node. This leads me to believe it is an issue with the difference between the node size and the distance the target has to move before looking for a new path (pathUpdateMoveThreshold in EnemyBasicMovement).

Test 7: Reduce NodeRadius to Half (or Less) of Update Path Threshold

Originally the values for pathUpdateMoveThreshold and the nodeRadius were both 0.5, which lead to a overall node size (nodeDiamater) of 1.0. I reduced the nodeRadius to 0.25 (which gives them a total nodeDiameter of 0.5), so this was at or below the threshold.

SOLVED

This completely removed the errors from happening (I add mostly because I am unsure if there exists a case where having the nodeDiameter exactly equal the threshold could cause an error).

SUMMARY

There was a lot of going back and forth to figure out where this error was coming from, and it was even harder since I was having some other issues since I had not fully converted the 3D system to the 2D system. As far as I can tell, this should cover most of the strange cases the pathing should run into even with an updating target (which should mostly be extra for my purposes anyway). I should add a check between the node size and the update threshold to make sure that issue does not happen again.

Coroutine Error Investigation in A* Tutorial Series – Path Smoothing – E09

December 13, 2019

A* Tutorial Series

A* Pathfinding (E09: path smoothing 2/2)

A* Pathfinding (E09: path smoothing 2/2)

Link – Tutorial

By: Sebastian Lague


Intro

I wanted to further my investigation on the issue I had with coroutines when dealing with updating the pathing of the A* units when following the A* tutorial series, specifically in episode 9. I have included the sections “Error with Updating Path” and “Fix for: Error with Updating Path” from my last post to keep them together, along with my updates for the investigation.

Error with Updating Path

For some reason my version was not working properly. The path was updating fine, as I could see with the visualization, but the unit would stop moving and never move toward the target again. I was getting an index out of bounds error, some I have some issue with one of my arrays somewhere, but not sure where. It was running fine in the tutorial, so I must have missed something.

Fix for: Error with Updating Path

I have no idea how this works, but I saw it in the comments of the tutorial. For some reason, in the OnPathFound method in the Unit class, if you pass the IEnumerator FollowPath into the StopCoroutine and StartCoroutine methods as “FollowPath” instead of FollowPath() or using an IEnumerator variable, the updating works.

I think it may have to do with how coroutines are handled in Unity. My guess is that the quotations form is the only one that is properly stopping the correct “FollowPath” coroutine and then starting a new one. The other approaches might not be stopping it properly, which is leading to a weird issue where the coroutines are stacking on top of each other.

My other guess is that somehow these other techniques stop the coroutine but keep track of where it is stopped, and then start back up where they started. This could possibly be what is leading to an index out of bounds issue since a value might not be updated as it should after changing the path.

Testing

The issue is that using StartCoroutine(“FollowPath”) and StopCoroutine(“FollowPath”) works completely fine, but replacing “FollowPath” with FollowPath() or followPathRoutine = FollowPath() does not work. There is some functional difference between these ways of referencing coroutines that I do not see. For matters of testing, I am trying to see if I can get the variable reference method to work (followPathRoutine = FollowPath()).

Test 1

The error was pointing to this line specifically:

while (path.turnBoundaries[pathIndex].HasCrossedLine(position2D))

So this line does make sense for a possible out of bounds index error when looking in the turnBoundaries array. My first test was to check what the pathIndex value is right before this while loop executes to see if it was being passed a weird value at some point. It turned out that I never got a weird pathIndex value when the error occurred (upon moving the target), so the pathIndex was not being changed or anything.

This was actually a bigger issue than I thought because pathIndex is specifically set to 0 at the beginning of this coroutine where this line is located, which indicates to me that the coroutine is not properly being terminated and reset. This supported my thought that maybe the coroutine was resuming immediately within the while loop. However, a more logical issue may be that the path is being updated, but not the pathIndex, so this new path has a different turnBoundaries array where that same pathIndex (since it isn’t properly being reset) gets an out of bounds error.

Test 2

I followed this test up with another quick debug log just outside of the entire while loop containing this while loop (so basically at the initialization of this coroutine) just to see if the coroutine was being executed more than once at all. As I expected, it was not even being restarted at any point while hitting this issue. Since the error is within the coroutine and happening when a new one should be started (but clearly isn’t), this made it fairly obvious something was wrong with how the coroutines were being stopped and started.

Test 3

I decided to split up the functionality to see if I could isolate the issue better. I simply added in an Update method to StopCoroutine when space was pressed to see specifically what just doing that would result in. I started the game and pressing space did indeed stop the unit from moving. I then furthered this test by moving the target now, knowing for sure that the coroutine had been stopped. This did allow the unit to reacquire the target and start moving towards it again. However, if I moved it several times, the error would come back again.

It is also very important to note that pressing space bar again when the unit was moving towards the newly acquired target did NOT stop it anymore. This suggests to me that my coroutine variable lost reference to the specific instance responsible for moving the unit. This would also make sense with the issues that I was having since it tries to start and stop the same exact instance of the coroutine.

Furthermore, my debug log of the path index was still running during these tests, and I could see specifically that the pathIndex was NOT reset when it reacquired the new target and starting moving in this setup. So pressing space stopped the coroutine, stopping the unit, but when it started moving again, the index was not updated. It actually kept the same pathIndex it had before pressing space, but was using it to move still.

The main difference between how it normally runs, and the “press space to stop” version work is that it normally gets a new path, then stops and starts the coroutine, where as the other version stops the coroutine, THEN gets the new path, then starts another coroutine. This difference is allowing the unit to at least somewhat reacquire the target and move again.

Test 4

To more accurately match the case with pressing spacebar since that seemed to work a bit more consistently, I moved the StopCoroutine before updating the path variable. I then also added a debug check here to output what the current number of waypoints were every time the path was updated. This would help confirm the indexing issue since if the number of waypoints were lower that the current pathIndex that does not seem to be updating, it would be pretty clear that this is indeed the issue.

Sure enough this proved my suspicions. I could now move the target some without the pathing breaking, but once I hit a point where a path had less waypoints than the pathIndex, the error came up and it stopped moving. So in this case, the coroutine is clearly not being reset properly.

Test 5: FIX

Since my followRoutine IEnumerator variable seemed to be losing reference to the current coroutine I needed access to, I decided to try and reset it values each time I needed to run a new coroutine. After stopping the coroutine with StopCoroutine(followRoutine), I then set followRoutine = FollowPath() again, before calling StartCoroutine(followRoutine). This approach actually worked perfectly! It was now stopping the correct coroutine instance and creating an entirely new one when starting on its new path. The pathIndex was properly being reset to 0 as that coroutine was properly called from its beginning each time.

Summary

The first big thing is that the different input types for StartCoroutine and StopCoroutine actually act differently just from these inputs. That is very important to know moving forward with those. It also appears that this method of setting an IEnumerator variable can work in these types of cases by making sure to set it equal to the IEnumerator again. This must have something to do with how coroutines can have multiple instances of themselves running, so this helps specificy which instance of the coroutine to stop and start.