How to use Unity’s live recompile

When you’re running your project in Unity and you edit and save your source code, the default behaviour of Unity is to stop, recompile and continue playing the project. Most of the time it throws a whole bunch of errors, fails and you simply restart the project and keep playing. Most people turn this feature off (Menu Unity->Preferences->General->Script Changes While Playing->Stop Playing and Recompile).  The fact is almost nobody uses this feature or even talks about it.  Information about how to use live recompile is scarce.   Coming from Visual Studio’s C++/C# with its edit and continue I’m quite used to the concept and it’s hard one to give up,  so I spent a lot of time trying figure out how to use it in Unity.  The ability to code while your game is running is a powerful one, especially when you are trying to fix an issue that is difficult to reproduce or takes some time to get too. It also gives you immediate feedback and saves you the time of testing and reproducing it again.  Here is my guide on how I do it.

 

How a live recompile works

  1. You modify a script and save it while the project is in ‘play’ mode
  2. Unity disables all active components triggering OnDisable
  3. Serializes all serializable gameobjects and their components
  4. Begins a live recompile (Reloading script assemblies)
  5. Set’s all static global variables and non serialized data structures to the declared state (often null)
  6. Triggers OnEnable on all previously active/enabled components
  7. Resumes playing

The result is all your static (globals) and non serialized variables now crash your project when they are accessed because they have become invalid (null)

Non serialized data structures like dictionaries, linkedLists, hashsets ect.. will be null or set to their declared values. Arrays, lists and all the basics like Vector3, Quaternions are work fine.

To use live recompile successfully

  1. Re-assign globals (such as singleton instances) in OnEnable
  2. Define all structs as [System.Serializable]
  3. Serialize and de-serialize all static and non serializable data variables in OnDisable and OnEnable respectively

Avoiding statics and non serialized collections will save you the most effort, all that’s really needed is to re-assign singleton classes and define all structs as serialized.

Be aware that OnEnable is called every time a component is called before it starts and enabled, so never assume it’s because of a live recompile, same goes for OnDisable

How to tell you are in a live recompile

In most cases (if your script is never disabled) simply null checking your globals and data is enough to catch and recover from a live recompile, but in some cases it’s important to distinguish a live recompile from a normal activation/deactivation event. The best way to tell if a live recompile is occurring is to check if EditorApplication.isUpdating is true in both your OnEnable/OnDisable.

private void OnDisable()
{
    #if UNITY_EDITOR
    if(EditorApplication.isUpdating)
    {
        //cache non serialized data
    }
    #endif
}
private void OnEnable()
{
    #if UNITY_EDITOR
    if (EditorApplication.isUpdating)
    {
        //re-initialize globals and non serialized data structures
    }
    #endif
}

An alternative method for catching a live recompile after it has completed is to use a callback attribute ‘DidReloadScripts’ (note this method will be called every time scripts are reloaded even when not in play mode)

#if UNITY_EDITOR
   [UnityEditor.Callbacks.DidReloadScripts]
   private static void OnScriptsReloaded()
   {
       if (Application.isPlaying)
       {
           // Compilation is complete re-initialize globals and data structures
       }
   }
#endif

Examples: Live Recompile Fail and Pass

Here is an example of a script with all the common issues that will fail a live recompile followed by an example that fixes all the issues and passes it

using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
 
public class LiveRecompileFail: MonoBehaviour
{
    // Will pass a live recompile without issue
    public string myString = "hello";
    public string[] myStringArray;
    public List<int> mylist = null;
 
    // Will fail a live recompile because objects are either static or non serializable data structures
    static LiveRecompileFail instance = null;
    static int numberOfEnables = 0;
    public struct aStruct {  public string s;   public int i; }
    public aStruct myStruct;
    public HashSet<int> myHashSet = new HashSet<int> { 5, 4, 3, 2, 1 };
    public Dictionary<string, int> myDictionary = null;
 
    void Awake()
    {
        Debug.Log("LIVE_RECOMPILE START");
        if (instance == null)
            instance = this;
 
        myStringArray = new string[] { "one", "two", "three" };
        mylist = new List<int> { 1, 2, 3, 4, 5 };
 
        myStruct = new aStruct() { s = "struct", i = 99 };
        myHashSet = new HashSet<int>(mylist); // modify our hashset
        myDictionary = new Dictionary<string, int> { { "one", 1 }, { "two", 2 } };
    }
 
    string ParseToString(object obj) => JsonConvert.SerializeObject(obj);
 
    void PrintStatus()
    {
        string myObjectString = "";
        myObjectString += "instance " + (instance == null?"null":instance.ToString()) +"\n";
        myObjectString += "numberOfEnables " + ParseToString(numberOfEnables) +"\n";
        myObjectString += "myStringArray " + ParseToString(myStringArray) +"\n";
        myObjectString += "mylist " +  ParseToString(mylist) +"\n";
        myObjectString += "myStruct " +  ParseToString(myStruct) +"\n";
        myObjectString += "myDictionary " + ParseToString(myDictionary) +"\n";
        myObjectString += "myHashSet " + ParseToString(myHashSet) +"\n";
        Debug.Log(myObjectString);
    }
 
    private void OnEnable()
    {
        Debug.Log("LIVE_RECOMPILE ONENABLE");
        numberOfEnables++;
        PrintStatus();
    }
}

The result of a live recompile will be:

instance null (fail)
numberOfEnables 1 (fail, should be 2)
myStringArray [“one”,”two”,”three”] (pass)
mylist [1,2,3,4,5] (pass)
myStruct {“s”:null,”i”:0} (fail)
myDictionary null (fail)
myHashSet [5,4,3,2,1] (fail, resets to the declared state)


