Coroutine generates GC.

Hi,

I use a coroutine to fade in and out a TextMeshProUGUI but it allocates GC, and I am curious if there is a way to avoid it.

Thank you for your time.

 private TextMeshProUGUI objectTMP;

    public float fadeDuration = 2.0f;
    public float fadeWaitTime = 1.0f;
    public float maxOutline;

    private void Awake()
    {
        objectTMP = gameObject.GetComponent<TextMeshProUGUI>();
    }

    private void Start()
    {
        StartCoroutine(FadeRepeatCoroutine(objectTMP, fadeDuration, fadeWaitTime));
    }

    IEnumerator FadeInOutCoroutine(TextMeshProUGUI tmpro, bool fadeIn, float duration)
    {
        float minOutline = 0;
        float counter = 0f;
        float a, b;

        if (fadeIn)
        {
            a = minOutline;
            b = maxOutline;
        }
        else
        {
            a = maxOutline;
            b = minOutline;
        }

        while (counter < duration)
        {
            counter += Time.deltaTime;

            tmpro.outlineWidth = Mathf.Lerp(a, b, counter / duration);

            yield return null;
        }
    }


    IEnumerator FadeRepeatCoroutine(TextMeshProUGUI textToFade, float duration, float waitTime)
    {
        WaitForSeconds waitForXSec = new WaitForSeconds(waitTime);

        while (true)
        {
            yield return FadeInOutCoroutine(textToFade, true, duration);
            yield return FadeInOutCoroutine(textToFade, false, duration);
            yield return waitForXSec;

        }
    }

Here is an image of the profiler:

Coroutines always make garbage when you fire one off.

More important question is, does it even matter?

2 Likes

Fifty six lines of code for a fader?? That’s a little bit … over-engineered!

Fading, simple and easy:

Should be more like 2 float variables and one super-simple line.

2 Likes

I found it on stackoverflow and this approach was suggested by “Programmer” and from his accolades I trusted him without question :smile:
https://stackoverflow.com/questions/44933517/fading-in-out-gameobject/44935943#44935943

I tried your suggestion, and while it is less spaghetti it still generates 400B of GC which is better than 0.8 kB with the coroutine! Thank you.

The worry now is since I am checking if the desired value is == 0, is MoveTowards prone to miscalculating the float, like never reaching exactly 0? Would it be a good idea to use Mathf.Approximately ?

    public float desiredOutline;
    public float fadeTime;
    private float currentOutline = 0;
    private TextMeshProUGUI objectTMP;
    private float baseDesiredOutline;

    private void Awake()
    {
        baseDesiredOutline = desiredOutline;
        objectTMP = gameObject.GetComponent<TextMeshProUGUI>();
    }

    void Update()
    {
        if (currentOutline == desiredOutline)
        {
            currentOutline = desiredOutline;
            if (desiredOutline != 0)
            {
                desiredOutline = 0;
            }
            else
            {
                desiredOutline = baseDesiredOutline;
            }
        }
        currentOutline = Mathf.MoveTowards(currentOutline, desiredOutline, fadeTime * Time.deltaTime);
        objectTMP.outlineWidth = currentOutline;
    }

Target platform is mobile. The less GC the better. :smile:

Though this post celebrated that Coroutines do not generate GC on their own only when you return an object

https://www.reddit.com/r/Unity3D/comments/4l0r65/finally_coroutines_dont_allocate_garbage_every/

I’m not quite seeing why there’s GC going on, unless it’s TMP being bad. You should put the profiler in deep mode and figure out what’s up. Also probably test in builds, might be some editor only stuff.

Unsolicited code review time!

The currentOutline = desiredOutline; there isn’t necessary, you already checked that it had that value!
For fun, if you want to, you can code-golf the entire thing down to:

if (currentOutline == desiredOutline)
    desiredOutline = baseDesiredOutline - currentOutline;

Since x - 0 = x and x - x = 0

Though it makes you have to think more about the code when you read it than you used to have.

1 Like

My mind was not even aware of such possibilities :smile: :smile: It works and makes sense! Thank you.

Well, the issue is this part:

        while (true)
        {
            yield return FadeInOutCoroutine(textToFade, true, duration);
            yield return FadeInOutCoroutine(textToFade, false, duration);
            yield return waitForXSec;
        }

Each call to the generator method FadeInOutCoroutine creates a new state machine object which is returned. If everything would be implemented in a single endless running coroutine, there would be no problem

1 Like

No, the code I posted in the above link actually does not generate GC.

The only lines of code are:

  • variable declarations
  • a variable assignment using a static Mathf.MoveTowards() method
  • whatever you do with the output value (currentQuantity)

It’s likely something else you are doing or that TMPro is doing. It’s possible TMPro rebuilds the mesh when you change the fade color for all I know.

1 Like

Interesting. Thank you everyone!

I will ask in the TMP forums, will post a link when and if it gets resolved.

Don’t lose sight of the question Spiney asks:

Been using Unity in production code for a decade, and generally as long as you don’t do needless things, GC is NEVER an issue. Worrying about GC and such is a complete waste of time 99.99% of the time. Just don’t do needless thigns.

1 Like

Hi, again and thank you all. This code below is the final version and generates 0 GC. It was a TMP issue and if you would like to read on it more here is the link of the discussion:

    public float desiredOutline;
    public float fadeTime;
    private float currentOutline = 0;
    private TextMeshProUGUI objectTMP;
    private Material tmpMaterial;
    private float baseDesiredOutline;
    private bool hasReachedMax = false;
  
    private void Awake()
    {
        baseDesiredOutline = desiredOutline;
        objectTMP = gameObject.GetComponent<TextMeshProUGUI>();
        tmpMaterial = objectTMP.fontMaterial;
    }
    void Update()
    {
        if (hasReachedMax == false)
        {
            objectTMP.UpdateMeshPadding(); // Update padding until max
        }
        if (currentOutline == desiredOutline)
        {
            if(hasReachedMax == false) // When max update one last time
            {
                objectTMP.UpdateMeshPadding();
                hasReachedMax = true;
            }
            desiredOutline = baseDesiredOutline - currentOutline; // Fade the outline opposite direction
        }
        currentOutline = Mathf.MoveTowards(currentOutline, desiredOutline, fadeTime * Time.deltaTime);
        tmpMaterial.SetFloat(ShaderUtilities.ID_OutlineWidth, currentOutline); // Use material instance to avoid GC
    }