Super weird behavior loading GridGraph from file vs cache, by ScriptableObject and by Monobehavior

I’m building my own Utility AI system from the ground up, and I’ve been getting a lot of weird behavior trying to get my GridGraph to load on scene start and be usable by my ScriptableObject, AIManager. Also I sometimes need the graph in editor mode to bake data, though I haven’t done that recently and I already resolved it by calling FindAstarPath() and forcing a scan before bake.

  1. First, and most importantly, why does the GridGraph clear when I make a script change (to any script) and the editor loads? Is there any way to stop this from clearing the graphs? If the graph would not clear every time a script change is loaded, it would solve this whole headache.

  2. My AIManager is a ScritableObject, so it needs to be able to use the GridGraph at runtime. This is fine if I’m only scanning, since I just call scan in the SO’s OnEnable() and subscribe a callback to OnPostScan(), then I initialize everything when the scan is complete. However there’s no corresponding ability to have a callback run when loading is finished, like OnLoadFromCacheComplete() or OnGraphReady(). How do I asynchronously know when the GridGraph is ready without it being scanned, and without a Start() method? I don’t want to scan on awake since I’m already doing a lot of other setup and am trying to reduce the load time.

Right now I only have two ways to make it work, neither of which is ideal:

  • Have a Monobehavior script attached to some GameObject that calls the AIManager’s initialize() function on Start().
  • Have the ScriptableObject run LoadGraph() from a static helper class I made (code below) in OnEnable(). This slows down the editor pretty significantly, since every time I make a code change, etc the graph is reloaded from its cache (also works from a file and scan).

Please advise on how to resolve this issue. I’ve spent a crazy amount of time trying to wrangle A* to work with this architecture.

public static void LoadGraph(AstarLoadMode loadMode, TextAsset graphData = null)
		{
			AstarPath.FindAstarPath();
			
			if (loadMode == AstarLoadMode.FromFile)
			{
				Debug.Log("Loading AStar Graph From File");
				if (graphData != null)
				{
					AstarPath.active.data.DeserializeGraphs(graphData.bytes);
				}
				else
				{
					Debug.LogError("Attempted to load AStar graph from file but was passed null graphData");
				}
			}
			else if (loadMode == AstarLoadMode.FromCache)
			{
				Debug.Log("Loading AStar Graph From Cache");
				AstarPath.active.data.LoadFromCache();
			}
			else
			{
				Debug.Log("Loading AStar Graph via Scan");
				AstarPath.active.Scan();
			}
		}

Hi

  1. A scanned graph contains a ton of data. Unity simply cannot handle that amount of data using its serialization system (I’ve tried), so it cannot be persisted. The graph settings are deserialized when the AstarPath component runs its Awake function, or in edit mode if you call AstarPath.FindAstarPath().
    When a script is changed unity has to reload the whole C# runtime, the data cannot survive over this domain reload.
  2. That sounds like kind of an odd architectural design to be honest. ScriptableObjects are typically used to store persistent data as an asset, they are typically not used as managers in the game. People usually use MonoBehaviours for that since data for a running game is not persistent.

Calling AstarPath.Scan() is synchronous, the graphs will be scanned immediately after the function call is complete. DeserializeGraphs is also synchronous. If you want to scan the graph asynchronously you can use the AstarPath.ScanAsync coroutine.
If a cache is specified this is loaded synchronously during Awake. Your scripts can safely assume it is loaded in e.g. Start. So I don’t think you’d ever need a OnLoadFromCacheComplete because it’s not a synchronous operation.

Thanks for the quick and thoughtful reply @aron_granberg! For now I’ll just keep using a Monobehavior that calls AIManager on Start(). I’ll think on a better long-term approach in the future, but with limited time outside my day job I’m trying to prioritize this AI system I’m building from scratch, which is pretty non-trivial.

