World-space UI
WorldSpaceServiceиSusWorldSpacePanelживут в sus-core. Предпочитайте SusApp (авто__SusWorldSpacePanel__+WorldSpaceService.Default). Kit v1SusWorldTrackerService— legacy, не используйте для нового кода (см. документацию сервисов в пакете kit).
1. Назначение
World-space компоненты отображают UI над объектами в 3D-мире: полоски здоровья, таблички с именем/уровнем, всплывающий урон. Элементы «висят» над Transform юнита и следуют за ним при движении камеры и объекта.
Позиционирование выполняет WorldSpaceService (sus-core): каждый кадр Tick() пересчитывает Camera.WorldToScreenPoint → координаты панели и обновляет left/top элемента.
2. Подключение
Предпочтительно: пусть SusApp создаст world-панель и подключит WorldSpaceService.Default (__SusWorldSpacePanel__, PanelSettings.renderMode = WorldSpace). Отключается через UseWorldSpace(false) для чисто 2D-приложений.
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): создайте сервис, подключите драйвер, затем забиндите:
// 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:
| Prop | Тип | По умолчанию | Назначение |
|---|---|---|---|
Value | Prop<float> | 1f | Заполнение 0..1 (Health / MaxHealth) |
ShowLabel | Prop<bool> | true | Показывать подпись "67%" |
Стиль: 200×20px, тёмный трек, зелёная полоса hb-fill (пороги warning / critical), подпись по центру.
CSS-классы: 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));Хуки стиля
Контракт USS-классов — внешний вид переопределяется без правки C#. Состояния переключаются этими классами:
hb-fillhb-labelsus-healthbarsus-healthbar--criticalsus-healthbar--healthysus-healthbar--warning
3.2 SusNameplate
Табличка с именем и уровнем юнита.
Файл: sus-kit/Components/world/SusNameplate.sharq
Props:
| Prop | Тип | По умолчанию | Назначение |
|---|---|---|---|
UnitName | Prop<string> | "" | Имя юнита |
Level | Prop<int> | 0 | Уровень (0 = не показывать) |
HpFraction | Prop<float> | -1f | Заполнение встроенной HP-полосы 0..1; < 0 скрывает полосу |
Align | Prop<string> | "start" | Раскладка имени + HP: start | center | end |
Стиль: имя + опциональный уровень + опциональная HP-полоса (--sk-nameplate-hp-width, по умолчанию 80px).
CSS-классы: 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));Хуки стиля
Контракт USS-классов — внешний вид переопределяется без правки C#. Состояния переключаются этими классами:
np-hp-barnp-hp-fillnp-levelnp-namenp-rootnp-root--align-centernp-root--align-endnp-root--align-startnp-rowsus-slot
3.3 SusFloatingDamage
Всплывающий урон: число взлетает вверх и гаснет за 1.2с, после чего самоудаляется из иерархии (RemoveFromHierarchy → WorldSpaceService auto-Unbind).
Файл: sus-kit/Components/world/SusFloatingDamage.sharq
Props:
| Prop | Тип | По умолчанию | Назначение |
|---|---|---|---|
Damage | Prop<int> | 0 | Величина урона/лечения |
IsCritical | Prop<bool> | false | Критический удар (оранжевый) |
IsHeal | Prop<bool> | false | Лечение (зелёный, префикс «+») |
Анимация: 1.2с — движение вверх на 40px + затухание (opacity: 1 → 0). По завершении: RemoveFromHierarchy().
CSS-классы: 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.2sПул (рекомендуется):
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-labelsus-floating-damagesus-floating-damage--critsus-floating-damage--defaultsus-floating-damage--heal
3.4 SusWorldMarker (core) + SusUnitNameplate (kit)
SusWorldMarker — контейнер из sus-core (sus-core/Runtime/World/SusWorldMarker.cs) — только tracking / проекция на край экрана. Он не содержит chrome юнита. Кладите kit-контент (как правило, SusUnitNameplate) как дочерний элемент.
Props контейнера (core):
| Prop | Тип | По умолчанию | Назначение |
|---|---|---|---|
Target | Prop<Transform> | null | Transform для слежения |
WorldOffset | Prop<Vector3> | (0,2,0) | Смещение над объектом |
Camera | Prop<Camera> | null | Падает обратно на WorldSpaceService.Camera |
UpdateRate | Prop<int> | 16 | Интервал перепозиционирования (мс) |
SusUnitNameplate — sus-kit/Components/world/SusUnitNameplate.sharq (chrome юнита: имя, уровень, фракция, иконка, опционально HP).
| Prop | Тип | По умолчанию | Назначение |
|---|---|---|---|
UnitName | Prop<string> | "" | Отображаемое имя |
Level | Prop<int> | 0 | Уровень |
Faction | Prop<string> | "" | ally | enemy | neutral |
Icon | Prop<string> | "" | Phosphor / алиас иконки |
HpFraction | Prop<float> | 1f | Заполнение HP 0..1 |
ShowHealthBar | Prop<bool> | false | Показывать HP-полосу |
Target | Prop<Transform> | null | Опциональный источник для авто-биндинга |
AutoBind | Prop<bool> | true | Читать IWorldUnit* из 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);Хелперы проекции на край экрана: WorldSpaceService.GetEdgePosition() (EdgeSide.Left/Right/Top/Bottom).
Хуки стиля
Контракт USS-классов — внешний вид переопределяется без правки C#. Состояния переключаются этими классами:
sus-unit-nameplatesus-unit-nameplate__contentsus-unit-nameplate__factionsus-unit-nameplate__faction--allysus-unit-nameplate__faction--enemysus-unit-nameplate__faction--neutralsus-unit-nameplate__hpsus-unit-nameplate__hp-fillsus-unit-nameplate__hp-fill--criticalsus-unit-nameplate__hp-fill--warningsus-unit-nameplate__iconsus-unit-nameplate__levelsus-unit-nameplate__namesus-unit-nameplate__platesus-unit-nameplate__row
4. API WorldSpaceService
Сервис в sus-core/Runtime/Services/WorldSpaceService.cs.
| Метод | Назначение |
|---|---|
BindToWorld(el, target, offset) | Привязать элемент к Transform. Предпочтительно: world-панель; fallback: WorldMarkerLayer (под экранами). Возвращает WorldBinding. |
Unbind(el) | Отвязать, удалить из world-панели / marker-слоя. |
UnbindTarget(transform) | Удалить все биндинги для данного Transform. |
Tick() | Покадровый пересчёт позиций. Вызывать из LateUpdate. |
AttachDriver() | Создать GameObject с WorldSpaceDriver (LateUpdate → Tick). |
Свойства:
| Свойство | Тип | Назначение |
|---|---|---|
WorldSpacePanel | SusWorldSpacePanel | Предпочтительный хост (вариант A, под экранами) |
MarkerLayer | VisualElement (WorldMarkerLayer) | Screen-space хост варианта B, вставляется под экранами |
OverlayHost | OverlayHost | Устаревший legacy-fallback — избегать (хост рисуется поверх экранов) |
MainCamera / Camera | Camera | Камера для WorldToScreenPoint (fallback-путь) |
Count | int | Экземпляр: число активных биндингов |
BindingCount | int (static) | WorldSpaceService.BindingCount — то же число через Default / standalone |
UpdateIntervalMs | float | Интервал обновления в мс (0 = каждый кадр) |
WorldBinding:
| Поле | Тип | Назначение |
|---|---|---|
Element | VisualElement | Заякоренный элемент |
Target | Transform | Цель в 3D-мире |
Offset | Vector3 | Смещение в мировых единицах |
ClampToScreenEdges | bool | Прижимать к краям, если цель за пределами экрана |
5. Поведение Tick
Каждый вызов Tick() для каждого биндинга:
- Если
Target == null→ авто-Unbind и удаление из marker-слоя. Camera.WorldToScreenPoint(Target.position + Offset).- Если
z < 0(цель за камерой) →display: None. - Иначе
display: Flex, перевод черезScreenToPanel, центрирование. - Если
ClampToScreenEdges = true→Mathf.Clampпо краям панели.
Ранний выход:
Count == 0/BindingCount == 0— нечего обновлять.UpdateIntervalMs > 0 && (now - lastTick) < interval— троттлинг.
6. Z-order
Предпочтительно: world UI живёт в отдельном UIDocument / SusWorldSpacePanel (PanelSettings.renderMode = WorldSpace), который камера рисует под всем screen UI. Screen-оверлеи остаются на screen-space документе.
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 (вариант B): если world-панели нет, WorldSpaceService размещает плоские маркеры в выделенном WorldMarkerLayer — первом дочернем элементе screen-корня, поэтому он рендерится ниже экранов и OverlayHost. World-маркеры никогда не должны находиться в OverlayHost (самый верхний слой): меню или HUD должны иметь возможность их перекрыть. По возможности предпочитайте отдельную world-панель.
7. Вариант A — World-space панель (SusWorldSpacePanel)
Продвинутый / ручной сценарий. Обычно вы не создаёте это сами —
SusAppуже строит__SusWorldSpacePanel__и подключаетWorldSpaceService.Default(см. §2). Этот раздел для ручных настроек (кастомные panel settings, тесты, bootstrap без SusApp).
Для настоящего 3D-рендера с перспективой и перекрытием геометрии — SusWorldSpacePanel. Это MonoBehaviour, требующий UIDocument с PanelSettings.renderMode = WorldSpace.
Отличия от варианта B:
- Элементы живут в отдельной world-space панели, которую камера рендерит как часть сцены.
- Позиция задаётся
transform.positionконтейнера (не пересчитывается из WorldToScreenPoint). - Billboard: элементы всегда смотрят на камеру.
- Масштаб на расстоянии: элементы не уменьшаются/увеличиваются на любой дистанции.
Подключение
Создайте объект в сцене или через код:
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
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
| Метод | Назначение |
|---|---|
AttachElement(el, target, offset) | Привязать элемент к Transform в 3D |
DetachElement(el) | Отвязать элемент |
DetachTarget(transform) | Отвязать всё, что связано с Transform |
DetachAll() | Очистить всё |
Свойства для настройки:
| Свойство | Тип | По умолчанию | Назначение |
|---|---|---|---|
EnableBillboard | bool | true | Всегда смотреть на камеру |
EnableDistanceScaling | bool | true | Масштабировать по расстоянию |
BaseDistance | float | 10 | Расстояние, на котором scale = 1x |
MinScale | float | 0.5 | Минимальный масштаб |
MaxScale | float | 2 | Максимальный масштаб |
8. Ограничения
- Вариант A: требует asset
PanelSettingsсrenderMode = WorldSpace— создать в Editor (Assets → Create → UI Toolkit → Panel Settings). - Вариант B: элементы «плоские» и не перекрываются с геометрией сцены.
RuntimePanelUtils.ScreenToPanelтребует, чтобы элемент находился в панели.