Same approach for level up text fade

I wanted to use this type of text animation for level up event as well (instead of the particle effect). I did this by using the DamageTextSpawner and adding a UnityEvent in BaseStat for onLeveledUp. I just want the text “Level Up!” to appear above the player’s head so I dont need to pass in any value. This is my DamageTextSpawner class:

using UnityEngine;

namespace RPG.UI.DamageText
{
    public class DamageTextSpawner : MonoBehaviour
    {
        [SerializeField] DamageText damageTextPrefab = null;
        [SerializeField] GameObject levelUpTextPrefab = null;

        // Called when TakeDamageEvent<float> is invoked in Health 
        public void SpawnDamageText(float damageAmount)
        {
            DamageText instance = Instantiate<DamageText>(damageTextPrefab, transform);
            instance.SetValue(damageAmount);
        }

        public void SpawnLevelUpText()
        {
            Instantiate<GameObject>(levelUpTextPrefab, transform);
        }
    }
}

I was just wondering if there is a better way to approach this? Should I create a separate gameobject with a canvas for the level up text or can I use the same canvas that has the damage text and do some adjusting there? Any help much appreciated!

I set my DamageTextSpawner up to take either a float and a string, and the DamageText itself to also take either a float or a string for setup using overloading:

Here’s my complete HealthChangeDisplay (what I called the DamageText in my own projects)

HealthChangeDisplay.cs
using TMPro;
using UnityEngine;

namespace TkrainDesigns.UI
{
    //[RequireComponent(typeof(TextMeshPro))]
    public class HealthChangeDisplay : MonoBehaviour
    {
        TextMeshPro text;


        [SerializeField] float speed = 10.0f;
        [Range(1, 10)] [SerializeField] float duration = 1.0f;
        [SerializeField] Color damageColor = Color.red;
        [SerializeField] Color healedColor = Color.green;


        float lifeTime = -1;

        private void Awake()
        {
            text = GetComponent<TextMeshPro>();
            if (!text)
            {
                text = GetComponentInChildren<TextMeshPro>();
            }
        }


        // Update is called once per frame
        void Update()
        {
            if (!text)
            {
                Destroy(gameObject);
                return;
            }

            transform.localPosition += Vector3.up * speed * Time.deltaTime;
            text.alpha = 1 - (lifeTime / duration);
            lifeTime += Time.deltaTime;
            if (lifeTime > duration)
            {
                Destroy(gameObject);
            }
        }

        public void Initialize(float delta)
        {
            if (!text)
            {
                Destroy(gameObject);
                return;
            }

            text.color = delta > 0 ? healedColor : damageColor;
            text.text = $"{Mathf.Abs(delta)}";
            lifeTime = 0;
        }

        public void Initialize(string message)
        {
            if (!text)
            {
                Destroy(gameObject);
                return;
            }

            text.text = message;
            lifeTime = 0;
        }
    }
}

