- A* version: [5.3.0] (purchased version of A* Pathfinding Project Pro)
- Unity version: [6000.0.32f1]
- Burst version: [1.8.18]
- Collections: [2.5.1]
Having upgraded from [5.2.5] to [5.3.0], I am seeing some performance deterioration issues, seemingly related to Navmesh Cut.
On playing my game scene after the upgrade, this was immediately noticeable, with large stutters happening when agents enabled and disabled their Navmesh Cut components. In [5.2.5], these same events worked very smoothly and had made no noticeable performance impact.
After attempting some profiling comparisons, it appears the worst offending area is “Tile Initialization”. This table shows the top 5 areas with the largest time difference.
AstarPath.Update / Work Items | ver 5.2.5 | ver 5.3.0 | Time diff ms | ||
---|---|---|---|---|---|
GC Alloc | Time ms | GC Alloc | Time ms | ||
Tile Initialization | 256 B | 0.03 | 110.2 KB | 4.43 | 4.4 |
Connect With Neighbours | 0 B | 0.02 | 0 B | 3.41 | 3.39 |
Create Node Connections | 0 B | 0.01 | 0 B | 2.41 | 2.4 |
Collect navmesh cuts | 216 B | 0.02 | 93.0 KB | 2.01 | 1.99 |
JobHandle.complete | 0 B | 0.5 | 0 B | 0.86 | 0.36 |
Garbage collection allocation also seems much higher in “Tile Initialization” and “Collect navmesh cuts”.
I imagine this would be easy for others to recreate, as all you need is a recast graph and a gameobject with the Navmesh Cut component.
I will share my test setup here though for completeness.
To make sure this was not caused by my own code library or other Assets, I started a new blank Unity project for these tests.
Steps to recreate:
Create a New Scene.
Ensure Astar Path [5.2.5] is imported.
Add a Plane at (0, 0 ,0) with scale (20, 1, 20)
Add an empty GameObject at (0, 0 ,0) with the AStarPath component and a single Recast Graph with Size (200, 3, 200)
Add an empty GameObject at (0, 1 ,0) called “Agent” with a Seeker, Character Controller and Funnel Modifier components
Other changed Recast Graph settings:
Agent / Character Radius = 0.6
Rasterization / Voxel Size = 0.3
Rasterization / Use Tiles / Tile Size = 32
Rasterization / Min Regios Size = 6
Other changed AStarPath settings:
Pathfinding / Thread Count = None
Debug / Path Logging = Only Errors
Other changed Seeker settings:
Start Point Snapping = Original
End Point Snapping = Original
Add this new script “AStarBounce.cs” to the project and attach to the Agent:
using UnityEngine;
using Pathfinding;
using System.Collections;
using Unity.Profiling;
public class AstarBounce : MonoBehaviour
{
public bool useCutter = true;
public float speed = 20;
public float nextWaypointDistance = 2;
public float repathRate = 0.5f;
static readonly ProfilerMarker s_PerfMarker = new ProfilerMarker("EnableNavmeshCut");
private Vector3? targetPosition = null;
private Vector3 targetPositionN, targetPositionS;
private bool headNorth = true;
private Seeker seeker;
private CharacterController controller;
private NavmeshCut navmeshCut;
private Path path = null;
private int currentWaypoint = 0;
private float lastRepath = float.NegativeInfinity;
private bool reachedEndOfPath = false;
private bool isWaiting = false;
public void Awake()
{
seeker = GetComponent<Seeker>();
controller = GetComponent<CharacterController>();
navmeshCut = GetComponent<NavmeshCut>();
navmeshCut.enabled = useCutter;
targetPositionS = transform.position;
targetPositionN = new Vector3(transform.position.x, transform.position.y, transform.position.z + 12f);
}
public void OnPathComplete(Path p)
{
p.Claim(this);
if (!p.error)
{
if (path != null) path.Release(this);
path = p;
// Reset the waypoint counter so that we start to move towards the first point in the path
currentWaypoint = 0;
using (s_PerfMarker.Auto())
{
navmeshCut.enabled = false;
}
}
else
p.Release(this);
}
public void Update()
{
if (isWaiting || AstarPath.active.isScanning)
return;
if (reachedEndOfPath || !targetPosition.HasValue)
{
if (useCutter)
navmeshCut.enabled = true;
StartCoroutine(WaitAndSetNewTarget());
return;
}
if (Time.time > lastRepath + repathRate && seeker.IsDone())
{
lastRepath = Time.time;
seeker.StartPath(transform.position, targetPosition.Value, OnPathComplete);
}
if (path == null)
{
return; // We have no path to follow yet, so don't do anything
}
// The distance to the next waypoint in the path
float distanceToWaypoint;
while (true)
{
distanceToWaypoint = Vector3.Distance(transform.position, path.vectorPath[currentWaypoint]);
if (distanceToWaypoint < nextWaypointDistance)
{
if (currentWaypoint + 1 < path.vectorPath.Count)
{
currentWaypoint++;
}
else
{
reachedEndOfPath = true;
break;
}
}
else
{
break;
}
}
// Direction to the next waypoint. Normalize it so that it has a length of 1 world unit
Vector3 dir = (path.vectorPath[currentWaypoint] - transform.position).normalized;
// Multiply the direction by our desired speed to get a velocity
Vector3 velocity = dir * speed;
// Move the agent using the CharacterController component
controller.SimpleMove(velocity);
}
private IEnumerator WaitAndSetNewTarget()
{
isWaiting = true;
yield return new WaitForSeconds(2);
path = null;
reachedEndOfPath = false;
if (headNorth)
targetPosition = targetPositionN;
else
targetPosition = targetPositionS;
headNorth = !headNorth;
isWaiting = false;
}
}
Play the scene with Profiling enabled. The Agent should just:
- Enable its Navmesh Cut component. This also registers a ProfilerMarker called “EnableNavmeshCut”.
- Wait 2 seconds, then Pathfind.
- On finding a path, disable the Navmesh Cut component and move along the Z Axis a short distance
- On reaching target, repeat from step one, but will then move in the opposite direction
To minimise impact on the profiler from the editor/scene view:
- Set Show Graph disabled
- Have no items in scene selected
In the profiler, search for the EnableNavmeshCut marker. After this frame, from roughly two frames you can see some activity under the AStar path workitems, from which the profile tables above are derived.
Repeat the same test again, but remove AstarPath and import [5.3.0].
Profile for [5.2.5]
Profile for [5.3.0]
Let me know if you need more info.