Here is how to fix the above example and make it work in live recompile. Note I use json for ease of use to serialize the objects into a list of strings but you can use your own methods.

using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
 
public class LiveRecompilePass : MonoBehaviour
{
    // Will pass a live recompile without issue
    public string myString = "hello";
    public string[] myStringArray;
    public List<int> mylist = null;
 
    // Will now pass a live recompile with proper definition and serialization
    static LiveRecompilePass instance = null;
    static int numberOfEnables = 0;
   //define as serializable
    [System.Serializable] public struct aStruct {  public string s;   public int i; }
    public aStruct myStruct;
    public HashSet<int> myHashSet = new HashSet<int> { 5, 4, 3, 2, 1 };
    public Dictionary<string, int> myDictionary = null;
 
    List<string> liveRecompileSave;
 
    void Awake()
    {
        Debug.Log("LIVE_RECOMPILE START ");
        if (instance == null)
            instance = this;
 
        myStringArray = new string[] { "one", "two", "three" };
        mylist = new List<int> { 1, 2, 3, 4, 5 };
 
        myStruct = new aStruct() { s = "struct", i = 99 };
        myHashSet = new HashSet<int>(mylist); // modify our hashset
        myDictionary = new Dictionary<string, int> { { "one", 1 }, { "two", 2 } };
    }
 
    string ParseToString(object obj) => JsonConvert.SerializeObject(obj);
 
    void PrintStatus()
    {
        string myObjectString = "";
        myObjectString += "instance " + (instance == null?"null":instance.ToString()) +"\n";
        myObjectString += "numberOfEnables " + ParseToString(numberOfEnables) +"\n";
        myObjectString += "myStringArray " + ParseToString(myStringArray) +"\n";
        myObjectString += "mylist " +  ParseToString(mylist) +"\n";
        myObjectString += "myStruct " +  ParseToString(myStruct) +"\n";
        myObjectString += "myDictionary " + ParseToString(myDictionary) +"\n";
        myObjectString += "myHashSet " + ParseToString(myHashSet) +"\n";
        Debug.Log(myObjectString);
    }
 
    private void OnEnable()
    {
        Debug.Log("LIVE_RECOMPILE ONENABLE");
        if (instance == null) //re-assign the instance variable
        {
            instance = this;
            if (liveRecompileSave != null)
            {
                int index = 0;
                numberOfEnables = JsonConvert.DeserializeObject<int>(liveRecompileSave[index++]);
                myDictionary = JsonConvert.DeserializeObject<Dictionary<string, int>>(liveRecompileSave[index++]);
                myHashSet = JsonConvert.DeserializeObject<HashSet<int>>(liveRecompileSave[index++]);
                liveRecompileSave = null;
            }
        }
 
        numberOfEnables++;
        PrintStatus();
    }
 
    private void OnDisable()
    {
        Debug.Log("LIVE_RECOMPILE ONDISABLE");
        if(Application.isEditor)
            // store non serializable data as json strings assuming a live recompile is occurring
            liveRecompileSave = new List<string> { ParseToString(numberOfEnables), ParseToString(myDictionary),ParseToString(myHashSet)};
    }
}

The successful result will be:

instance GameObject (LiveRecompilePass)
numberOfEnables 2
myStringArray [“one”,”two”,”three”]
mylist [1,2,3,4,5]
myStruct {“s”:”struct”,”i”:99}
myDictionary{“one”:1,”two”:2}
myHashSet [1,2,3,4,5]


The amount of work to support live recompile is small if you start early and you keep your project simple, don’t use to many statics and non serialized collections and maintain all your scripts, but the benefit of coding in real time with your project in play mode is huge. Especially in situations where you are trying to tweak or debug parts of your project that takes time to get to and makes reloading tedious.

Also to note, some assets don’t support live recompile well or at all.

Dotween Live Recompile

Dotween is a common asset that runs after a live recompile, but every time a recompile occurs the tweens run faster because of some internal timing issue, my fix is to re-calibrate Dotween timescale:

void OnEnable()
{
        if (Application.isEditor)
        {
            float someValue = 0f;
            float startTime = Time.time;
            DOTween.To(() => someValue, x => someValue = x, 1f, 1f).OnComplete(() =>
            {
                float timeScale = Time.time - startTime;
                Debug.Log($"Dotween TimeScale: {Time.time - startTime}");
                if (timeScale < 0.9f)
                    DOTween.timeScale = timeScale;
            });
        }
    }
}

A* Project Live Recompile

A* Project is a popular asset that does not support live recompile out of the box but it can with this simple script attached to the Pathfinding object. Important: you must set the run time order of this script before A*Project in Project Settings -> Script Execution Order

using UnityEngine;
using Pathfinding.Serialization;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class AStarLiveRecompile : MonoBehaviour
{
    public AstarPath aStar;
#if UNITY_EDITOR
    byte[] aStarData = null;
 
    private void OnEnable()
    {
        if(aStar == null)
            aStar = GetComponent<AstarPath>();
 
        if (aStar != null && aStarData != null && EditorApplication.isUpdating == true)
        {
            AstarPath.active = aStar; //reset the global instance of AstarPath
            aStar.data.DeserializeGraphs(aStarData);
        }
    }
 
    private void OnDisable()
    {
        if(aStar != null && EditorApplication.isUpdating == true)
            aStarData =  aStar.data.SerializeGraphs(new SerializeSettings() { nodes=true,editorSettings=true });
    }
#endif