Search Unity

  1. The Unity Pro & Visual Studio Professional Bundle gives you the tools you need to develop faster & collaborate more efficiently. Learn more.
    Dismiss Notice
  2. Want more efficiency in your development work? Sign up to receive weekly tech and creative know-how from Unity experts.
    Dismiss Notice
  3. Participate with students all over the world and build projects to teach people. Join now!
    Dismiss Notice
  4. Build games and experiences that can load instantly and without install. Explore the Project Tiny Preview today!
    Dismiss Notice
  5. Improve your Unity skills with a certified instructor in a private, interactive classroom. Watch the overview now.
    Dismiss Notice
  6. Want to see the most recent patch releases? Take a peek at the patch release page.
    Dismiss Notice

Custom Editor Window Progress Bar / Status

Discussion in 'Extensions & OnGUI' started by Stephan-B, May 17, 2013.

  1. Stephan-B

    Stephan-B

    Unity Technologies

    Joined:
    Feb 23, 2011
    Posts:
    2,269
    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 :)
     
  2. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    Why don't you draw another element (progress bar) in the place of the button that's been clicked?

    Progress bar is nothing but the resizable rectangle (plus another rectangle for a background).

    Right from my head:

    Code (csharp):
    1. private Rect _buttonBounds = new Rect(10, 10, 140, 35);
    2. private bool _buttonClicked;
    3. private float _loadingProgress; // decimal number, 0 to 1
    4.  
    5. private GUIStyle _bgStyle;
    6. private GUIStyle _frontStyle;
    7.  
    8. void OnGUI() {
    9.     if (_buttonClicked) {
    10.         GUI.Box(_buttonBounds, _bgStyle); // background
    11.         GUI.Box(new Rect(_buttonBounds.x, _buttonBounds.y, _buttonBounds.width*_loadingProgress, _buttonBounds.height, _frontStyle);
    12.     }
    13.     else {
    14.         if (GUI.Button(_buttonBounds, "Click me")) {
    15.             _buttonClicked = true;
    16.             _loadingProgress = 0; // should be updated by the process
    17.         }
    18.     }
    19. }
     
  3. Stephan-B

    Stephan-B

    Unity Technologies

    Joined:
    Feb 23, 2011
    Posts:
    2,269
    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.
     
  4. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    Unity + multithreading = NO. At least, not for all the platforms.

    What people used to do in the case of performance hit like this is:

    1. 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.

    2. Do the processing in "chunks" (but within the single thread). After each chunk is processed, refresh the UI.
     
  5. Stephan-B

    Stephan-B

    Unity Technologies

    Joined:
    Feb 23, 2011
    Posts:
    2,269
    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.
     
  6. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    Ah, sorry. It's in editor, now I see - I kinda thought this is a runtime...

    So threading might just work in these 3 platforms (PC, Mac, Linux).

    btw you could try this: EditorApplication.update

    This is the application-wise update signal source. You can stick in your own callback with your own timing logic.
     
  7. Stephan-B

    Stephan-B

    Unity Technologies

    Joined:
    Feb 23, 2011
    Posts:
    2,269
    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:

    Code (csharp):
    1.  
    2. void OnEnable()
    3. {
    4.      EditorApplication.update += SomeMethodToHandleCallback;
    5. }
    6.  
    7. void OnDisable()
    8. {
    9.      EditorApplication.update -= SomeMethodToHandleCallback;
    10. }
    11.  
    12.  
    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?
     
  8. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    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. :)
     
  9. Stephan-B

    Stephan-B

    Unity Technologies

    Joined:
    Feb 23, 2011
    Posts:
    2,269
    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.
     
  10. Stephan-B

    Stephan-B

    Unity Technologies

    Joined:
    Feb 23, 2011
    Posts:
    2,269
    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 :)
     
  11. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    Good to know it I would need it someday.. :)
     
  12. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    3,358
    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.
     
  13. deeprest

    deeprest

    Joined:
    Jun 8, 2016
    Posts:
    1
    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)

    Code (CSharp):
    1. public class ProgressUpdateExample : EditorWindow
    2. {
    3.   [MenuItem("Tool/ProgressUpdateExample")]
    4.   static void Init()
    5.   {
    6.     ProgressUpdateExample window = (ProgressUpdateExample)EditorWindow.GetWindow(typeof(ProgressUpdateExample));
    7.     window.Show();
    8.   }
    9.  
    10.   System.Action ProgressUpdate;
    11.   bool processing = false;
    12.   float progress = 0;
    13.   int index=0;
    14.   int length=0;
    15.  
    16.   List<GameObject> gos = new List<GameObject>();
    17.  
    18.   void OnGUI()
    19.   {
    20.     if( processing )
    21.     {
    22.       if( index == length )
    23.       {
    24.         processing = false;
    25.         progress = 1;
    26.         ProgressUpdate = null;
    27.       }
    28.       else
    29.       {
    30.         ProgressUpdate();
    31.         progress = (float)index++ / (float)length;
    32.       }
    33.       // IMPORTANT: while processing, this call "drives" OnGUI to be called repeatedly instead of on-demand.
    34.       Repaint();
    35.     }
    36.  
    37.     EditorGUI.ProgressBar( EditorGUILayout.GetControlRect( false, 30 ), progress, "progress" );
    38.  
    39.     if( GUI.Button( EditorGUILayout.GetControlRect( false, 30 ), "List all Prefabs" ) )
    40.     {
    41.       // gather prefabs into list
    42.       gos.Clear();
    43.       string[] guids = AssetDatabase.FindAssets("t:prefab");
    44.       foreach (string guid in guids)
    45.       {
    46.         GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>( AssetDatabase.GUIDToAssetPath( guid ) );
    47.         gos.Add( prefab );
    48.       }
    49.  
    50.       // initialize progress update
    51.       length = gos.Count;
    52.       index = 0;
    53.       progress = 0;
    54.       processing = true;
    55.       ProgressUpdate = delegate() {
    56.         GameObject go = gos[index];
    57.         Debug.Log("prefab: " + go.name, go );
    58.       };
    59.     }
    60.   }
    61. }