I have a custom Editor Window with several buttons. One of those buttons starts a fairly long process which could take over a minute to complete. In order to provide feedback to the user, I want to add a progress bar / indicator below this button (not in a separate window but in this same custom editor window).
If I could use a coroutine to start this process and then simply Repaint() the Editor Window that would be simple but since this is an Editor Window and this long process is not a monobehaviour script plus given this is an editor tool, I don’t even have any game objects in the scene…
I realize I could use EditorUtility.DisplayProgressBar() but as I stated, I don’t want a separate windows. How should I go about accomplishing this?
In terms of round about ways … I guess, as the user hits this button, I could create a gameobject to which I would attach a monobehaviour script from which I could launch this process which would free the OnGUI of the editor window to allow me to update progress bar / texture… but again that seems like a round about way …
or maybe … I am way over thinking this and failing to see the simple solution … if that is the case. Please enlighten me
The drawing is not the issue. The lengthy process that begins as the button is clicked (which can take up to a minute) causes the OnGUI() to be halted for that period of time.
From this lengthy process, I can create a new window or use EditorUtility.DisplayProgressBar() but I am unable to update the original window since it’s OnGUI() is locked up until this process terminates and returns whatever value (a texture in this case).
So I’ve been reading up in Threading which I guess would work… I am assuming that I could have a new thread launched when the button is clicked to perform my calculations which would leave the window’s OnGUI() free to be updated as the progress is made and eventually once the results are in.
I was hoping for a simpler solution that would not require threading.
Unity + multithreading = NO. At least, not for all the platforms.
What people used to do in the case of performance hit like this is:
Create the “Processing…” label, be sure it is displayed in the first place and then start the lengthy process. Don’t mind updating the label until the process is finished.
Do the processing in “chunks” (but within the single thread). After each chunk is processed, refresh the UI.
My understanding on Multi-threading with Unity is that as long as the other threads are used for non Unity stuff (computations of path for instance, or particle flow / flocking, modifying a large mesh, background tasks like activities) that it is fine. Whatever results of the other threads, can then be feed back into the main Unity thread.
BTW: Coroutines would have been great for what I needed but given Yield WaitForSeconds() doesn’t get updates in Edit Mode, you can’t use that.
Since I am working on an Editor Tool, until people start to develop games on mobile devices, I should be fine with using multi-threading
In terms of EditorApplication.update, or other callbacks (I haven’t used any of those yet)… do I presume you simply register for those callbacks like you would of your own delegates? So for instance:
Now in terms of those, since the main thread is locked up, I still need to start a new thread for the long process and then dispatch an event in that process which would be handled by that callback… Correct?
No, I’ve been suggesting the asynchronous approach using the EditorApplication.update approach instead of the “Yield WaitForSeconds()”, without the yield return.
Inside the handler you could measure time and do stuff if a certain amount of time passed.
However, this doesn’t solve your “frozen UI” problem.
I just experimented with having the button call a method that starts a new thread which simply sleeps for 5 seconds and then terminates to see if the OnGUI() will keep updating during that 5 second interval… and it does as expected
Now I need to learn more about Multi-threading and modify my long process to be able to run in a separate thread.
I need to also figure out what UnityEngine stuff can be used outside the main thread. Is there a list of that somewhere?
I know I can create Vectors outside the main thread but Texture2D or using the Time class doesn’t work.
It’s all working nice with Multi-threading ThreadPools to be more specific.
When the user clicks the button, a ThreadPool.QueueUserWorkItem starts… it begins preparing stuff … two other ThreadPool.QueueUserWorkItem are started to do the real number crunching … while the previous one waits for the results. Once they are in, it combines the results and fires an event which contains the data before terminating. Works great and the progress bar keeps on updating in the editor window
Unfortunately multi-threading doesn’t work if you need to access, modify, or create any of Unity’s objects except structs (Vector3, etc) and value types. So if your process needs access to any of those types, you’re out of luck. In my case, I wanted to add a progress bar while texture atlases are being created. This process involves AssetDatabase calls, loading ScriptableObjects, modifying Texture2Ds, etc. None of this works outside the main thread. So if you are doing work with Unity data types, you’re pretty much stuck either using Coroutines or structuring your process so it can work on it in small pieces per update cycle and update the GUI in between.
For anyone who finds themselves here in the future:
Do the work within a delegate and call it once per call to OnGUI().
Be sure to force OnGUI() to update repeatedly by calling Repaint().
This example lists all prefabs in the project while fluidly updating the progress bar.
(Tested in Unity 2017.3.0f3)
public class ProgressUpdateExample : EditorWindow
{
[MenuItem("Tool/ProgressUpdateExample")]
static void Init()
{
ProgressUpdateExample window = (ProgressUpdateExample)EditorWindow.GetWindow(typeof(ProgressUpdateExample));
window.Show();
}
System.Action ProgressUpdate;
bool processing = false;
float progress = 0;
int index=0;
int length=0;
List<GameObject> gos = new List<GameObject>();
void OnGUI()
{
if( processing )
{
if( index == length )
{
processing = false;
progress = 1;
ProgressUpdate = null;
}
else
{
ProgressUpdate();
progress = (float)index++ / (float)length;
}
// IMPORTANT: while processing, this call "drives" OnGUI to be called repeatedly instead of on-demand.
Repaint();
}
EditorGUI.ProgressBar( EditorGUILayout.GetControlRect( false, 30 ), progress, "progress" );
if( GUI.Button( EditorGUILayout.GetControlRect( false, 30 ), "List all Prefabs" ) )
{
// gather prefabs into list
gos.Clear();
string[] guids = AssetDatabase.FindAssets("t:prefab");
foreach (string guid in guids)
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>( AssetDatabase.GUIDToAssetPath( guid ) );
gos.Add( prefab );
}
// initialize progress update
length = gos.Count;
index = 0;
progress = 0;
processing = true;
ProgressUpdate = delegate() {
GameObject go = gos[index];
Debug.Log("prefab: " + go.name, go );
};
}
}
}