AstarPath.active.data.DeserializeGraphs(textAsset.bytes) very slow

Hi,

I’ve just started working with A* pathfinding V4.2 in Unity 2018.2.12f1. I’m making a large scale game in which my environment is stored in large tile prefabs which are loaded from the Resources folder at run time and dropped when the player passes through certain trigger sin order to keep a minimal amount of active terrain / environment objects loaded at a given time. I’ve extended the AStarPath script a bit to allow generation of a .bytes file that is saved to a resources folder so it can be streamed in at runtime as was done with the environment prefabs.

The issue I’m running into, with a fairly simple navmesh, is that I get a large hang in the editor when I assigned the .bytes property to the AstarPath.active.data.DeserializeGraphs method. If I wait long enough, it finishes and the graph appears in the game and is fully functional, but the hang takes about 30 seconds. which is a total deal breaker, so I’m wondering if this is a to-be-expected limitation of this process or if you have any idea what I’m missing something.

  • The navmesh file I’m loading in this test is 9KB.
  • The mesh is comprised of 582 verts and 336 tris.

Example code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NavMeshTestSpawn : MonoBehaviour {

[ResourcePathField(typeof(TextAsset), "NavMeshes")]
public string m_Navmesh;

void Start () {

}

private IEnumerator LoadNavmesh(string name)
{
	ResourceRequest resourceLoadRequest = Resources.LoadAsync(string.Format("NavMeshes/{0}", name));

	yield return resourceLoadRequest; // wait until ready

	var textAsset = (TextAsset)resourceLoadRequest.asset;
	AstarPath.active.data.DeserializeGraphs(textAsset.bytes); // hang of 30 seconds occurs here
}

void Update () {
	
}

private void OnGUI()
{
	if (GUI.Button( new Rect(100,200,250, 30), "load navmesh")){
		StartCoroutine(LoadNavmesh(m_Navmesh));
	}
}

}

Hi

That seems ridiculously slow. I wouldn’t expect it to take more than maybe tens of milliseconds.
Any chance you could upload the file so that I could take a look at it?

Thanks for the fast response, we’re good, it’s a non issue…

I wrote a script that bypasses your AstarPath UI because I’m using ProBuilder to dynamically place my nav meshes in the editor and I wanted to avoid the step of exporting the ProBuilder objects to a mesh, then re-importing it. The problem is, I wasn’t exporting the mesh at all, I was simply assigning the in-memory instance to the NavMeshGraph.sourceMesh property, then calling MenuScan, SerializeGraphs and SaveToFile. This all appeared to work, but I guess Astar needs an actual mesh asset saved to the Resources directory (as is stated by the error message when you add a mesh to the sourceMesh field that doesn’t reside in the Resources folder).

Here was my old code:

bool GenerateNavMeshGraph(CombineInstance[] sourceMesh, string meshName, string path)
{
	Mesh mesh = new Mesh
	{
		name = meshName
	};

	mesh.CombineMeshes(sourceMesh);

	mesh.RecalculateNormals();

	NavMeshGraph nmgEditor = (NavMeshGraph)AstarPath.active.data.FindGraphOfType(typeof(NavMeshGraph));
	if (nmgEditor != null)
	{
		nmgEditor.sourceMesh = mesh;

		var serializationSettings = Pathfinding.Serialization.SerializeSettings.Settings;
		serializationSettings.nodes = true;
		Pathfinding.AstarPathEditor.MenuScan();

		uint checksum;

		var bytes = AstarPathEditor.mSelf.SerializeGraphs(serializationSettings, out checksum);
		Pathfinding.Serialization.AstarSerializer.SaveToFile(path, bytes);

		AssetDatabase.Refresh();

		EditorUtility.DisplayDialog("Done Saving", "Done saving graph data.", "Ok");
		nmgEditor.sourceMesh = null;

		SaveNavMeshPathsToDisk();
		return true;
	}

	return false;
}

Here’s my new code that works:

