К содержимому

World-space UI

WorldSpaceService и SusWorldSpacePanel живут в sus-core. Предпочитайте SusApp (авто __SusWorldSpacePanel__ + WorldSpaceService.Default). Kit v1 SusWorldTrackerServicelegacy, не используйте для нового кода (см. документацию сервисов в пакете 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-приложений.

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): создайте сервис, подключите драйвер, затем забиндите:

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:

PropТипПо умолчаниюНазначение
ValueProp<float>1fЗаполнение 0..1 (Health / MaxHealth)
ShowLabelProp<bool>trueПоказывать подпись "67%"

Стиль: 200×20px, тёмный трек, зелёная полоса hb-fill (пороги warning / critical), подпись по центру.

CSS-классы: 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:

PropТипПо умолчаниюНазначение
UnitNameProp<string>""Имя юнита
LevelProp<int>0Уровень (0 = не показывать)
HpFractionProp<float>-1fЗаполнение встроенной HP-полосы 0..1; < 0 скрывает полосу
AlignProp<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-*

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 auto-Unbind).

Файл: sus-kit/Components/world/SusFloatingDamage.sharq

Props:

PropТипПо умолчаниюНазначение
DamageProp<int>0Величина урона/лечения
IsCriticalProp<bool>falseКритический удар (оранжевый)
IsHealProp<bool>falseЛечение (зелёный, префикс «+»)

Анимация: 1.2с — движение вверх на 40px + затухание (opacity: 1 → 0). По завершении: RemoveFromHierarchy().

CSS-классы: 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) — только tracking / проекция на край экрана. Он не содержит chrome юнита. Кладите kit-контент (как правило, SusUnitNameplate) как дочерний элемент.

Props контейнера (core):

PropТипПо умолчаниюНазначение
TargetProp<Transform>nullTransform для слежения
WorldOffsetProp<Vector3>(0,2,0)Смещение над объектом
CameraProp<Camera>nullПадает обратно на WorldSpaceService.Camera
UpdateRateProp<int>16Интервал перепозиционирования (мс)

SusUnitNameplatesus-kit/Components/world/SusUnitNameplate.sharq (chrome юнита: имя, уровень, фракция, иконка, опционально HP).

PropТипПо умолчаниюНазначение
UnitNameProp<string>""Отображаемое имя
LevelProp<int>0Уровень
FactionProp<string>""ally | enemy | neutral
IconProp<string>""Phosphor / алиас иконки
HpFractionProp<float>1fЗаполнение HP 0..1
ShowHealthBarProp<bool>falseПоказывать HP-полосу
TargetProp<Transform>nullОпциональный источник для авто-биндинга
AutoBindProp<bool>trueЧитать IWorldUnit* из Target
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. 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).

Свойства:

СвойствоТипНазначение
WorldSpacePanelSusWorldSpacePanelПредпочтительный хост (вариант A, под экранами)
MarkerLayerVisualElement (WorldMarkerLayer)Screen-space хост варианта B, вставляется под экранами
OverlayHostOverlayHostУстаревший legacy-fallback — избегать (хост рисуется поверх экранов)
MainCamera / CameraCameraКамера для WorldToScreenPoint (fallback-путь)
CountintЭкземпляр: число активных биндингов
BindingCountint (static)WorldSpaceService.BindingCount — то же число через Default / standalone
UpdateIntervalMsfloatИнтервал обновления в мс (0 = каждый кадр)

WorldBinding:

ПолеТипНазначение
ElementVisualElementЗаякоренный элемент
TargetTransformЦель в 3D-мире
OffsetVector3Смещение в мировых единицах
ClampToScreenEdgesboolПрижимать к краям, если цель за пределами экрана

5. Поведение Tick

Каждый вызов Tick() для каждого биндинга:

  1. Если Target == nullавто-Unbind и удаление из marker-слоя.
  2. Camera.WorldToScreenPoint(Target.position + Offset).
  3. Если z < 0 (цель за камерой) → display: None.
  4. Иначе display: Flex, перевод через ScreenToPanel, центрирование.
  5. Если ClampToScreenEdges = trueMathf.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-console

Fallback (вариант 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: элементы всегда смотрят на камеру.
  • Масштаб на расстоянии: элементы не уменьшаются/увеличиваются на любой дистанции.

Подключение

Создайте объект в сцене или через код:

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

МетодНазначение
AttachElement(el, target, offset)Привязать элемент к Transform в 3D
DetachElement(el)Отвязать элемент
DetachTarget(transform)Отвязать всё, что связано с Transform
DetachAll()Очистить всё

Свойства для настройки:

СвойствоТипПо умолчаниюНазначение
EnableBillboardbooltrueВсегда смотреть на камеру
EnableDistanceScalingbooltrueМасштабировать по расстоянию
BaseDistancefloat10Расстояние, на котором scale = 1x
MinScalefloat0.5Минимальный масштаб
MaxScalefloat2Максимальный масштаб

8. Ограничения

  • Вариант A: требует asset PanelSettings с renderMode = WorldSpace — создать в Editor (Assets → Create → UI Toolkit → Panel Settings).
  • Вариант B: элементы «плоские» и не перекрываются с геометрией сцены.
  • RuntimePanelUtils.ScreenToPanel требует, чтобы элемент находился в панели.