Regarding using ScriptableObjects this way: I hear what you’re saying, my first thought as well was that SOs aren’t for holding instance data. However, in the last few months I’ve been reading a lot about how to make better architecture decisions (ex https://unity.com/how-to/architect-game-code-scriptable-objects). A presenter in this Unite 2017 talk (https://www.youtube.com/watch?v=raQ3iHhE_Kk) described how relying on instance references leads to loads of problems.

For now I’m doing AIAgent spawning using prefabs and a pooling system. I may build out a more sophisticated Factory pattern-based system down the road, but for now its just prefabs. So to reference an AIManager Monobehavior each agent would have to use GameObject.Find() to get the manager, which is expensive and breaks easily. At first glance it might seem like the solution is to have the spawn system register each new unit with the AIManager, however that would require at least one GetComponent() call for each new spawned unit, an expensive call.

So using a SO AIManager avoids all these pitfalls. I asked around on some GameDev Discords and apparently using SOs for system managers is often preferred for these reasons. However if you think there’s a better architecture I should use I’d love to hear it! I do a ton of reading about how to write better code, but for the most part I have to try and figure out what the best approach should be since there isn’t a definitive source with answers to these kinds of practical Unity-specific questions.

With all due respect, I think you are basing your architecture on bad assumptions.

Sure GameObject.Find is super slow, don’t use it. GetComponent is not that slow however, it’s negligible for the startup of each object.

If you want a global manager you could just use a singleton to get a reference very efficiently. Singletons have a pretty bad reputation, but to get a reference to a global manager they are not bad.
You could route the reference via a scriptable object if you want to get rid of the singleton.

@aron_granberg no offense at all, this is super helpful! Thank you! Like I said, since there isn’t a definitive source I have to make a best guess about how to build these kinds of systems better. But without doing a ton of lengthy experiments with the profiler it’s just a guess based on some reading and advice from strangers in Discord.

I was actually thinking about making a Singleton at some point that was just a few references to different system managers like AIManager. One other approach I was thinking of was to have a MangerRunner Monobehavior that held the collection of managers but as ScriptableObjects, only with an OnStart() that the ManagerRunner calls.

Other than lacking a Start() method, what’s the downside to using ScriptableObjects for Manager systems? Any instance data that shouldn’t persist when play stops is cleared.

Thanks so much for your advice and wisdom on this. It’s hard to figure out the best way to build systems on my own, so I really appreciate a master dev like you taking the time to help me learn. :slight_smile:

The downside is generally that scriptable objects are persistent, so logically they should not hold ephemeral runtime data. Even if you make sure to clear it, you can easily get into a weird state if you get an exception at some point which causes things not to be cleared.

@aron_granberg A totally fair point! I’m convinced. I just now started implementing a Singleton for the Managers. I have a Singleton Monobehavior class defined like

Singleton<T> : MonoBehaviour where T : Singleton<T>

And so far it’s working perfectly, I just tried it on my PoolManager, which just inherits from Singleton<PoolManager>.

I have one last question/request for advice. Since I’m trying to keep everything as de-coupled as possible, I’ll probably have a fair number of Managers, maybe 4-6. so far I have PoolManager, SpawnManager, and AIManager. Should I make one GameManager Singleton, which has a reference to each of these smaller system managers? Or I could make a Singleton for each, which would make referencing them a little cleaner, but I’m not sure if its worth having multiple singletons. Since they’re all of a different class it may not technically break the single rule.

I don’t think I’ve seen many (or possibly any) ScriptableObjects in the A* Pathfinding Project. I’d be curious to hear your thoughts on the evangelism for scriptable object-centric architectures. In addition to the one I linked above, there’s another great Unite talk called “Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution” that introduced some MonoBehavior phobia in me.

Thanks again so, so much for your advice. As I said before, I greatly experience a pro such as yourself taking the time.

In this case I’d use a single singleton. It will make it easier if you at some point need to move away from the singleton architecture.

When I started writing this package, I don’t think Unity even had scriptable objects XD.

I’d recommend against this pattern. Inheritance is quite limiting in many ways. Inheriting from Singleton<T> will prevent you from inheriting anything else.