Table of Contents

Class UITKWorldLabelLayer

Namespace
FishMMO.Client
Assembly
FishMMO.Client.dll

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
Object
Component
Behaviour
MonoBehaviour
UITKWorldLabelLayer
Inherited Members
MonoBehaviour.IsInvoking()
MonoBehaviour.CancelInvoke()
MonoBehaviour.StopCoroutine(Coroutine)
MonoBehaviour.StopAllCoroutines()
MonoBehaviour.destroyCancellationToken
MonoBehaviour.useGUILayout
MonoBehaviour.didStart
MonoBehaviour.didAwake
MonoBehaviour.runInEditMode
Behaviour.enabled
Behaviour.isActiveAndEnabled
Component.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.transform
Component.transformHandle
Component.gameObject
Component.tag
Object.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.name
Object.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

float

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

int

ExecutionOrder

Runs this layer's LateUpdate after every camera writer's. See the class remarks.

public const int ExecutionOrder = 1000

Field Value

int

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

float

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

float

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

int

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

float

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

bool

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

float

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

UITKWorldLabelLayer

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.