> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/all-out-docs/ru/ui/world-space-ui.md).

# UI в мировом пространстве

Используйте UI в мировом пространстве, чтобы выводить текст над головами игроков и любой другой текст, который должен ощущаться частью самого игрового мира.

UI в мировом пространстве рисуется в 2D-мире, а не на экране. Используйте его для табличек с именами, полос здоровья, знаков и любого текста, который должен ощущаться привязанным к игровым объектам.

{% hint style="info" %}
UI в мировом пространстве использует **метры** для задания размеров. Если вы используете размеры в экранных пунктах, ваш UI будет огромным.
{% endhint %}

### Основные правила

* Интерактивный UI должен использовать `ao_update`/`ao_late_update` (интерфейс игрока: `Player.ao_late_update` внутри `is_local_or_server()`), никогда `ao_draw`.
* `ao_draw` предназначен только для неинтерактивных декоративных визуальных элементов.
* Для UI, привязанного к движущемуся объекту, вызовите `UI.begin_world_space_ui(entity)` а `и отложите вызов UI.end_world_space_ui()` чтобы интерполяция отрисовки следовала за этим объектом.
* Используйте `UI.push_world_draw_context()` только для непривязанной отрисовки в мире.
* Используйте метры для размеров и смещений rect.
* Поднимайте Z для правильной сортировки по глубине (обычно используют `pos.y`).
* Используйте `subrect` только для процентного заполнения (например, для полос здоровья).

### Примеры

#### Отображение уровня игрока

```go
draw_player_level :: proc(player: Player) {
    UI.begin_world_space_ui(player.entity);
    defer UI.end_world_space_ui();

    pos := player.entity.world_position;
    UI.push_z(pos.y);
    defer UI.pop_z();

    ts := UI.default_text_settings();
    ts.size = 0.30; // Размер текста в мировом пространстве
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    text_pos := pos + v2{0, 1.7};
    rect := Rect{text_pos, text_pos}.grow(0.05, 0.4, 0.05, 0.4);
    UI.text(rect, ts, "Уровень %", {player.level});
}
```

#### Знаки базы игрока

```go
draw_base_sign :: proc(sign: Entity, label: string) {
    UI.begin_world_space_ui(sign);
    defer UI.end_world_space_ui();

    pos := sign.world_position;
    UI.push_z(pos.y);
    defer UI.pop_z();

    ts := UI.default_text_settings();
    ts.size = 0.30;
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    text_pos := pos + v2{0, 2.2};
    rect := Rect{text_pos, text_pos}.grow(0.06, 0.8, 0.06, 0.8);

    // Необязательная тёмная подложка для лучшей читаемости
    UI.quad(rect, core_globals.white_sprite, {0, 0, 0, 0.6});
    UI.text(rect, ts, label);
}
```

#### Полосы здоровья

```go
// Примечание: health/max_health — это определяемые пользователем поля в вашем собственном классе, а не встроенные поля Entity.
draw_world_ui :: proc(entity: My_Entity) {
    UI.begin_world_space_ui(entity.entity);
    defer UI.end_world_space_ui();

    pos := entity.entity.world_position;
    UI.push_z(pos.y);
    defer UI.pop_z();

    bar_pos := pos + v2{0, 1.5};
    bar_rect := Rect{bar_pos, bar_pos}.grow(0.1, 0.5, 0.1, 0.5);

    UI.quad(bar_rect, core_globals.white_sprite, {0, 0, 0, 0.8});

    health_pct := entity.health / entity.max_health;
    fill_rect := bar_rect.inset(0.02).subrect(0, 0, health_pct, 1);
    fill_color := lerp(v4{1, 0, 0, 1}, {0, 1, 0, 1}, health_pct);
    UI.quad(fill_rect, core_globals.white_sprite, fill_color);
}
```

### Индикаторы прогресса

`World_Progress_Bar` предоставляет стандартную полосу в мировом пространстве:

```go
Health :: class : Component {
    current: int;
    maximum: int = 100;
}

draw_health_bar :: proc(health: Health) {
    UI.begin_world_space_ui(health.entity);
    defer UI.end_world_space_ui();

    maximum := max(1, health.maximum);
    progress := clamp(
        health.current.(float) / maximum.(float),
        0.0,
        1.0
    );

    options := World_Progress_Bar.default_options();
    options.y_bias = 1.5;
    World_Progress_Bar.draw(health.entity.world_position, progress, options);
}
```

Приведения сохраняют деление в формате с плавающей точкой, а ограниченный знаменатель предотвращает деление на ноль.

Для пользовательской полосы, заполнение которой меняется со временем, храните один `Float_Interpolation_Helper` на каждую полосу:

```go
Smooth_Bar :: class : Component {
    progress: float;
    fill_history: Float_Interpolation_Helper;

    draw :: method() {
        UI.begin_world_space_ui(entity);
        defer UI.end_world_space_ui();

        center := entity.world_position + v2{0, 1.5};
        rect := Rect{center, center}.grow(0.1, 0.5, 0.1, 0.5);
        UI.quad(rect, core_globals.white_sprite, {0, 0, 0, 1});

        params: Quad_Params;
        params.fill = UI.quad_fill(
            fill_history.update(clamp(progress, 0.0, 1.0)),
            .RIGHT
        );
        UI.quad(rect.inset(0.02), core_globals.white_sprite, {0.1, 1, 0.1, 1}, params);
    }
}
```

Радиальные заполнения начинаются с 12 часов и используют знаковые значения: `-1..0` заполняет против часовой стрелки, а `0..1` заполняется по часовой стрелке. Для анимируемой радиальной перезарядки передайте `fill_history.update(clamp(amount, -1, 1))` до `UI.quad_fill(..., .RADIAL)`. Не ограничивайте до `0..1` если только поведение только по часовой стрелке не является намеренным.

### Стрелки туториала

Перегрузка для entity включает смещение интерполяции цели:

```go
draw_target_arrow :: proc(player: Player, target: Entity) {
    options := Tutorial_Arrow.default_options();
    Tutorial_Arrow.draw(player, target, options);
}
```

Для позиции, которой управляет скрипт и которая движется, храните `Position_Interpolation_Helper` и передавайте его смещение:

```go
Player :: class : Player_Base {
    objective_position: v2;
    objective_history: Position_Interpolation_Helper;

    draw_objective_arrow :: method() {
        options := Tutorial_Arrow.default_options();
        offset := objective_history.update(objective_position);
        Tutorial_Arrow.draw(this, objective_position, options, offset);
    }

    ao_late_update :: method(dt: float) {
        if is_local_or_server() {
            draw_objective_arrow();
        }
    }
}
```

После того как вы переместили entity в новую позицию, вызовите `entity.mark_teleported()` чтобы интерполяция не размазывала переход между старой и новой позициями.

### Преобразование координат

```go
// Преобразовать мировую позицию в экранную
screen_pos := world_to_screen(entity.world_position);

// Преобразовать экранную позицию в мировую
world_pos := screen_to_world(get_mouse_screen_position());
```

### Советы

* Делайте мировой UI минималистичным и читаемым на расстоянии. Если у вас есть камера с масштабированием, не забудьте уменьшать размер текста при отдалении.
* Используйте `fit_aspect(texture.get_aspect())` если вы рисуете иконки в мировом пространстве.
* Если текст мерцает или накладывается сам на себя, проверьте значения Z и интервалы.
