Making damage numbers or damage text popups appear above a sprite in Unity, at first glance, seems like a simple one. There are definitely many approaches to this, but one must beware of inefficient, resource intensive or bug-prone solutions.
Due to how strongly integrated text and Unity UI are, it seems obvious that a first solution is to render a simple "UIText" object above an enemy sprite - that is, a fully UI based solution. This comes with the rather significant problem of mapping the UI space to the world space, as an enemy at (0,0,0) in world space might have completely different coordinates to place UI on.
Additionally, since the UI Canvas is usually linked to a Camera, if you have a moving camera (say, in a platformer or top down shooter) that tracks the player, you'll have to offset the position of the text to compensate for the camera motion. Zooming and rotation of the camera produce an even larger problem. Here, by following the UI solution, we need to produce two mappings - World Space to the Moving Camera Space, then from a Moving Camera space to a static camera space, with position, rotation and scaling offsets attuned to the time of instantiation. It's just too much.
Instead of this solution, you might also choose to "hardcode" text sprites, and instantiate individual letters or words as sprites, rather than UIText, but this lacks flexibility and requires you to make a new sprite for each number and word, and essentially recreate the whole business of rendering text from sprites of characters (eg. a font) from scratch. I feel like this also is an incorrect approach.
The purpose of this article is to present a better solution to this problem, by leveraging TextMeshPro, and compares the efficacy of Unity's built in Animation suite, and a procedural, code based approach to the problem.
A solution that I have been using is by using objects that share the Global coordinates that the rest of your game is in, but preserves the flexibility of working with a text processing engine, complete with Rich Text support and myriad additional features. Unity does have a built in "TextMesh" 3D object, but I think that TextMeshPro is the better alternative. It requires first a quick import, and is packaged (free of charge) with the Unity Engine.
At a high level, we'll be creating a customizable text object, which will be instantiated above our enemy when our it is damaged. After the text object is instantiated, it will float up and disappear, and then remove itself from the scene by calling Destroy() on itself.
This is the order of events:
This can be installed through the Unity Package Manager (Menu Bar > Window > Package Manager > TextMeshPro > Install).
Then, you can add a non UI text component by going to the menu bar, and creating a GameObject > 3D > Text / TextMeshPro. This will prompt the import of TMP Essentials, which includes a default font and some other useful starting assets, all nested away in a separate folder that you won't have to worry about.
You can customize this text component to say whatever you like by editing the Text field in the inspector, and you should disable wrapping as this effect is, in my opinion, undesirable in a text popup effect.
Before animating, we should clearly define what sort of effect we're looking for. My effect will shrink, fade out and move upwards as time passes, eventually becoming invisible by which time it will be destroyed. Let's break down these effects individually:
To make an object shrink, we must reduce its scale, but for text we can reduce the font size. The default justification is to the upper left corner of the textbox, and I don't think this looks good as the text appears to translate as you reduce its font size. To prevent this unpredictable scaling, you can set the justification to be centered on both axes.
Fadeout is as simple as setting the Vertex Colour's alpha to zero, and upwards movement is an increase in the Y coordinate (in 2D space). These are all the required fields for animating.
If we use Unity animation, we must use a Parent gameobject, to hold our text. This is because Unity animation, as far as I know, cannot handle animating local coordinates, and positional animation is done in global coordinates. I made a new empty gameobject and called it "TextHolder". Now why is this important?
Local coordinates are stated relative to a parent, and global coordinates are stated relative to the scene's origin at (0,0,0). If we were to animate the position of our text, going from say, (0,0,0) to (0,4,0), these would be global coordinates. We wouldn't be able to move the text away from these positions in global space, and our text would always spawn and move into the same positions, so instead we must force it to move upwards 4 units in local coordinates by making all coordinates relative to a parent gameobject. Reset the transforms of both objects to (0,0,0).
To open the animation tab, you can go to Window > Animation > Animation or hit Ctrl + 6 on the keyboard. With the Text (not the holder) selected, create a new animation and call it whatever you like. Then, we can add keyframes by autokeyframing (the red recording button) at 0:00s and 1:00s (a one second long animation). Just set the initial values at 0s for y position, scale and vertex colour, then set the final values at 1s (eg. Y position higher up, smaller font size, vertex colour as transparent)
To destroy the gameobject when the animation is done, we have several options, but the easiest is with a hacky method with Animation Events. You can add one by going to the final frame and then hitting the bookmark looking icon. This allows us to call any public method attached to the gameobject being animated, and we can create a simple "self-deletion" script called "DeleteOnAnimEnd.cs" and attach this to the animated text gameobject. We also want to clean up the parent, so we will create a one line function that destroys the parent's gameobject once called.
public class DestroyOnAnimationEnd : MonoBehaviour
{
public void DestroyParent(){
Destroy(gameObject.transform.parent.gameObject);
}
}
If we set this as the function to be called at the animation event, (make sure to click the little bookmark in the timeline) we will have it trigger the DestroyParent() function at that frame, and thus destroy the parent (and child) gameobject.
Prefabs in Unity are useful to create extensible, multipurpose and instantiable content. We can drag the textholder gameobject from the hierarchy into the project tab to create a prefab. Then, if we drag out the textholder prefab into the scene, we'll see it is regenerated.
Our enemy can be anything, it's just a 2D sprite in this case that will have something spawn on top of it.
To imitate damage, well create a new script that detects when the player presses the X key on the keyboard, then instantiates the damage text over the enemy's head with a customizable string. I called it GameManager.cs, and the code is below.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class GameManager : MonoBehaviour
{
public GameObject damageTextPrefab, enemyInstance;
public string textToDisplay;
// Update is called once per frame
void Update()
{
if(Input.GetKeyDown(KeyCode.X)){
GameObject DamageText = Instantiate(damageTextPrefab, enemyInstance.transform);
DamageText.transform.GetChild(0).GetComponent<TextMeshPro>().SetText(textToDisplay);
}
}
}
To activate this code, we'll have to attach it to something in our scene. I suggest the main camera.
For a more advanced and flexible technique, we can do everything from code, with means we can easily adjust the start and end colour, height, scale and other properties from code, rather than by adding new keyframes. The following code will permit you to do so, simply add this to a textmeshpro Text gameobject, and set the properties. No parenting is required. You can turn the gameobject, with this component, into a prefab and instantiate it the same way as above, but we can change the line
DamageText.transform.GetChild(0).GetComponent<TextMeshPro>().SetText(textToDisplay);
to
DamageText.GetComponent<TextMeshPro>().SetText(textToDisplay);
as we no longer need the child reference.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//using TMPro;
public class DamageTextProcedural : MonoBehaviour
{
public Color color_i,color_f;
public Vector3 initialOffset, finalOffset; //position to drift to, relative to the gameObject's local origin
public float fadeDuration;
private float fadeStartTime;
// Start is called before the first frame update
void Start()
{
fadeStartTime = Time.time;
}
// Update is called once per frame
void Update()
{
float progress = (Time.time-fadeStartTime)/fadeDuration;
if(progress <= 1){
//lerp factor is from 0 to 1, so we use (FadeExitTime-Time.time)/fadeDuration
transform.localPosition = Vector3.Lerp(initialOffset, finalOffset, progress);
DamageText.color = Color.Lerp(color_i, color_f, progress);
}
else Destroy(gameObject);
}
}
We are taking advantage of the LERP function to smoothly transition from an initial to final state, controlled by a progress variable which is affected by the total time passed divided by the duration of the animation. As this is altogether more complex than the other techniques I explore, I will make a separate guide on this in the future. For now, I suggest you only use this code as a jumping off point, and I have no guarantees that it will work :)
While there are many esoteric ways of creating a hit indicator, damage text and crit marker in Unity, I think instantiating an animated TextMeshPro text is the easiest way, but it can be a bit inflexible so a procedural approach to circumvent the animation component can produce more desirable results, with more skill and effort applied.