Re-compiling scripts at runtime causes pathfinding grid to be deleted

I have a layered grid graph in my scene. If I update any one of my scripts at runtime, the graph is destroyed as soon as my scripts have finished compiling. I immediately see the text “No AstarPath object found in the scene. Make sure there is one or do not create paths in Awake” printed in the Console, and then all seekers stop pathfinding. There is indeed still an AstarPath object in the scene, but the graph that was created with it has vanished.

Is this a known issue? What could be the possible problem or solution?

P.S. - I’ve confirmed that this happens even in the Layered Grid Graph example scene, which I have left completely untouched, so it doesn’t seem to be caused by any of my scripts.

This happens to all data that Unity can’t serialize automatically. I’m surprised you’ve managed to have your own scripts work with runtime recompilation.

I’ve got automatic recompilation turned off when in play mode, so I can’t check, but it might be that things will work if you use the startup cached stuff (under Save & Load). Not sure, though.

Yes, I’ve always been able to recompile scripts at runtime and keep the game stable without problems - except in the case of projects using A* pathfinding.

Unfortunately, trying to use the startup cache stuff didn’t work. The Console prints out things like “Caught exception while deserializing data” and “Failed to deserialize graphs” once the scene loads, even if I don’t try to recompile at runtime.

I’m hoping that Aaron will be back sometime soon to provide some input…

Hi

Unity’s serialization works for simple things. Unfortunately it isn’t able to serialize all the data required for pathfinding as it doesn’t (among other things) support references (without a huge amount of memory overhead). Recompiling during runtime in moderately complex games in Unity is unfortunately usually not possible due to the limitations of the serialization system.

Ah, that’s a real shame! I’m super bummed out about that.

Is there a solution / workaround? Is there a way to “save” the pathfinding grid at runtime, right before a recompile, and then load it after the scripts have recompiled?

Hi

Saving the graph is possible, however it will not help much because you will still loose a bunch of other information. Like for example the path an agent was following before the recompile and any current path requests.

An agent losing their path and having to recalculate isn’t a showstopper. As long as agents can just calculate a new path after the graph has been saved and loaded, it won’t present a problem.

What function do I need to call in order to save a graph and then restore it after recompiling scripts?

You can take a look at this page: https://arongranberg.com/astar/docs/save-load-graphs.php
I’m not quite sure where you would do it though, you will have to check the Unity documentation.

I set up something really simple just to test it out:

byte[] bytes;

void Update ()
{		
     if (Input.GetKeyDown("left"))
     {
	bytes = AstarPath.active.astarData.SerializeGraphs ();
     }

     if (Input.GetKeyDown("right"))
     {
	AstarPath.active.astarData.DeserializeGraphs (bytes);
     }
}

I tap the left arrow key, then recompile code, then tap the right arrow key. It works - the destroyed Layered Grid Graph comes back. However, agents don’t resume pathfinding. Even when I create new agents, they still don’t do any pathfinding.

Sorry for asking to be spoonfed here, but what am I supposed to do to get the agents pathfinding again once the grid has been re-created?

Maybe their coroutine for recalculating paths has been stopped? I honestly don’t know.

I was missing a step. After AstarPath.active.astarData.DeserializeGraphs (bytes); I needed to run AstarPath.active.Scan() to actually scan the grid. I confirmed that the grid was being re-created with 100% accuracy. Unfortunately, agents are still not moving.

I put Debug.Log(“Test”) into TrySearchPath() and confirmed that it’s still running on every frame. So, the coroutine didn’t stop. However, agents still don’t move.

From here, I couldn’t make any further progress trying to debug it. I can confirm which functions are successfully being called (Start, Init, RepeatTrySearchPath, TrySearchPath, SearchPath, GetFeetPosition, and seeker’s StartPath) without errors. But, where is the part of the script that actually makes it move? I can’t figure out what calls MovementUpdate(). The AIPath script doesn’t seem to have an Update or FixedUpdate function.

If a graph is existing and scanned, and a seeker is standing still, what part of the code is hitting a wall?

(I checked CanSearch and CanMove, and yes, they are set to true.)

Ah, but the AIPath script inherits from AIBase which does have those methods.

I’m not sure what part of the code would fail unfortunately :confused:

Hello!

I’m not ready to give up on run time recompilation - it is such a time saver! Granted I am new to Unity so maybe I have too high hopes…

So I continued building on YndereDevs code, and got this, which does the trick - at least for my project!

It will

  • serialize the graphs automatically instead of on button presses
  • if a recompile was detected, awake the Pathfinder component, preparing it to be used by agents and performing the scan
  • awake any Seeker component, initializing its StartEndModifier
  • reinit any class based on AIBase and cancel any previously ongoing path

Usage

  • add this script as a component to the same game object as the Pathfinder
  • in the Pathfinder settings, choose Thread Count = None. This will run the path finding in a Coroutine, avoiding additional thread issues
  • notice that sometime agents will try to walk somewhere before this scripts had a chance to wake up the pathfinder object. This will result in errors in the debug log. But just click the pause button to continue running the game anyway, and it will be reinitialized

Only tested with grid graph and ~5 agents…

using Pathfinding;
using System;
using System.Reflection;
using UnityEngine;

[Serializable]
public class AIPathSerializer : MonoBehaviour, ISerializationCallbackReceiver
{
    private byte[] bytes;
    private bool recompiled = false;

    // Start is called before the first frame update
    void OnEnable()
    {
        // The first time the game starts, Awake will be called by Unity, so no need to do anything here
        if (recompiled)
        {
            print("AIPathSerializer is awaking all path finders");

            // Start the path finder singleton, which also scans the scene
            AstarPath path = GetComponent<AstarPath>();
            MethodInfo awake = typeof(AstarPath).GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance);
            awake.Invoke(path, null);

            // Ensure that the seekers have their post processes set up
            awake = typeof(Seeker).GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance);
            Seeker[] seekers = GameObject.FindObjectsOfType<Seeker>();
            foreach (Seeker seeker in seekers)
                awake.Invoke(seeker, null);

            // Ensure that the AI knows to restart any ongoing path
            awake = typeof(AIBase).GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance);
            MethodInfo cancelCurrentPathRequest 
                = typeof(AIBase).GetMethod("CancelCurrentPathRequest", BindingFlags.NonPublic | BindingFlags.Instance);
            AIBase[] ais = GameObject.FindObjectsOfType<AIBase>();
            foreach (AIBase ai in ais)
            {
                print("Awaking " + ai);
                awake.Invoke(ai, null);
                cancelCurrentPathRequest.Invoke(ai, null);
            }
        }
        // Any subbsequent OnEnable from here will be a recompilation
        recompiled = true;
    }

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {   
        // Serialize graphs, but only if the are not already serialized
        if (bytes == null)
        {
            bytes = AstarPath.active.data.SerializeGraphs();
        }
    }

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
        // Deserialize graphs, but only if we have something to work on
        if (bytes != null)
        {
            if (AstarPath.active)
            {
                AstarPath.active.data.DeserializeGraphs(bytes);
            }
        }
    }
}

Oh, this is fantastic!! Your code worked perfectly!!

Thank you so much, KattenMedHatten!! You’re a lifesaver!!

1 Like