跳转到内容

World-space UI

WorldSpaceServiceSusWorldSpacePanel 位于 sus-core。优先使用 SusApp(自动创建 __SusWorldSpacePanel__ + WorldSpaceService.Default)。Kit v1 的 SusWorldTrackerService 已经过时(legacy)——新代码不要使用它(参见 kit 包中的 services 文档)。


1. 用途

World-space 组件把 UI 显示在 3D 世界中的物体上方:血条、 姓名/等级牌、伤害飘字。这些元素“悬挂”在 Transform 单位之上,并在相机或物体移动时跟随。

定位由 WorldSpaceService(sus-core)完成:每帧 Tick() 重新计算 Camera.WorldToScreenPoint → panel 坐标, 并更新元素的 left/top


2. 接入

推荐做法:SusApp 创建 world panel 并接好 WorldSpaceService.Default__SusWorldSpacePanel__PanelSettings.renderMode = WorldSpace)。纯 2D 应用可以通过 UseWorldSpace(false) 关闭该功能。

csharp
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));

手动接入 / 测试(不使用 SusApp):创建一个 service,挂上 driver,然后绑定:

csharp
// 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. 组件

3.1 SusHealthBar

血条 0..100%。响应 Prop<float> Value

文件: sus-kit/Components/world/SusHealthBar.sharq

Props:

PropTypeDefaultDescription
ValueProp<float>1f填充比例 0..1(Health / MaxHealth)
ShowLabelProp<bool>true显示标签“67%”

样式: 200×20px,深色底槽,绿色 hb-fill 填充条(有 warning / critical 阈值),标签居中。

CSS classes: sus-healthbar, hb-fill, hb-label

csharp
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));

样式钩子

USS 类契约 — 无需改 C# 即可覆盖外观。状态通过这些类切换:

  • hb-fill
  • hb-label
  • sus-healthbar
  • sus-healthbar--critical
  • sus-healthbar--healthy
  • sus-healthbar--warning

3.2 SusNameplate

显示单位名称和等级的牌子。

文件: sus-kit/Components/world/SusNameplate.sharq

Props:

PropTypeDefaultDescription
UnitNameProp<string>""单位名称
LevelProp<int>0等级(0 = 不显示)
HpFractionProp<float>-1f内联血条填充比例 0..1;< 0 隐藏血条
AlignProp<string>"start"名称 + HP 的布局:start | center | end

样式: 名称 + 可选等级 + 可选 HP 条(--sk-nameplate-hp-width,默认 80px)。

CSS classes: sus-nameplate, np-root, np-name, np-level, np-hp-bar, np-hp-fill, np-root--align-*

csharp
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));

样式钩子

USS 类契约 — 无需改 C# 即可覆盖外观。状态通过这些类切换:

  • np-hp-bar
  • np-hp-fill
  • np-level
  • np-name
  • np-root
  • np-root--align-center
  • np-root--align-end
  • np-root--align-start
  • np-row
  • sus-slot

3.3 SusFloatingDamage

伤害飘字:数字向上飞出并在 1.2 秒内淡出,然后 自动从层级中移除(RemoveFromHierarchy → WorldSpaceService 自动 Unbind)。

文件: sus-kit/Components/world/SusFloatingDamage.sharq

Props:

PropTypeDefaultDescription
DamageProp<int>0伤害/治疗数值
IsCriticalProp<bool>false暴击(橙色)
IsHealProp<bool>false治疗(绿色,前缀“+”)

动画: 1.2 秒 - 向上位移 40px + 淡出(opacity: 1 → 0)。 完成后:RemoveFromHierarchy()

CSS classes: sus-floating-damage, fd-label

csharp
// 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.2s

对象池(推荐):

csharp
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
);

样式钩子

USS 类契约 — 无需改 C# 即可覆盖外观。状态通过这些类切换:

  • fd-label
  • sus-floating-damage
  • sus-floating-damage--crit
  • sus-floating-damage--default
  • sus-floating-damage--heal

3.4 SusWorldMarker(core)+ SusUnitNameplate(kit)

SusWorldMarker 是一个 sus-core 容器(sus-core/Runtime/World/SusWorldMarker.cs)—— 只负责跟踪 / 边缘投影,不含任何单位外观。把 kit 内容 (通常是 SusUnitNameplate)作为子元素放进去。

容器 Props(core):

PropTypeDefaultDescription
TargetProp<Transform>null要跟随的 Transform
WorldOffsetProp<Vector3>(0,2,0)相对物体的偏移
CameraProp<Camera>null回退到 WorldSpaceService.Camera
UpdateRateProp<int>16重新定位间隔(ms)

SusUnitNameplatesus-kit/Components/world/SusUnitNameplate.sharq(单位外观:名称、等级、阵营、图标、可选 HP)。

PropTypeDefaultDescription
UnitNameProp<string>""显示名称
LevelProp<int>0等级
FactionProp<string>""ally | enemy | neutral
IconProp<string>""Phosphor / 图标别名
HpFractionProp<float>1fHP 填充比例 0..1
ShowHealthBarProp<bool>false显示 HP 条
TargetProp<Transform>null可选的自动绑定来源
AutoBindProp<bool>trueTarget 读取 IWorldUnit*
csharp
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);

边缘投影辅助方法:WorldSpaceService.GetEdgePosition()EdgeSide.Left/Right/Top/Bottom)。

样式钩子

