Hello, I’m making a turn based RPG and can’t figure out how to properly display damage over the characters. My idea was to make a Label appear and fade out showing the damage that was done. Here’s my code:
public IEnumerator DisplayDamage(string s, Vector3 pos)
{
DamageText damageText = new DamageText(allControls, s);
Vector3 newPos = cam.WorldToScreenPoint(pos);
damageText.txt.style.left = newPos.x;
damageText.txt.style.bottom = newPos.y;
uiDoc.rootVisualElement.Q("Display").Add(damageText.txt);
StartCoroutine(anim.FadeOut(damageText.txt));
yield return new WaitForSeconds(1f);
uiDoc.rootVisualElement.Q("Display").Remove(damageText.txt);
}
The Label is displayed on screen and fades out as intended, however it is never where I want it to be (usually I pass the transform.position I want it to be on top of). Here’s a screenshot, which also shows the “Display” VisualElement (the outlined space):
For instance, this attack hits both of the enemies, and thus two Labels appear. However, they’re not on top of the enemy.
Thanks in advance for the help!
There’s specific conversion methods here for converting world positions to UI Toolkit panel coordinates: Unity - Scripting API: RuntimePanelUtils
This has come up in some previous threads so a bit of searching should find you some more complete code examples.
This is my new code:
public IEnumerator DisplayDamage(string s, Vector3 pos)
{
DamageText damageText = new DamageText(allControls, s);
Vector2 newPos = RuntimePanelUtils.CameraTransformWorldToPanel(uiDoc.rootVisualElement.panel, pos, cam);
damageText.txt.style.left = newPos.x;
damageText.txt.style.bottom = newPos.y;
uiDoc.rootVisualElement.Q("Display").Add(damageText.txt);
StartCoroutine(anim.FadeOut(damageText.txt));
yield return new WaitForSeconds(1f);
uiDoc.rootVisualElement.Q("Display").Remove(damageText.txt);
}
And this is the result:
I’m not sure what to do, maybe it has something to do with the Panel? I feel like the position is correct, just “squeezed” in some manner.
Baste
April 22, 2025, 10:50am
4
Tried using the UITK debugger to make sure that there’s nothing else interacting with the layout? Could be some flex setup or whatever if they’re not absolutely positioned.
Now I’m at a computer with Unity, I can give a better example.
Namely that you should be assigning an elements transform.position, alongside ensuring the visual element has its position mode set to absolute.
You also need to convert the panel coordinates to a coordinate local to the parent element of the labels.
Here’s an example package demonstrating it:
WorldSpaceConversionExample.unitypackage (5.9 KB)
Which creates and positions some labels to where some transform components reside:
And the code for those that don’t want to download it, where the important part is at line 83:
namespace LBG.Testing
{
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public sealed class WorldSpaceConversionExample : MonoBehaviour
{
#region Inspector Fields
[SerializeField]
private Camera _camera;
[SerializeField]
private List<Transform> _transforms = new();
#endregion
#region Internal Members
private UIDocument _uiDocument;
private readonly List<TransformLabelPair> _transformLabelPairs = new();
#endregion
#region Unity Callbacks
private void Awake()
{
_uiDocument = GetComponent<UIDocument>();
}
private void OnEnable()
{
var root = _uiDocument.rootVisualElement;
var lanelContainer = root.Q<VisualElement>("label-container");
foreach (Transform t in _transforms)
{
var label = new Label();
lanelContainer.Add(label);
var pair = new TransformLabelPair(t, label);
_transformLabelPairs.Add(pair);
}
}
private void Update()
{
foreach (var pair in _transformLabelPairs)
{
pair.UpdateLabelPosition(_camera);
}
}
#endregion
#region Nested Types
public sealed class TransformLabelPair
{
#region Internal Members
private readonly Transform _transform;
private readonly Label _label;
#endregion
#region Constructors
public TransformLabelPair(Transform transform, Label label)
{
_transform = transform;
_label = label;
}
#endregion
#region Methods
public void UpdateLabelPosition(Camera camera)
{
Vector3 position = _transform.position;
var labelPanel = _label.panel;
Vector3 panelPosition = RuntimePanelUtils.CameraTransformWorldToPanel(labelPanel, position, camera);
var labelParent = _label.parent;
var localPosition = labelParent.WorldToLocal(panelPosition);
_label.transform.position = localPosition;
_label.text = _transform.position.ToString();
}
#endregion
}
#endregion
}
}
Overall, very straight forward.
Baste
April 22, 2025, 11:08am
6
VisualElement.transform is obsolete in Unity 6.2, so they’re expecting you to be able to do things with .style instead.
I guess style.position in that case then.
Edit: Or not. Apparently that’s the property for position mode . Well done naming that one, Unity.
The method can just be updated as such then:
public void UpdateLabelPosition(Camera camera)
{
Vector3 position = _transform.position;
var labelPanel = _label.panel;
Vector3 panelPosition = RuntimePanelUtils.CameraTransformWorldToPanel(labelPanel, position, camera);
var labelParent = _label.parent;
var localPosition = labelParent.WorldToLocal(panelPosition);
var style = _label.style;
style.top = localPosition.y;
style.left = localPosition.x;
_label.text = _transform.position.ToString();
}
Good suggestion!
As mentioned above, because of the deprecation, you should be able to use style.translate instead of transform on this example.
Following your instructions, I rewrote the function:
public IEnumerator DisplayDamage(string s, Vector3 pos)
{
DamageText damageText = new DamageText(allControls, s);
Vector3 panelPosition = RuntimePanelUtils.CameraTransformWorldToPanel(uiDoc.rootVisualElement.panel, pos, cam);
var labelParent = damageText.txt.parent;
var localPosition = labelParent.WorldToLocal(panelPosition);
var style = damageText.txt.style;
style.bottom = localPosition.y;
style.left = localPosition.x;
uiDoc.rootVisualElement.Q("Display").Add(damageText.txt);
StartCoroutine(anim.FadeOut(damageText.txt));
yield return new WaitForSeconds(1f);
uiDoc.rootVisualElement.Q("Display").Remove(damageText.txt);
}
And this is the result:
I think it’s relevant to tell what is printed with Debug.Log(labelParent):
TemplateContainer (x:0.00, y:0.00, width:NaN, height:NaN) world rect: (x:0.00, y:0.00, width:NaN, height:NaN)
I would follow Baste’s advice and use the UI Toolkit debugger to, well, debug this rather than just blindly copying our code. Getting this to work does depend on the right UI set up as well, and adding visual elements to the right parent. Make sure to download my example to see how I set it up.
Finally, I found the issue. Despite the position on the Label being set to Absolute, I had all the distances set to 0px. Unsetting them all to auto seems to have done the trick. This is the code now:
public IEnumerator DisplayDamage(string s, Vector3 pos)
{
DamageText damageText = new DamageText(allControls, s);
Vector3 panelPosition = RuntimePanelUtils.CameraTransformWorldToPanel(uiDoc.rootVisualElement.panel, pos, cam);
var labelParent = damageText.txt.parent;
var localPosition = labelParent.WorldToLocal(panelPosition);
damageText.txt.style.bottom = localPosition.y;
damageText.txt.style.left = localPosition.x;
uiDoc.rootVisualElement.Q("Display").Add(damageText.txt);
StartCoroutine(anim.FadeOut(damageText.txt));
yield return new WaitForSeconds(1f);
uiDoc.rootVisualElement.Q("Display").Remove(damageText.txt);
}
And this is the result:
It’s not too accurate but I can definitely work around that. Thanks everyone for your help!
EDIT: I accidentally set style.bottom value instead of style.top. Now it’s accurate.