along with my spawner (which is probably more complex than normally needed, but I make it so that when lots of messages are being displayed at once, they don’t all print at once using a Queue

HealthChangeDisplaySpawner.cs
using System;
using System.Collections.Generic;
using TkrainDesigns.UI;
using UnityEngine;

public class HealthChangeDisplaySpawner : MonoBehaviour
{
    public HealthChangeDisplay healthChangeDisplay;

    private Queue<Action> actions = new Queue<Action>();

    public void SpawnHealthChangeDisplay(float delta)
    {
        if (healthChangeDisplay)
        {
            actions.Enqueue(() =>
            {
                HealthChangeDisplay h = Instantiate(healthChangeDisplay, transform.position, Quaternion.identity);
                h.Initialize(delta);
            });
        }
    }

    public void SpawnHealthChangeString(string message)
    {
        if (healthChangeDisplay)
        {
            actions.Enqueue(() =>
            {
                HealthChangeDisplay h = Instantiate(healthChangeDisplay, transform.position, Quaternion.identity);
                h.Initialize(message);
            });
        }
    }

    private float last = 0;
    private bool emptyQueue = true;

    private void Update()
    {
        last += Time.deltaTime;
        if (actions.Count > 0 && (emptyQueue || last > .25f))
        {
            var action = actions.Dequeue();
            action.Invoke();
            last = 0;
            emptyQueue = actions.Count == 0;
        }
    }
}

So the OnLevelUp UnityEvent links to the spawner with the string method “Level Up!”
It also works with any other type of message you might want to send… for example, you could link to the spawner when experience is gained with a string of $"+ {experienceGained} experience!";

1 Like

Thanks for this!

I tried to imitate this approach but instead of animating the text through code, I kept the Animation clip on the gameobject itself and just instantiated the object with the animation.

This is my updated DisplayText (used to be DamageText)

using UnityEngine;
using TMPro;
using System;

namespace RPG.UI.DisplayText
{
    public class DisplayText : MonoBehaviour
    {
        TextMeshProUGUI displayText = null;
        [SerializeField] Color damageColor = new Color(142f, 0f, 0f, 1f);
        [SerializeField] Color experienceColor = new Color(106f, 255f, 120f, 1f);
      
        private void Awake()
        {
            displayText = GetComponent<TextMeshProUGUI>();
            if (!displayText)
            {
                displayText = GetComponentInChildren<TextMeshProUGUI>();
            }
        }

        public void SetValue(float amount)
        {
            displayText.color = damageColor;
            displayText.text = String.Format("{0:0}", amount);
        }

        public void SetString(string message)
        {
            displayText.color = experienceColor;
            displayText.text = message;
        }
    }
}

and this is my updated DisplayTextSpawner (used to be DamageTextSpawner)

using System;
using System.Collections.Generic;
using UnityEngine;

namespace RPG.UI.DisplayText
{
    public class DisplayTextSpawner : MonoBehaviour
    {
        [SerializeField] DisplayText displayTextPrefab = null;
        private Queue<Action> actions = new Queue<Action>();
        private float last = 0;
        private bool emptyQueue = true;

        private void Update()
        {
            last += Time.deltaTime;
            if (actions.Count > 0 && (emptyQueue || last > .25f))
            {
                var action = actions.Dequeue();
                action.Invoke();
                last = 0;
                emptyQueue = actions.Count == 0;
            }
        }
    
        // Called when TakeDamageEvent<float> is invoked in Health 
        public void SpawnDisplayNumber(float amount)
        {
            if (displayTextPrefab)
            {
                actions.Enqueue(() =>
                {
                    DisplayText instance = Instantiate<DisplayText>(displayTextPrefab, transform);
                    instance.SetValue(amount);
                });            
            }
        }

        public void SpawnDisplayString(string message)
        {
            if (displayTextPrefab)
            {
                actions.Enqueue(() =>
                {
                    DisplayText instance = Instantiate<DisplayText>(displayTextPrefab, transform);
                    instance.SetString(message);
                });        
            }
        }

    }
}

The issue I am running into is that the DisplayTextPrefab is spawning but I cannot see the text. Is this possibly because the animation clip is not playing because I am not running it in the Update()? Also, I am assuming the Queue is so that a function is not called until 0.25f have passed so not multiple text are spawning at the same time?

Yes, it my case, the messages were doled out every 1/4 second… There were situations where I needed extra messages like hit + killed + experience + levelup… those would all stack up into one blob if left to run in realtime.

You’ll only need an Update in the DisplayText if you don’t have an animation associated with it to make it move (i.e. if you need to move it by adjusting transform.position each frame).

Are you getting any errors in the console when the text spawns?

I managed to fix it by changing it to [SerializeField] TextMeshProUGUI and manually dragging it in rather than getting the component in Awake() in DisplayText.cs. Is there a different between those two ways of getting the TextMeshPro component?

Also can I get a bit of an explanation on the Update() in your spawner?

private void Update()
        {
            last += Time.deltaTime;
            if (actions.Count > 0 && (emptyQueue || last > .25f))
            {
                var action = actions.Dequeue();
                action.Invoke();
                last = 0;
                emptyQueue = actions.Count == 0;
            }
        }

From my understanding, if there are actions in the queue and the has been more than 0.25f after the last action, then invoke the next action (spawn a new text). I don’t fully understand the empty queue checking in the if statement though? if (actions.Count > 0 && (emptyQueue || last > .25f))

-solved-

If the TextMeshProUGUI is on the same GameObject as your script, you can usually use GetComponent<TextMeshProUGUI>() in Awake() to retrieve it. If it’s not, it’s better to make it a SerializedField and assign it manually in the inspector to ensure that it is there.

The emptyQueue is only set if there are no more actions in the Queue once we’ve fired off the last action. It’s not strictly necessary, as no matter what, in 1/4 second, the last>.25f is true anyways. I don’t even remember why I put it in there.

So if two or more actions occur on the same frame (perhaps Hit + Killed + experiencegained), then they are throttled, so they don’t stack together. If, however, two more more actions happen on different frames (say enemy A hits you and enemy B hits you three frames later), then the system won’t throttle the 2nd message, because the Queue will be empty on the next frame when enemy B hits. It makes things a bit more “urgent” in combat, while still throttling messages where appropriate.

emptyQueue = actions.Count == 0; 

is equivalent to

if(actions.Count==0) emptyQueue = true; else emptyQueue = false;

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms