UI Optimization - Hundreds of floating damage text

Hey all, I struggle with an issue that I don’t know how to fix. I’m new to Unity (many years on UE) so I don’t know how and where optimize this kind of things atm.

I work on a project with hundreds of actors spawned in the world (that’s why I switch to Unity, UE can’t handle at all many pawns in the world and UMG is very very poorly optimized for hundred widgets displayed). I’ve made a Floating Damage Text script, work like a charm, but when I hit like 300 actors with multiple hits, fps drop to 40 with huge freeze.

I’ve made a Prefab that include a Canvas and a Text as a child, where I put my damage text that I need to display.

Then, with a SphereCollider, I do an overlap and detect all “enemy” in the radius, then I get the component and do the TakeDamage then the DisplayFloatingText. All works fine, but when I need to display hundreds of floating damage text, fps drop really down, like 40 fps, with huge freeze.

Is there any way to optimize the text render, or the settings of UI in world, or the prefab of the Floating text, or any other way to display floating damage in a more optimized way ? Like, render it in 2D on the player screen, not in 3D in the world ? Any suggestion or help is welcome !


My DamageIndicator script :

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

public class DamageIndicator : MonoBehaviour
{

    public Text text;
    public float lifetime = 0.6f;
    public float minDist = 2f;
    public float maxDist = 3f;

    private Vector3 iniPos;
    private Vector3 targetPos;
    private float timer;

    // Start is called before the first frame update
    void Start()
    {
        transform.LookAt(2 * transform.position - Camera.main.transform.position);

        float direction = Random.rotation.eulerAngles.z;
        iniPos = transform.position;
        float dist = Random.Range(minDist, maxDist);
        targetPos = iniPos + (Quaternion.Euler(0, 0, direction) * new Vector3(dist, dist, 0f));
        transform.localScale = Vector3.zero;
    }

    // Update is called once per frame
    void Update()
    {
        timer += Time.deltaTime;

        float fraction = lifetime / 4f;

        if (timer > lifetime) Destroy(gameObject);
        else if (timer > fraction) text.color = Color.Lerp(text.color, Color.clear, (timer - fraction) / (lifetime - fraction));

        transform.localPosition = Vector3.Lerp(iniPos, targetPos, Mathf.Sin(timer / lifetime));
        transform.localScale = Vector3.Lerp(Vector3.zero, Vector3.one, Mathf.Sin(timer / lifetime));
    }

    public void SetDamageText(int damage)
    {
        damage = 10;
        text.text = damage.ToString();
    }
}

My Enemyhealth script :

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

public class EnemyHealth : MonoBehaviour
{

    public GameObject damageText;
    public int maxHealth = 100;

    private int curHealth;

    // Start is called before the first frame update
    void Start()
    {
        curHealth = maxHealth;
    }

    public void TakeDamage(int damage)
    {
        curHealth -= damage;
        DamageIndicator indicator = Instantiate(damageText, transform.position, transform.rotation).GetComponent<DamageIndicator>();
        indicator.SetDamageText(damage);
        if (curHealth < 1) Destroy(gameObject);
    }
}

My spell function :

    public void SpellAoE()
    {
        Collider[] colliders = Physics.OverlapSphere(transform.position, 2f);
        foreach(Collider Enemy in colliders)
        {
            EnemyHealth enemy = Enemy.GetComponent<EnemyHealth>();

            if (enemy != null)
            {
                enemy.TakeDamage(Random.Range(1, 15));
                Debug.Log("Hitted");
            }
        }
    }

}

You should use a single canvas you don’t need to have 300 different canvas