Can't put UI Elements on top of GameObjects

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.

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.

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.