Damage Popups performance

So I have previously tried to tackle adding damage popups to my game. while general implementation was easy enough, everything I tried resulted in huge performance loss under simple stress testing. 100 damage popups at a time in an otherwise empty scene would drop my fps in editor from around 300-350 to about 100-150.

I tried a single canvas on the player with each popup being a text object in the canvas, positioned by a script.
I tried world canvas per enemy, with each popup being a text object in the canvas, positioned by a script.
I tried each popup as its own world canvas…
All the while I was using a pooling system to prevent Instantiate/Destroy.

So now, after a long break from working on that sort of system, I am about to crack into it again. This time, I figured I would ask for any best practices before getting started.

So it is a very generalized question but, when it comes to these damage popup texts, how does one accomplish it without it cutting performance?

First of all, if you’re doing 100 damage popups that means there’s probably 25-100 damage events going on simultaneously, with all the raycasting/collisions, effect spawning, etc that comes with that. You’re probably going to have a lot more trouble with that and it’s probably where your focus should be if you haven’t sorted it already.

300 to 150 fps is not that much of a drop, it’s nowhere near as significant as dropping from 60 to 30, so it’s debatable as to whether it’s worth worrying about at this point. IMO the better approach is to build out all the gameplay without optimizing performance but roughly aiming around 30fps, and then profiling and optimizing what actually affects performance the most until everything is 60+.

You need to run the scene through profiler and see what is eating your milliseconds.

1 Like

Yes, profile it. The editor fps of an empty scene dropping from 500 to 100 tells you exactly nothing.

I bet the issue is actually with font rendering and it may depend on the font asset (ie static vs dynamic and other settings) and whether you use TextMesh Pro.

Also, you could simply make each digit its own sprite and combine them together so that 1271 is actually four sprites. You would only need a tiny atlas with 20 sprites plus maybe some extra for + - ! and whatever else you need to display as characters inside the popups. This is probably going to be the most performant option.

I think font renderers already do that or something similar. You could verify that by checking what the font looks like in wireframe mode. There’s high chance t hat you’ll see wireframe squares.

The original unity font (and not text mesh pro) also definitely supported “texture fonts”, where you could feed a font texture images into it and have it rendered.

Yes, they do, at least TextMesh Pro does it. But it also depends on whatever font settings are being used, at a minimum one should specify a custom character range (eg “0-9” plus any extra characters).

For full control, using your own sprite rendering gives you the most control and it’s relatively trivial to implement (for monospace fonts). One reason to do so would be to animate characters individually, shaking them or have them appear with slight delay (typewriter feel) and such things.

The fps is cut in half i would call that significant. Never look at the fps in absolute nunbers. Also never look at fps. Look at frame times.

25-30 raycats is nothing on a modern computer. The problem lies elsewhere, profile.

I can cut fps in half from the get go just by applying some settings.

It’s not clear if OP is actually experiencing <60fps at all, in which case optimizing text popups, imo, is premature optimization.

Anyways, profiling is never a bad idea.

I haven’t had issues putting the texts on a dedicated canvas, pooling them, then positioning them via a single function that loops a list (not a script/coroutine for each one). And make sure pixel perfect is off.

I can’t remember if it scaled to 100 or not.

Of course profiling shall be the first thing to do! Unity has a really powerful profiler. You’ll learn how to use it in 30 minutes.

Still here some guesses in the blue:
100 damage texts probably also mean 100 memory alocations because strings in C# sadly suck when it comes to performance.
TextMeshPro has a method SetCharArray which allows you to set directly from an array with fixed memory and avoid creating a “string” object at all. That could be a first try.
However the number of game objects themselves could already be a pain. I’d suggest particles: Make a particle with the numbers 0 to 9 as frames and no animation speed and emit those via code to form the numbers. That also allows for cool effects like tumbling dropping numbers ala PUBG.

@Stardog 's suggestion is worth a try too: Push all damages to a queue and then handle that queue in one go in one script. That can sometimes boost things.

You can surely fire clusters of them off per frame and find a balancing point. After all yer talking milliseconds here. If it takes four frames to fire off 100 for example it will take 48 milliseconds. A blink of an eye is typically 400 milliseconds for context.

1 Like

I wonder how worldtoscreenspace on an overlay canvas compares to having world space canvas and elements.

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

public class Hud : MonoBehaviour
{
    List<Transform> worldObjects = new List<Transform>();
    List<RectTransform> Texts = new List<RectTransform>();
    public GameObject prefab;  //just a ui Text with alignment set to centered
    Vector3 offset = Vector3.zero;
   
    void Start()
    {
        for (int i=0;i<10;i++)
        {
            worldObjects.Add(GameObject.CreatePrimitive(PrimitiveType.Cube).transform);
            worldObjects[i].position = offset;
            offset.x +=2;
            offset.z +=2;
            GameObject ob = Instantiate(prefab, transform); //transform is canvas root

            var txt = ob.GetComponent<Text>();
            Texts.Add(ob.GetComponent<RectTransform>());
            txt.text = $"item {i}";
            Texts[i].position = Camera.main.WorldToScreenPoint(worldObjects[i].position);
        }
    }
    private void Update()
    {
        for (int i = 0; i < Texts.Count; i++)
        {
            Texts[i].position = Vector3.MoveTowards(Texts[i].position, Texts[i].position + Vector3.up, Time.deltaTime * 150);
        }
    }
}

350fps is your fastest number there, which is 2.86ms per frame.

100fps is your slowest number there, which is 10ms per frame.

This means that your 100 damage popups are eating 10 - 2.86 = ~7.14ms.

Assuming you’re targeting 60fps, that means you have 16.7ms per frame. Your popups are eating nearly half of that, which is not a situation I’d be satisfied with.

Caveat: I assume that the FPS numbers you gave are from the Editor? Performance numbers in the Editor do not reflect performance on your target platform in a build. So you need to test in a build on your target device. Depending on your target platform the difference could be dramatic. Heck, performance of my projects in the Editor itself varies dramatically based on stuff totally unrelated to the simulation’s own performance, such as which GameObject’s data is being displayed in the Inspector*.*

Anyway, when I’m doing performance testing I either make separate builds for each thing I’m testing, where I have changed only that thing, or I make a single build with a button which toggles between each of the things I’m testing. Then I attach the Profiler to that build on my target device, and check out as much detail as I reasonably can that way.

When optimising something, the process should always be: Measure → Change → Measure.

Personally, one thing I’d specifically measure is how much time the Canvas takes to draw 100 popups which are statically placed (i.e. not moving, no code messing with them). If that’s too slow then it doesn’t matter how much you can optimise elsewhere, you just need to find a different approach because you can’t make Canvas itself faster.

As for what that other approach may be, TextMesh Pro does not require a Canvas to render, so if it’s just text, or text and icons, I’d start there. You could also combine it with textured quads if it needs a backer or whatnot.

From a performance perspective I love the particle system idea as they’re really good at rendering small items in bulk, but getting it to look right will be super fiddly, so I wouldn’t investigate it unless other options weren’t working out.

Don’t worry about this until you’ve got general performance under control. If you’re creating and destroying a large number of items it’s a good idea, but it does not directly impact your baseline frame rate, it will cause periodic hiccups at GC time. Solve that once it becomes a problem. Until then, don’t let it slow down your productivity on the core issue you need to solve.

2 Likes

Am assuming their usecase is that a new damage text shall appear every time an enemy is hit and they are testing that by hitting the enemy rapidly like that. So pooling is crucial (or other means of avoiding massive memory acquisition).

No argument there. But solve one problem at a time.