bool GenerateNavMeshGraph(CombineInstance[] sourceMesh, string meshName, string path)
{
	string meshPath = string.Format("Assets/Resources/NavMeshes/NewMesh_{0:hhmmssffff}.asset", DateTime.Now);

	if (MeshMergingTool.ExportMergedMesh(sourceMesh, meshName, meshPath))
	{

		NavMeshGraph nmgEditor = (NavMeshGraph)AstarPath.active.data.FindGraphOfType(typeof(NavMeshGraph));
		if (nmgEditor != null)
		{
			nmgEditor.sourceMesh = AssetDatabase.LoadAssetAtPath<Mesh>(meshPath);

			var serializationSettings = Pathfinding.Serialization.SerializeSettings.Settings;
			serializationSettings.nodes = true;
			Pathfinding.AstarPathEditor.MenuScan();

			uint checksum;

			var bytes = AstarPathEditor.mSelf.SerializeGraphs(serializationSettings, out checksum);
			Pathfinding.Serialization.AstarSerializer.SaveToFile(path, bytes);

			AssetDatabase.Refresh();

			EditorUtility.DisplayDialog("Complete", string.Format("The Navmesh was successfully saved to '{0}'", path), "Ok");

			nmgEditor.sourceMesh = null;

			// clean up the mesh sicne it's no longer needed
			AssetDatabase.DeleteAsset(meshPath);
			AssetDatabase.Refresh();

			SaveNavMeshPathsToDisk();

			return true;
		}
		else
		{
			EditorUtility.DisplayDialog("Save Failed", "Could not find the NavMeshGraph editor, make sure the 'Astar Path' component is enabled and expanded.", "Ok");
		}
	}
	else
	{
		EditorUtility.DisplayDialog("Save Failed", "Could not export temporary mesh asset.", "Ok");
	}

	return false;
}
1 Like

Hi

Ah. So the package tries to find the mesh when the graph is deserialized. Your mesh that was not saved to disk probably had an empty name, so when the package tries to find that mesh it tries to load it using Resources.LoadAll("", typeof(Mesh)).

Now the LoadAll is a bit peculiar when passed an empty string. The docs say

Pathname of the target folder. When using the empty string (i.e., “”), the function will load the entire contents of the Resources folder.

So it would try to load the whole Resources folder, which is probably what is taking so long.
If you are the nodes in the serialized file, you don’t need the mesh though. You can just set the mesh field to null after you have scanned the graph, but before you serialize it. When you load the graph later it will work just fine.

Also. I’m not sure why, but you are using some editor calls there. Why not use the ones that don’t require the editor?

AstarPath.active.Scan();

and

AstarPath.active.data.SerializeGraphs(...);

“Also. I’m not sure why, but you are using some editor calls there. Why not use the ones that don’t require the editor?”

I’m not completely following. Are you saying I can avoid this code:

	NavMeshGraph nmgEditor = (NavMeshGraph)AstarPath.active.data.FindGraphOfType(typeof(NavMeshGraph));
	
	var serializationSettings = Pathfinding.Serialization.SerializeSettings.Settings;
	serializationSettings.nodes = true;
	Pathfinding.AstarPathEditor.MenuScan();
	
	var bytes = AstarPathEditor.mSelf.SerializeGraphs(serializationSettings, out checksum);

By using these:

AstarPath.active.Scan();

AstarPath.active.data.SerializeGraphs(…);

If so, I’d love to get that working. The reason I’m doing what I’m doing is because I don’t know how else to create a Navgraph instance from a mesh asset. I’d like to do something more like this if possible:

		var meshAsset = AssetDatabase.LoadAssetAtPath<Mesh>(tempMeshPath);
		NavGraph navGraph = null;
		// todo: populate a navgraph instance with the value of meshAsset 
		AstarPath.active.Scan(navGraph);

		var serializationSettings = Pathfinding.Serialization.SerializeSettings.Settings;
		serializationSettings.nodes = true;

		uint checksum;
		var bytes = AstarPath.active.data.SerializeGraphs(serializationSettings, out checksum);
		Pathfinding.Serialization.AstarSerializer.SaveToFile(path, bytes);

		AssetDatabase.Refresh();

I’m just not sure how to get a NavGraph from a mesh asset without piggy backing on the Editor UI.

Hi

You can do something like this:

var graph = AstarPath.active.data.AddGraph(typeof(NavMeshGraph)) as NavMeshGraph;
graph.sourceMesh = something;
AstarPath.active.Scan();

graph.sourceMesh = null; // Work around the deserialization issue we talked about earlier
var serializationSettings = Pathfinding.Serialization.SerializeSettings.Settings;
serializationSettings.nodes = true;
uint checksum;
var bytes = AstarPath.active.data.SerializeGraphs(serializationSettings, out checksum);
System.IO.File.WriteAllBytes(somePathHere, bytes);

Works like a charm! thanks a million!!

Here’s my final code:

