World-space UI
WorldSpaceServiceandSusWorldSpacePanellive in sus-core. Prefer SusApp (auto__SusWorldSpacePanel__+WorldSpaceService.Default). Kit v1SusWorldTrackerServiceis legacy — do not use for new code (see services docs in the kit package).
1. Purpose
World-space components display UI above objects in the 3D world: health bars, name/level signs, damage pop-up. Elements “hang” over Transform unit and follow it when the camera and object move.
Positioning is done by WorldSpaceService (sus-core): every frame Tick() recalculates Camera.WorldToScreenPoint → panel coordinates and updates the left/top of the element.
2. Connection
Preferred: let SusApp create the world panel and wire WorldSpaceService.Default (__SusWorldSpacePanel__, PanelSettings.renderMode = WorldSpace). Opt out with UseWorldSpace(false) for pure 2D apps.
SusApp.Create(uiDocument)
.UseTheme(SusTheme.Dark)
.Mount<HomeScreen>();
// Bind after Mount — Default is already set
var hp = new SusHealthBar();
hp.Value.Value = 0.67f;
WorldSpaceService.BindToWorld(hp, unit.transform, offset: new Vector3(0, 2f, 0));Manual / tests (no SusApp): create a service, attach a driver, then bind:
// Variant-B markers live in a dedicated layer UNDER the screens — never the OverlayHost.
var markerLayer = SusBootstrap.GetOrCreateWorldMarkerLayer(uiDocument.rootVisualElement);
var world = new WorldSpaceService
{
MarkerLayer = markerLayer,
MainCamera = Camera.main
};
world.AttachDriver();
WorldSpaceService.Default = world;
var hp = new SusHealthBar();
world.Bind(hp, unit.transform, offset: new Vector3(0, 2f, 0));3. Components
3.1 SusHealthBar
Health bar 0..100%. Reacts to Prop<float> Value.
File: sus-kit/Components/world/SusHealthBar.sharq
Props:
| Prop | Type | Default | Destination |
|---|---|---|---|
Value | Prop<float> | 1f | Filling 0..1 (Health / MaxHealth) |
ShowLabel | Prop<bool> | true | Show label "67%" |
Style: 200×20px, dark track, green hb-fill bar (warning / critical thresholds), centered label.
CSS classes: sus-healthbar, hb-fill, hb-label
var hp = new SusHealthBar();
hp.Value.Value = 0.67f; // 67%
hp.ShowLabel.Value = false; // hide percentage
world.BindToWorld(hp, unit.transform, offset: new Vector3(0, 2f, 0));3.2 SusNameplate
A plate with the name and level of the unit.
File: sus-kit/Components/world/SusNameplate.sharq
Props:
| Prop | Type | Default | Destination |
|---|---|---|---|
UnitName | Prop<string> | "" | Unit name |
Level | Prop<int> | 0 | Level (0 = do not show) |
HpFraction | Prop<float> | -1f | Inline HP bar fill 0..1; < 0 hides the bar |
Align | Prop<string> | "start" | Layout of name + HP: start | center | end |
Style: name + optional level + optional HP strip (--sk-nameplate-hp-width, default 80px).
CSS classes: sus-nameplate, np-root, np-name, np-level, np-hp-bar, np-hp-fill, np-root--align-*
var np = new SusNameplate();
np.UnitName.Value = "Warrior";
np.Level.Value = 5; // will show "Lv.5"
np.HpFraction.Value = 0.75f; // show HP bar at 75%
np.Align.Value = "center";
world.BindToWorld(np, unit.transform, offset: new Vector3(0, 2.5f, 0));3.3 SusFloatingDamage
Pop-up damage: the number flies up and goes out in 1.2s, then self-removes from the hierarchy (RemoveFromHierarchy → WorldSpaceService auto-Unbind).
File: sus-kit/Components/world/SusFloatingDamage.sharq
Props:
| Prop | Type | Default | Destination |
|---|---|---|---|
Damage | Prop<int> | 0 | Damage/Heal Amount |
IsCritical | Prop<bool> | false | Critical Strike (Orange) |
IsHeal | Prop<bool> | false | Treatment (green, prefix “+”) |
Animation: 1.2s - upward movement 40px + fade-out (opacity: 1 → 0). On completion: RemoveFromHierarchy().
CSS classes: sus-floating-damage, fd-label
// From the pool
var dmg = pool.Get();
dmg.Damage.Value = 50;
dmg.IsCritical.Value = true;
world.BindToWorld(dmg, unit.transform, offset: new Vector3(0, 2.5f, 0));
// dmg will delete itself in 1.2sPool (recommended):
var pool = new SusObjectPool<SusFloatingDamage>(
createFunc: () => new SusFloatingDamage(),
onGet: d => { d.style.opacity = 1f; d.style.display = DisplayStyle.Flex; },
onRelease: d => d.RemoveFromHierarchy(),
maxSize: 20
);3.4 SusWorldMarker (core) + SusUnitNameplate (kit)
SusWorldMarker is a sus-core container ( sus-core/Runtime/World/SusWorldMarker.cs) — tracking / edge projection only. It holds no unit chrome. Put kit content (typically SusUnitNameplate) as a child.
Container props (core):
| Prop | Type | Default | Destination |
|---|---|---|---|
Target | Prop<Transform> | null | Transform to follow |
WorldOffset | Prop<Vector3> | (0,2,0) | Offset over object |
Camera | Prop<Camera> | null | Falls back to WorldSpaceService.Camera |
UpdateRate | Prop<int> | 16 | Reposition interval (ms) |
SusUnitNameplate — sus-kit/Components/world/SusUnitNameplate.sharq (unit chrome: name, level, faction, icon, optional HP).
| Prop | Type | Default | Destination |
|---|---|---|---|
UnitName | Prop<string> | "" | Display name |
Level | Prop<int> | 0 | Level |
Faction | Prop<string> | "" | ally | enemy | neutral |
Icon | Prop<string> | "" | Phosphor / icon alias |
HpFraction | Prop<float> | 1f | HP fill 0..1 |
ShowHealthBar | Prop<bool> | false | Show HP strip |
Target | Prop<Transform> | null | Optional auto-bind source |
AutoBind | Prop<bool> | true | Read IWorldUnit* from Target |
var marker = new SusWorldMarker();
marker.Target.Value = unit.transform;
marker.WorldOffset.Value = new Vector3(0, 2f, 0);
var plate = new SusUnitNameplate();
plate.UnitName.Value = "Warrior";
plate.ShowHealthBar.Value = true;
plate.HpFraction.Value = 0.8f;
marker.Add(plate);
WorldSpaceService.BindToWorld(marker, unit.transform, marker.WorldOffset.Value);Edge projection helpers: WorldSpaceService.GetEdgePosition() ( EdgeSide.Left/Right/Top/Bottom).
4. WorldSpaceService API
Service in sus-core/Runtime/Services/WorldSpaceService.cs.
| Method | Destination |
|---|---|
BindToWorld(el, target, offset) | Bind element to Transform. Preferred: world panel; fallback: WorldMarkerLayer (below screens). Returns WorldBinding. |
Unbind(el) | Unbind, remove from world panel / marker layer. |
UnbindTarget(transform) | Remove all bindings from the given Transform. |
Tick() | Frame-by-frame recalculation of positions. Call from LateUpdate. |
AttachDriver() | Create a GameObject with WorldSpaceDriver (LateUpdate → Tick). |
Properties:
| Property | Type | Destination |
|---|---|---|
WorldSpacePanel | SusWorldSpacePanel | Preferred host (variant A, under screens) |
MarkerLayer | VisualElement (WorldMarkerLayer) | Variant-B screen-space host, inserted below screens |
OverlayHost | OverlayHost | Deprecated legacy fallback — avoid (host paints over screens) |
MainCamera / Camera | Camera | Camera for WorldToScreenPoint (fallback path) |
Count | int | Instance: number of active bindings |
BindingCount | int (static) | WorldSpaceService.BindingCount — same count via Default / standalone |
UpdateIntervalMs | float | Update interval in ms (0 = every frame) |
WorldBinding:
| Field | Type | Destination |
|---|---|---|
Element | VisualElement | Anchored element |
Target | Transform | Target in a 3D world |
Offset | Vector3 | Offset in world units |
ClampToScreenEdges | bool | Press to the edges if the target is off-screen |
5. Tick behavior
Each Tick() call for each binding:
- If
Target == null→ auto-Unbind and removal from the marker layer. Camera.WorldToScreenPoint(Target.position + Offset).- If
z < 0(target behind the camera) →display: None. - Otherwise
display: Flex, translationScreenToPanel, centering. - If
ClampToScreenEdges = true→Mathf.Clampon the edges of the panel.
Early departure:
Count == 0/BindingCount == 0- nothing to update.UpdateIntervalMs > 0 && (now - lastTick) < interval— throttle.
6. Z-order
Preferred: world UI lives in a separate UIDocument / SusWorldSpacePanel (PanelSettings.renderMode = WorldSpace) that the camera draws under all screen UI. Screen overlays stay on the screen-space document.
World-space panel (separate UIDocument) ← variant A, UNDER screens
└── healthbar / nameplate / floating damage
Screen-space UIDocument (root)
├── WorldMarkerLayer ← first child = lowest (variant-B markers)
├── ScreenHost ← app content: Mount<T> / router SusRouteView
└── OverlayHost ← last child = always on top of screens
├── [Transition = 10] curtain
├── [Modal = 20] dialogues
├── [Tooltip = 30] tips
├── [Dropdown = 40] drops
├── [Toast = 45] snackbars
└── [Console = 50] dev-consoleFallback (variant B): if there is no world panel, WorldSpaceService places flat markers into a dedicated WorldMarkerLayer — the first child of the screen root, so it renders below screens and the OverlayHost. World markers must never sit in the OverlayHost (the top-most layer): a menu or HUD has to be able to cover them. Prefer the separate world panel when possible.
7. Option A - World-space panel (SusWorldSpacePanel)
Advanced / manual. Normally you don't create this yourself —
SusAppalready builds__SusWorldSpacePanel__and wiresWorldSpaceService.Default(see §2). This section is for manual setups (custom panel settings, tests, non-SusApp bootstraps).
For a true 3D render with perspective and overlapping geometry - SusWorldSpacePanel. This is a MonoBehaviour requiring UIDocument with PanelSettings.renderMode = WorldSpace.
Differences from option B:
- Elements live in a separate world-space panel, which the camera renders as part of the scene.
- The position is given by the container's
transform.position(not recalculated from WorldToScreenPoint). - Billboard: elements always look at the camera.
- Remote scale: elements do not become smaller/inflated at any distance.
Connection
Create an object in the scene or through code:
var go = new GameObject("WorldSpacePanel", typeof(UIDocument), typeof(SusWorldSpacePanel));
var doc = go.GetComponent<UIDocument>();
doc.panelSettings = Resources.Load<PanelSettings>("WorldSpacePanelSettings");
var panel = go.GetComponent<SusWorldSpacePanel>();
panel.TargetCamera = Camera.main;Integration with WorldSpaceService
var world = new WorldSpaceService { Camera = Camera.main };
world.AttachDriver();
// Switch to variant A
world.UseWorldSpacePanel(panel);
// Elements automatically go to the world-space panel
world.BindToWorld(hp, unit.transform, offset: new Vector3(0, 2f, 0));
// Return to variant B
world.UseScreenSpacePanel();API SusWorldSpacePanel
| Method | Destination |
|---|---|
AttachElement(el, target, offset) | Snap element to Transform in 3D |
DetachElement(el) | Unbind element |
DetachTarget(transform) | Untie everything with Transform |
DetachAll() | Clear all |
Properties to configure:
| Property | Type | Default | Destination |
|---|---|---|---|
EnableBillboard | bool | true | Always look at the camera |
EnableDistanceScaling | bool | true | Scale by distance |
BaseDistance | float | 10 | Distance at which scale = 1x |
MinScale | float | 0.5 | Minimum scale |
MaxScale | float | 2 | Maximum scale |
8. Limitations
- Option A: requires
PanelSettingsasset withrenderMode = WorldSpace- create in Editor (Assets → Create → UI Toolkit → Panel Settings). - Option B: elements are “flat” and do not overlap with the scene geometry.
RuntimePanelUtils.ScreenToPanelrequires the element to be in a panel.