Alexey Shimansky
Alexey Shimansky

Reputation: 632

Prevent losing data for EditorWindow in dll when Editor recompiles

I'm making a custom EditorWindow. The very very simplified version looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

namespace testDllForUnity {
    [Serializable]
    public class WindowTestDllForUnity : EditorWindow {
        string myString = "Hello World";
        private static float x = 0;
        private static float y = 0;        
        private static int yOffset = 10;
        private static float width = 100f;
        private static float height = 40f;

        private static List<Rect> buttonsRectList = new List<Rect>();

        [MenuItem("Window/WindowTestDllForUnity")]
        static void Init() {            
            WindowTestDllForUnity window = (WindowTestDllForUnity)EditorWindow.GetWindow(typeof(WindowTestDllForUnity));
            buttonsRectList.Add(new Rect(x, y, width, height));

            window.Show();
        }


        void OnGUI() {
            GUILayout.Label(myString, EditorStyles.boldLabel);
            if (GUILayout.Button("AddButton")) {
                y += height + yOffset;                
                buttonsRectList.Add(new Rect(x, y, width, height));
            }

            for (int i = 0; i < buttonsRectList.Count; i++) {
                if (GUI.Button(new Rect(buttonsRectList[i]), "button_" + i))
                    Debug.Log("button_" + i + " clicked!");                
            }
        }
    }
}

Actually, my window is more complex than that.

This code I built as dll with VisualStudio and put into the Unity's "Assets/Editor/" folder.

It works fine, but... if I add any C# script in Unity, write anything and save it - Unity launch his auto-compilation and my custom window becomes empty, just cleared window.

What should I do to prevent my window clearing? Is it possible to ask Unity not to rebuild window when it's compiling other scripts? Or should I save data every time and restore it after recompiling? Is not this a bad approach?

Upvotes: 1

Views: 4142

Answers (1)

Xarbrough
Xarbrough

Reputation: 1461

When you add, change or remove a script (or DLL) in Unity, all affected assemblies are recompiled and reloaded. When you enter and exit play mode, the assemblies are reloaded as well. This results in all static variables being cleared, because they belong to the loaded assembly and are not saved anywhere else.

To persist changes through assembly reload, Unity needs to be able to save data (your member variables) to disk and then repopulate them after rebuilding everything. Luckily, for a lot of cases, this is done automatically. The EditorWindow base class is already marked as [Serializable], so there is no need to also mark our child class. Next, all variables, which need to be saved, must also be serializable. Simple data types like string and int are serializable, but some types like Rect are not, which is the reason your list will not save, even if it is part of the instance (not static). See the Unity Manual - Script Serialization. Note, that the rules for EditorWindow classes seem to be slightly different than for the serialization of MonoBehaviour classes. For example, private variables are serialized without the [SerializeField] attribute, at least in my Version of Unity 2018.1.0f2.

Try the following code to get a better understanding:

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

namespace PersistChangesThroughAssemblyLoad
{
    public class MyEditorWindow : EditorWindow
    {
        Vector2 buttonStartPosition = new Vector2(0, 0);
        Vector2 buttonStartSize = new Vector2(100f, 40f);
        int yOffset = 10;

        // All data types which should be peristent, need to be
        // serializable, Vector2 is, but Rect is not.
        List<Vector2> buttonPositions = new List<Vector2>();
        List<Vector2> buttonSizes = new List<Vector2>();

        [MenuItem("Window/MyEditorWindow")]
        static void Init()
        {
            MyEditorWindow window = GetWindow<MyEditorWindow>("MyWindow");
            LogMessage(window, "Window will be opened via the menu item.");
            window.Show();
        }

        void OnEnable()
        {
            LogMessage("Window is enabled (either after opening or after assembly reload/recompile).");
        }

        void OnDisable()
        {
            LogMessage("Window is disabled (either when being closed or because the assembly is about to reload/recompile).");
        }

        void OnGUI()
        {
            if (GUILayout.Button("Add Button"))
            {
                buttonStartPosition.y += buttonStartSize.y + yOffset;
                AddNewButton(buttonStartPosition, buttonStartSize);
            }

            for (int i = 0; i < buttonPositions.Count; i++)
            {
                string buttonName = "Button " + i;
                if (GUI.Button(new Rect(buttonPositions[i], buttonSizes[i]), buttonName))
                {
                    LogMessage(buttonName + " was clicked!");
                }
            }
        }

        void AddNewButton(Vector2 position, Vector2 size)
        {
            buttonPositions.Add(position);
            buttonSizes.Add(size);
            LogMessage("Added new button. Total count: " + buttonPositions.Count);
        }

        void LogMessage(string message)
        {
            LogMessage(this, message);
        }

        static void LogMessage(Object context, string message)
        {
            Debug.Log("Window [" + context.GetInstanceID() + "]: " + message);
        }
    }
}

Pay attention to the instance ids logged in the console. When creating a new window from the menu, the id changes, but if the window is serialiable, it survives play mode. This approach should get you a long way, since most data can be broken down into simple data types. It is also convenient to create custom structs and mark them with the [Serializable] attribute like so:

[System.Serializable]
public struct MyRect
{
    public float x, y, width, height;
}

Other than that it is possible to write data to EditorPrefs in OnDisable and load it in OnEnable.

Upvotes: 3

Related Questions