Search Unity

Performance issue with GUILabels and ScrollView

Discussion in 'Scripting' started by Alan47, Feb 3, 2013.

  1. Alan47

    Alan47

    Joined:
    Mar 5, 2011
    Posts:
    163
    Hello everyone,

    I've got a problem with a Scroll View and GUILabels contained within it. I'm using this scroll view as a live logger of what's happening in the game, and everytime something happens, a new line is added to the log. Since I'm using a 4-times overdraw for the text to create a black outline around the text, along with some other tricks which also increase the label count, the total label count quickly becomes pretty large, such that it has a considerable impact on the CPU load and brings down the framerate of the game below 20fps, even if nothing else is going on in the game that would require notable CPU power.

    Now, the thing is, the part of the log which is actually visible on screen is rather small: I'm currently only displaying about 10 lines at a time. This window can be controlled by the player by using the Scroll View slider, so it looks like this:




    Even though 95% of all labels in the log is invisible (in other words: it does not get rendered to screen), Unity still seems to be doing the entire rendering calculation for all of them, every frame.

    Generally speaking, I can think of two possible solutions:

    1. Render all labels to a GUITexture at runtime and display only that texture in the ScrollView. Update that texture when a new line comes in.
    2. Implement a custom ScrollView component that scrolls line-by-line (instead of pixel-by-pixel)

    I'm not sure if option 1 is even possible in the free version of Unity, and even if it is, I suppose it would be rather difficult to do this, considering that on screen text is not exactly one of Unity's big strengths to begin with. Option 2 looks feasible to me, but would not feel as nice to use as a pixel-based scrolling view.

    Does anybody have any other suggestions? I've been thinking about this problem for quite some time now, but I don't see any solution besides the two mentioned above.


    Thanks,


    Alan
     
  2. atrakeur

    atrakeur

    Joined:
    Jun 14, 2011
    Posts:
    134
    The problem with the scrollview is that it do draw calls even if objects are outside of the viewport of the scrollview.

    You can just use the Vector2 used by the scrollview to calculate which lines are displayed, and display (call) only those lines...
     
  3. Alan47

    Alan47

    Joined:
    Mar 5, 2011
    Posts:
    163
    That sounds like a reasonable idea. Is there an easy way to determine the visible rectangle actually displayed on screen with respect to the larger "virtual" rectangle? Basically, what this would require is a method that determines the visible lines, replacing all other lines with a vertical space according to their total height. I guess that this can be done in a way such that the position and the size of the vertical slider remains constant, effectively hiding this exchange from the user. I need to look into this approach, maybe it would not even be a lot of work in terms of coding, more in terms of getting the math right ;)

    Thanks,


    Alan
     
  4. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    Yes, this is one way of the "virtualization".

    The scroll-by-row that you suggested is the other. In this approach you are not actually scrolling anything - you have a slider which changes the data rendered by each (static) row.

    Back to the "scrollable" approach. What you should is to calculate the Rect in local coordinates of the scrolled list. That is:

    • Rect.width and Rect.height would be width and height of the view itself (the outer height of the scrollview)
    • Rect.x coordinate would be 0 since not scrolling left/right
    • Rect.y would be the scrollPosition (supplied to GUI.BeginScrollView)

    Inside the GUI.BeginScrollView you are now rendering only rows intersecting with this Rect. In addition, you have to use absolute coordinates for each row (not GUILayout).
     
  5. Alan47

    Alan47

    Joined:
    Mar 5, 2011
    Posts:
    163
    Okay, I tried to turn your idea into code, but apparently, the Unity UI system does not quite appreciate it.
    Here are the relevant snippets:

    Code (csharp):
    1.  
    2. // this is the OnGUI method of the logger component
    3. public void OnGUI(){
    4.            GUILayout.BeginArea(onScreenRect, this.backgroundTexture);
    5.             this.scrollPosition = GUI.BeginScrollView(scrollViewRect, this.scrollPosition, this.CalculateRequiredSize(), false, false);
    6.             // make sure that there is always a "last rect" we can refer to later
    7.             GUILayout.Space(0.0f);
    8.             foreach (CustomLabel label in this.labels) {
    9.                 if (this.IsLabelVisible(label)) {
    10.                     // the label is within the visible area, render it
    11.                     label.Render();
    12.                 } else {
    13.                     // do not render the label, add a vertical space instead
    14.                     GUILayout.Space(label.TotalSize().y);
    15.                 }
    16.             }
    17.             GUI.EndScrollView();
    18.             GUILayout.EndArea();
    19. }
    20.  
    21. private bool IsLabelVisible(ULabel label) {
    22.     float topSkip = this.scrollPosition.y;
    23.     float bottomLine = GUILayoutUtility.GetLastRect().yMax;
    24.     if (topSkip >= bottomLine) {
    25.         // the label is above our current window
    26.         return false;
    27.     }
    28.     float windowHeight = this.GetOnScreenRect().height;
    29.     float bottomSkip = topSkip + windowHeight;
    30.     if (bottomLine >= bottomSkip) {
    31.         // the label is below our current window
    32.         return false;
    33.     }
    34.     // the label is visible
    35.     return true;
    36. }
    37.  

    The result is the following error:

    ... which happens when rendering the custom label. The above code works without errors if all of the labels are just painted one after another, but fails after I insert the condition which checks for label visibility.

    I've managed to find it's meaning: apparently, this kind of error is thrown when the number of components in layout groups differ between the calls of OnGUI which happen in the same frame. But... this can't be! The decision whether a label is visible or not in my case is perfectly deterministic and won't change during a single frame, since the scroll position can't change within a frame (even not moving the slider keeps throwing the error). And that's all that I've changed compared to a working version :confused:

    Any hint on this?

    Thanks,


    Alan
     
  6. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    I think it's the GUILayout thing: don't use it.

    Also try to comment out this line:

    Code (csharp):
    1. float bottomLine = GUILayoutUtility.GetLastRect().yMax;
     
  7. Alan47

    Alan47

    Joined:
    Mar 5, 2011
    Posts:
    163
    Awesome, that totally nailed down the problem: it works without errors if I just use some constant value here. Well, too bad I can't use the value stored in there, but I think that I'll be able to circumvent the problem by storing some values myself. You, Sir, are awesome :)


    Until today, the UI system never bugged me this much, it always worked, more or less. I guess I understand now why there are so many replacements for the internal UI system in the asset store ;)


    Thank you!


    Alan



    EDIT: Wow, amazing! I just needed to tweak some numbers to fit the expected window with the actual one and now the player really won't be able to see the difference - except that I'm again over 60 fps instead of 10, even on long logs!
    The only thing that remains: I still get those layout errors, every time a line is added to the log. The frame after that it works as expected again, which is kind of weird. When is it safe to modify the contents of the UI if not in the Unity main thread in an update() method? Because that's the only point in time when I really change something.
     
    Last edited: Feb 3, 2013