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.
[DefaultExecutionOrder(1000)]
[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
This layer draws the FREE-FLOATING text: damage and healing numbers, and any other caption with a life of its own. Overhead nameplates are not here — a name, a guild and a title are one plate with one anchor, drawn by UITKNameplateLayer into a sibling container beneath this one. See Nameplate for why they had to stop being a group of labels.
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 world-unit sizes (0.5 for damage and heals) and get the same apparent result at any distance.
• 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 number 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 label 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.
Execution order is part of the contract. Projection reads the camera transform,
and the camera is placed in PlayerInputController.LateUpdate (and
ArenaSpectatorCamera.LateUpdate). Both of those components are added to objects that
appear at world entry, long after this layer's own scene loads, so with a default execution
order Unity ran this LateUpdate FIRST and every label was projected against the camera
transform left over from the previous frame. That is not a constant lag: the owner's
character transform is written only on the network tick (30 Hz) and the camera is locked to
it, so the camera's per-frame delta is zero on most frames and a whole tick of travel on the
frames right after a tick — the stale read turned that into a tick-rate shimmer on every
world label whenever the player moved, while the meshes those labels sit above rendered
correctly. The positive order below puts this pass after every camera writer; UI Toolkit
repaints in PostLateUpdate, so the styles written here still land in the same
frame.
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.
That premise holds only while the CAMERA is still. Distance damps how fast a label crosses the screen when the labelled character moves; it does nothing at all when the viewer moves, and a camera turning at 180°/s sweeps a distant label across a third of the screen in three frames. Staggering through that is not an invisible saving, it is a three-frame staircase on the one kind of content — small, high-contrast text — that shows it most. So the stride is gated on FishMMO.Client.UITKWorldLabelLayer.cameraStill: it keeps the whole saving in the case it was written for, a player standing in a crowd, and gets out of the way the moment the view moves.
DistantUpdateInterval
Frame stride used for labels past DistantLodDistance.
[Tooltip("Frame stride for distant labels. 1 = every frame.")]
[Min(1)]
public int DistantUpdateInterval
Field Value
ExecutionOrder
Runs this layer's LateUpdate after every camera writer's. See the class remarks.
public const int ExecutionOrder = 1000
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
Remarks
This clamp is what a nameplate hits when the player walks up to a character, so it is the number that decides how large a name gets at its largest — not the authored world size, which only decides where the clamp starts biting. Against the 1200x800 reference panel (675 points tall at 16:9) a 0.25 world-unit nameplate reaches 24 points at about six metres and holds it inside that, which is 3.6% of screen height.
Retuned twice, downward both times: 96 filled a fifth of the screen, 48 was still reported as "slightly large" at 7.1%. The player's own Label Size multiplier scales this along with everything else (see FishMMO.Client.UITKWorldLabelLayer.labelScale), so anyone who wants the old size can still reach it — the default is what a player who never opens the options menu gets, and it should be the conservative end.
SERIALIZED IN ClientPreboot.unity AS WELL. The scene value is what ships and overrides this initialiser entirely, so changing one alone fixes nothing for a player.
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.