USS 类契约 — 无需改 C# 即可覆盖外观。状态通过这些类切换:

  • sus-unit-nameplate
  • sus-unit-nameplate__content
  • sus-unit-nameplate__faction
  • sus-unit-nameplate__faction--ally
  • sus-unit-nameplate__faction--enemy
  • sus-unit-nameplate__faction--neutral
  • sus-unit-nameplate__hp
  • sus-unit-nameplate__hp-fill
  • sus-unit-nameplate__hp-fill--critical
  • sus-unit-nameplate__hp-fill--warning
  • sus-unit-nameplate__icon
  • sus-unit-nameplate__level
  • sus-unit-nameplate__name
  • sus-unit-nameplate__plate
  • sus-unit-nameplate__row

4. WorldSpaceService API

服务位于 sus-core/Runtime/Services/WorldSpaceService.cs

MethodDescription
BindToWorld(el, target, offset)把元素绑定到 Transform。优先使用 world panel;回退到 WorldMarkerLayer(在屏幕之下)。返回 WorldBinding
Unbind(el)解除绑定,从 world panel / marker layer 中移除。
UnbindTarget(transform)移除给定 Transform 上的所有绑定。
Tick()逐帧重新计算位置。从 LateUpdate 调用。
AttachDriver()创建一个带 WorldSpaceDriver 的 GameObject(LateUpdate → Tick)。

属性:

PropertyTypeDescription
WorldSpacePanelSusWorldSpacePanel首选宿主(方案 A,位于屏幕之下)
MarkerLayerVisualElementWorldMarkerLayer方案 B 的 screen-space 宿主,插入在屏幕之下
OverlayHostOverlayHost已废弃的旧回退方案——避免使用(该宿主会盖在屏幕上面)
MainCamera / CameraCamera用于 WorldToScreenPoint 的相机(回退路径)
Countint实例:当前活跃绑定数量
BindingCountint(static)WorldSpaceService.BindingCount —— 通过 Default / 独立实例得到的同一个计数
UpdateIntervalMsfloat更新间隔(ms,0 = 每帧)

WorldBinding:

FieldTypeDescription
ElementVisualElement被锚定的元素
TargetTransform3D 世界中的目标
OffsetVector3世界单位下的偏移
ClampToScreenEdgesbool目标超出屏幕时贴到边缘

5. Tick 行为

每次 Tick() 调用,对每个绑定执行:

  1. Target == null自动 Unbind 并从 marker layer 中移除。
  2. Camera.WorldToScreenPoint(Target.position + Offset)
  3. z < 0(目标在相机后方)→ display: None
  4. 否则 display: Flex,通过 ScreenToPanel 转换坐标并居中。
  5. ClampToScreenEdges = true → 在 panel 边缘做 Mathf.Clamp

提前退出条件:

  • Count == 0 / BindingCount == 0 - 没有可更新的内容。
  • UpdateIntervalMs > 0 && (now - lastTick) < interval — 节流。

6. Z 序

推荐做法: world UI 放在独立UIDocument / SusWorldSpacePanelPanelSettings.renderMode = WorldSpace)中,由相机绘制在所有屏幕 UI 之下。 屏幕层的 overlay 保留在 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-console

回退方案(方案 B): 如果没有 world panel,WorldSpaceService 会把扁平的 标记放进一个专用的 WorldMarkerLayer——屏幕根的第一个子元素,因此它 渲染在屏幕以及 OverlayHost 之下。World 标记绝不能出现在 OverlayHost (最顶层)中:菜单或 HUD 必须能够遮住它们。尽可能优先使用独立的 world panel。


7. 方案 A - World-space panel(SusWorldSpacePanel)

进阶 / 手动接入。 通常你不需要自己创建它——SusApp 已经构建了 __SusWorldSpacePanel__ 并接好了 WorldSpaceService.Default(见 §2)。本节适用于 手动搭建的场景(自定义 panel settings、测试、非 SusApp 的引导流程)。

要实现带透视和几何遮挡的真正 3D 渲染 - 用 SusWorldSpacePanel。这是一个 MonoBehaviour,需要 UIDocument 配合 PanelSettings.renderMode = WorldSpace

与方案 B 的区别:

  • 元素位于独立的 world-space panel 中,相机会把它作为 场景的一部分来渲染。
  • 位置由容器的 transform.position 给出(不是根据 WorldToScreenPoint 重新计算)。
  • Billboard:元素始终面向相机。
  • 远距离缩放:元素在任何距离下都不会变小/变大。

接入

在场景中创建一个物体,或者通过代码创建:

csharp
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;

与 WorldSpaceService 集成

csharp
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

MethodDescription
AttachElement(el, target, offset)将元素锚定到 3D 中的 Transform
DetachElement(el)解除元素绑定
DetachTarget(transform)解除某个 Transform 上的所有绑定
DetachAll()清空全部

可配置属性:

PropertyTypeDefaultDescription
EnableBillboardbooltrue始终面向相机
EnableDistanceScalingbooltrue按距离缩放
BaseDistancefloat10缩放 = 1x 时的距离
MinScalefloat0.5最小缩放
MaxScalefloat2最大缩放

8. 限制

  • 方案 A:需要 renderMode = WorldSpacePanelSettings 资源 - 在 Editor 中创建(Assets → Create → UI Toolkit → Panel Settings)。
  • 方案 B:元素是“扁平”的,不会与场景几何体产生遮挡。
  • RuntimePanelUtils.ScreenToPanel 要求元素必须位于某个 panel 中。