Search Unity

UI timer without Garbage Collection possible?

Discussion in 'Scripting' started by zapposh, Apr 18, 2019.

  1. zapposh

    zapposh

    Joined:
    Nov 12, 2016
    Posts:
    117
    I have a TMPro UI timer (timeText) updating every frame, using TimeSpan.

    Code (CSharp):
    1.     private void UpdateTime()
    2.     {
    3.         timer += Time.deltaTime;
    4.         timeSpan = TimeSpan.FromSeconds(timer);
    5.  
    6.         timeToDisplay = timeSpan.ToString(@"mm\:ss\:ff");
    7.         timeText.text = timeToDisplay;
    8.     }
    This generates 11 calls and 0.9kb of GC / frame.
    I've tried other methods like calculating minutes, seconds and milliseconds separately, then building the string, but that has even much poorer results as far as calls and GC goes.

    Has anyone a more efficient solution that does not generate GC?
     
  2. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    It's impossible to create a new string and not generate garbage, and TextMeshPro may very well be doing something with the string you assign as well to generate even more garbage. Your best bet is to only update it as frequently as needed -- I doubt you need the time updated every single frame?
     
    zapposh likes this.
  3. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    You can try the ultra-trashy way: Instantiate 60 texts for 0-59 minutes, instantiate 60 for 0-59 seconds, etc. Then enable/disable the combination you need instead of changing the text. I'm not sure how much GC you'd save from it, but at least the string allocation is gone, and probably the text mesh generation is gone. You'll still get a canvas rebuild I guess? Not too sure :).
     
    PraetorBlue likes this.
  4. dadude123

    dadude123

    Joined:
    Feb 26, 2014
    Posts:
    789
    You can totally make a new string without allocating.
    Well, not a "string" directly, but you can definitely display changing text with numbers and stuff :p.


    Just use SetText() on the tmp text. It has specialized handling for floats and supports basic formatting (just decimal digits, that's all, but it's enough).
    There are multiple overloads for it, take a look.

    It 100% does not allocate anything to build the new string.

    Another alternative would be using a string builder (however that might allocate some small strings sometimes when dealing with number to text formatting).
     
  5. zapposh

    zapposh

    Joined:
    Nov 12, 2016
    Posts:
    117
    Actually came up with something similar, which is perhaps overkill, but works super well.
    Made a static array pool of intToStrings from 0 to 99 in awake, and the value of the array is then called via timeSpan.
    After generation, no further garbage at runtime, only 1 call.

    Here's the example for seconds (I use the same path for minutes and milliseconds):

    Code (CSharp):
    1.     private void UpdateTime()
    2.     {
    3.         timer += Time.deltaTime;
    4.         timeSpan = TimeSpan.FromSeconds(timer);
    5.         timeText.SetText(TimerStringsPool.GetNumberString(timeSpan.Seconds));
    6.     }
     
  6. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    I had no idea about the SetText()! That's on the textmeshpro text instead of UGUI text?
     
  7. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,456
    Thanks @dadude123, I actually missed that API.

    Also RE:
    Yes creating a string, will always allocate memory and if you want to pass it to any API's taking string, it will have to be managed memory.
    However, there are ways to get around having to allocate a new string every frame just to display the FPS/Time display/Coin counters that you might want to put into the UI. One way is TextMesh Pro's SetText API

    Another is going to take some reflection, unsafe pointers, and other potentially considered hacky ways to reuse one string per UI element that you need to feed with a string.

    I'm not gonna delve into the full breadth of that as it is quite dependent on the version of the scripting backend used but here are some resources on that for the adventurous:
    http://www.gavpugh.com/2010/04/01/xnac-avoiding-garbage-when-working-with-stringbuilder/
    http://www.gavpugh.com/2010/12/21/xnac-stringbuilder-changes-in-xna-4-0/
    http://www.nesterovsky-bros.com/weblog/2010/08/25/StringAndStringBuilderInNET4.aspx
     
    zapposh likes this.
  8. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    Text Mesh Pro allows you to give it a char array, so you can just do that. I've got GC-free strings like that.

    Essentially, to show the time 12:45, you do:

    Code (csharp):
    1. chars[0] = '1';
    2. chars[1] = '2';
    3. chars[2] = ':';
    4. chars[3] = '4';
    5. chars[4] = '5';
    6.  
    7. textMeshProThing.SetCharArray(chars);
    that won't allocate anything for the string generation. I also seem to remember that you can null-terminate strings using that apporach, so if you set chars[n] to '\0', no chars from n or later will be displayed.

    Turning time into chars and putting them into the correct slots is an exercise left to the reader.
     
  9. adamgryu

    adamgryu

    Joined:
    Mar 1, 2014
    Posts:
    188
    Just in case anyone is looking for a copy and paste solution - here's some seconds to char array code for format 0:00.000

    Code (CSharp):
    1.     private void SecondsToCharArray(float timeInSeconds, char[] array) {
    2.         int minutes = (int)(timeInSeconds / 60f);
    3.         array[0] = (char)(48 + (minutes % 10));
    4.         array[1] = ':';
    5.  
    6.         int seconds = (int)(timeInSeconds - minutes * 60);
    7.         array[2] = (char)(48 + seconds / 10);
    8.         array[3] = (char)(48 + seconds % 10);
    9.         array[4] = '.';
    10.  
    11.         int milliseconds = (int)((timeInSeconds % 1) * 1000);
    12.         array[5] = (char)(48 + milliseconds / 100);
    13.         array[6] = (char)(48 + (milliseconds % 100) / 10);
    14.         array[7] = (char)(48 + milliseconds % 10);
    15.     }
     
  10. better_walk_away

    better_walk_away

    Joined:
    Jul 12, 2016
    Posts:
    291
    How about changing the sprite of a Image? There is no string involved in this method.