bool GenerateNavMeshGraph(CombineInstance[] sourceMesh, string meshName, string path)
{
	var graph = AstarPath.active.data.AddGraph(typeof(NavMeshGraph)) as NavMeshGraph;
	var tempMesh = new Mesh
	{
		name = meshName
	};

	tempMesh.CombineMeshes(sourceMesh);
	tempMesh.RecalculateNormals();

	graph.sourceMesh = tempMesh;

	AstarPath.active.Scan();

	// important - clear the mesh after Scan and before SerializeGraphs to prevent hang that occurs loading this asset at runtime.
	// see http://forum.arongranberg.com/t/astarpath-active-data-deserializegraphs-textasset-bytes-very-slow/5886/3 for more info.
	graph.sourceMesh = null; 

	var serializationSettings = Pathfinding.Serialization.SerializeSettings.Settings;
	serializationSettings.nodes = true;
	uint checksum;
	var bytes = AstarPath.active.data.SerializeGraphs(serializationSettings, out checksum);
	System.IO.File.WriteAllBytes(path, bytes);

	AssetDatabase.Refresh();

	SaveNavMeshPathsToDisk();

	EditorUtility.DisplayDialog("Complete", string.Format("The Navmesh was successfully saved to '{0}'", path), "Ok");
	return true;
}
1 Like

Hey @ToddW: out of curiosity, how are you handling re-connecting your prefabs at runtime? I also stream room prefabs in and out, and am currently using a graph that moves with the player. It would be really nice if I could save the graph with the prefab and then at runtime load and position it, but this would also require connecting the prefab graphs at door positions (when the doors are open)

I keep a single GameObject in the scene at all times that has the Astar Path script on it and this script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml.Linq;
using System.Linq;

public class PathfindingManager : MonoBehaviour {

static PathfindingManager mSelf;
static List<string> mNavMeshesToLoad = new List<string>();

private void Awake()
{
	mSelf = this;
}

private void OnDestroy()
{
	mSelf = null;
}

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {
	ManageQueue();
}

void ManageQueue()
{
	if (AstarPath.active != null && AstarPath.active.data != null && mNavMeshesToLoad.Count > 0)
	{	
		foreach (string graphName in mNavMeshesToLoad)
		{
			var graph = AstarPath.active.data.FindGraph(x => x.name == graphName);
			if (graph == null)
			{
				mSelf.StartCoroutine(mSelf.LoadNavmesh(graphName));
			}
		}

		mNavMeshesToLoad.Clear();
	}
}

public static void RemovePathfindingGraphs(params string[] graphNames)
{
	if (graphNames != null)
	{
		foreach (var graphName in graphNames)
		{
			// remove it if it has been queued
			if (mNavMeshesToLoad.Contains(graphName)) mNavMeshesToLoad.Remove(graphName);

			if (AstarPath.active != null && AstarPath.active.data != null)
			{
				var graph = AstarPath.active.data.FindGraph(x => x.name == graphName);
				if (graph != null)
				{
					AstarPath.active.data.RemoveGraph(graph);
				}
			}
		}
	}
}

public static void AddPathfindingGraphs(params string[] graphNames)
{
	if (graphNames != null)
	{
		foreach (var graphName in graphNames)
		{
				if (!mNavMeshesToLoad.Contains(graphName)) mNavMeshesToLoad.Add(graphName);
		}
	}		
}

private IEnumerator LoadNavmesh(string graphName)
{
	ResourceRequest resourceLoadRequest = Resources.LoadAsync(string.Format("NavMeshes/{0}", graphName));
	yield return resourceLoadRequest; // wait until ready
	var textAsset = (TextAsset)resourceLoadRequest.asset;
	AstarPath.active.data.DeserializeGraphs(textAsset.bytes);
}

}

All of my Navmeshes are stored as .bytes files under Assets/Resources/NavMeshes. I expose a string[] of graphNames on my area prefabs that allows me to add one or more navmeshes to be tied to the area. then in the awake method of the Area script I pass in the graphNames string[] to PathfindingManager.AddPathfindingGraphs. This queues the graphs to be loaded asynchronously as soon as the AstarPath and PathfindingManager instances have been fully instantiated by the engine.

In the OnDestory method of my area scripts I then pass the graphNames array into PathfindingManager.RemovePathfindingGraphs, so the navmeshes that are out of range are unloaded.

1 Like

Oh awesome! That’s pretty much how I planned on doing it, I’m glad to hear that it is feasible, thanks for sharing the code

I was able to create some type of system based on this for my purposes, where I define a bounds and then auto generate a graph based on this, serializing it into a prefab / scriptable object asset containing a TextAsset. It would be nice if graphs were able to be edited even when not in the A* pathfinding object, to work better with prefabs. I suppose I will have to write my own system to do that

Thanks!