Class UITKWorldLabelLayer
Draws every active WorldLabel as a UI Toolkit element, projecting each one from its world position onto this screen-space panel each frame.
[DisallowMultipleComponent]
[RequireComponent(typeof(UIDocument))]
public sealed class UITKWorldLabelLayer : MonoBehaviour
- Inheritance
-
ObjectComponentBehaviourMonoBehaviourUITKWorldLabelLayer
- Inherited Members
-
MonoBehaviour.IsInvoking()MonoBehaviour.CancelInvoke()MonoBehaviour.StopCoroutine(Coroutine)MonoBehaviour.StopAllCoroutines()MonoBehaviour.destroyCancellationTokenMonoBehaviour.useGUILayoutMonoBehaviour.didStartMonoBehaviour.didAwakeMonoBehaviour.runInEditModeBehaviour.enabledBehaviour.isActiveAndEnabledComponent.GetComponent<T>()Component.TryGetComponent<T>(out T)Component.GetComponentInChildren<T>()Component.GetComponentsInChildren<T>()Component.GetComponentInParent<T>()Component.GetComponentsInParent<T>()Component.GetComponents<T>()Component.GetComponentIndex()Component.CompareTag(TagHandle)Component.transformComponent.transformHandleComponent.gameObjectComponent.tagObject.GetEntityId()Object.GetInstanceID()Object.GetHashCode()Object.InstantiateAsync<T>(T)Object.InstantiateAsync<T>(T, Transform)Object.InstantiateAsync<T>(T, Vector3, Quaternion)Object.InstantiateAsync<T>(T, Transform, Vector3, Quaternion)Object.Instantiate(Object, Vector3, Quaternion)Object.Instantiate(Object, Vector3, Quaternion, Transform)Object.Instantiate(Object)Object.Instantiate(Object, Scene)Object.Instantiate<T>(T, InstantiateParameters)Object.Instantiate<T>(T, Vector3, Quaternion, InstantiateParameters)Object.Instantiate(Object, Transform)Object.Instantiate<T>(T)Object.Instantiate<T>(T, Vector3, Quaternion)Object.Instantiate<T>(T, Vector3, Quaternion, Transform)Object.Instantiate<T>(T, Transform)Object.Destroy(Object)Object.DestroyImmediate(Object)Object.DontDestroyOnLoad(Object)Object.DestroyObject(Object)Object.FindObjectsOfType<T>()Object.FindObjectsByType<T>(FindObjectsSortMode)Object.FindObjectsByType<T>(FindObjectsInactive, FindObjectsSortMode)Object.FindObjectOfType<T>()Object.FindFirstObjectByType<T>()Object.FindAnyObjectByType<T>()Object.FindFirstObjectByType<T>(FindObjectsInactive)Object.FindAnyObjectByType<T>(FindObjectsInactive)Object.FindObjectsByType<T>()Object.FindObjectsByType<T>(FindObjectsInactive)Object.ToString()Object.nameObject.hideFlags
Remarks
UI Toolkit has no world-space render mode, so the labels that used to be TextMeshPro
components sitting in the scene are now plain data (WorldLabel) that this
layer positions. RuntimePanelUtils.CameraTransformWorldToPanel does the projection,
which is the supported path and accounts for the panel's own scaling — computing screen
coordinates by hand and dividing by a scale factor drifts as soon as the reference
resolution or DPI changes.
Two behaviours of true 3D text are reproduced deliberately rather than dropped:
• Perspective scaling. A world-unit font size is converted to panel points using the camera's vertical FOV and the label's distance, so distant labels shrink exactly as they did when they were geometry. Callers keep passing the same world-unit sizes they always did (1 for nameplates, 2 for damage, 4 for heals) and get the same apparent result.
• Depth ordering. Elements are reordered back-to-front by distance so a near label overlaps a far one. UI Toolkit paints in hierarchy order and has no depth buffer, so without this the draw order would be creation order, and a distant nameplate could sit on top of one right in front of the player.
What is not reproduced is occlusion by scene geometry: 3D text was hidden behind walls by the depth buffer, and a screen-space panel has none. OccludeBehindGeometry restores it with a physics raycast per label, off by default because it costs a raycast per label per frame and most labels are on targets the player can already see.
Why this class is written the way it is. It runs once per label per frame in the middle of combat, which makes it the client's hottest UI code, and the obvious implementation of every one of its jobs is the expensive one:
• Position goes through translate, never left/top. Writing
left/top marks the element's layout dirty, and Yoga then re-solves the
whole container — every frame, for every label. translate is a transform: it moves
the painted result and touches no layout at all. Centring on the anchor still needs a
percentage of the label's own size, and one element gets one translate, so each label is
an anchor element carrying the per-frame position and a text child
carrying the static -50% / -100% centring. The extra element buys back a full
relayout per frame.
• Elements are pooled. The GameObject side was already pooled; the elements were not,
so every damage number allocated a Label, an anchor and (previously) an
interpolated element name. That was the combat GC spike. Released elements go back to
FishMMO.Client.UITKWorldLabelLayer.elementPool with their per-label state cleared.
• Style writes are diffed. Colour, font size and text are compared against what was last pushed and only written when they actually differ, so a nameplate that is not changing costs one translate write per frame and nothing else.
• Depth order uses VisualElement.Sort. Re-adding every child to reorder is
O(N²) inside the hierarchy list and dirties the tree N times; Sort is one
O(N log N) pass and one dirty. It is also skipped entirely unless the existing order is
actually wrong, which is checked in a single allocation-free walk.
• There is a draw budget. MaxVisibleLabels caps how many labels are drawn at once; over budget, the nearest survive and the rest are hidden. Combined with the spawn budget in UITKLabelMaker this bounds the cost of a large AoE — or of a server that sends more labels than a client can reasonably draw.
Fields
DistantLodDistance
Beyond this distance a label is repositioned every DistantUpdateInterval frames instead of every frame. Zero disables the level of detail.
[Tooltip("Distance past which labels reposition every Nth frame instead of every frame. 0 = off.")]
[Min(0)]
public float DistantLodDistance
Field Value
Remarks
A label 60 metres away moves a fraction of a panel point per frame, so updating it at a third of the rate is invisible and removes two thirds of its per-frame cost. Nearby labels — the ones a player is actually reading — are never staggered.
DistantUpdateInterval
Frame stride used for labels past DistantLodDistance.
[Tooltip("Frame stride for distant labels. 1 = every frame.")]
[Min(1)]
public int DistantUpdateInterval
Field Value
MaxFontSize
Largest font size, in panel points, a label is allowed to grow to.
[Tooltip("Upper clamp for projected font size, in panel points.")]
[Min(1)]
public float MaxFontSize
Field Value
MaxVisibleDistance
Labels further than this from the camera are hidden. Zero disables the cutoff.
[Tooltip("Hide labels beyond this distance from the camera. 0 = no limit.")]
[Min(0)]
public float MaxVisibleDistance
Field Value
MaxVisibleLabels
Hard cap on how many labels are drawn at once. Zero disables the cap.
[Tooltip("Maximum labels drawn per frame; the nearest win. 0 = no limit.")]
[Min(0)]
public int MaxVisibleLabels
Field Value
Remarks
The draw half of the label budget. When more labels than this want to be on screen the nearest ones win and the rest are hidden for the frame — a player standing in a large AoE sees the numbers that matter rather than paying for hundreds of overlapping ones. The spawn half lives in UITKLabelMaker; both are needed, because this cap alone still leaves the GameObjects allocated.
MinFontSize
Smallest font size, in panel points, a label is allowed to shrink to.
[Tooltip("Lower clamp for projected font size, in panel points.")]
[Min(1)]
public float MinFontSize
Field Value
OccludeBehindGeometry
When true, a label with scene geometry between it and the camera is hidden.
[Tooltip("Hide labels blocked by scene geometry. Costs a raycast per label per frame.")]
public bool OccludeBehindGeometry
Field Value
Remarks
Costs one raycast per visible label per frame. Off by default; see the class remarks.
OcclusionMask
Layers treated as occluders when OccludeBehindGeometry is on.
[Tooltip("Layers that block labels when occlusion is enabled.")]
public LayerMask OcclusionMask
Field Value
- LayerMask
OffScreenMargin
Panel-point margin outside the panel within which a label is still drawn.
[Tooltip("Panel-point margin outside the viewport within which labels are still drawn.")]
[Min(0)]
public float OffScreenMargin
Field Value
Remarks
A label whose anchor is just off the edge can still have visible text, because the text is centred on the anchor and extends past it. The margin keeps that case correct while still culling the labels that are genuinely nowhere near the screen — which, with a wide draw distance, is most of them.
ProjectionCamera
Camera used for projection. Falls back to UnityEngine.Camera.main when unset.
[Tooltip("Camera used to project world positions. Defaults to Camera.main.")]
public Camera ProjectionCamera
Field Value
- Camera
Properties
Instance
The active layer, or null when no client scene is loaded.
public static UITKWorldLabelLayer Instance { get; }
Property Value
ScreenContainer
Container for labels positioned in screen space rather than projected from the world.
public VisualElement ScreenContainer { get; }
Property Value
- VisualElement
Remarks
Shared with UITKAdvancedLabel so a transient on-screen caption — a region name, a zone banner — does not need a UIDocument and PanelSettings of its own. It draws above the projected labels because it is declared after them in the UXML.