> 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/docs-zh/ui/world-space-ui.md).

# 世界空间 UI

世界空间 UI 绘制在 2D 世界中，而不是屏幕上。可用于姓名牌、生命条、标牌，以及任何应感觉固定在游戏对象上的文本。

{% hint style="info" %}
世界空间 UI 使用 **米** 进行尺寸设置。如果你使用屏幕空间的点大小，你的 UI 会大得惊人。
{% endhint %}

### 核心规则

* 从该玩家的 `ao_late_update` 下的 `is_local_or_server()`.
* 对于锚定到移动实体的 UI，请调用 `UI.begin_world_space_ui(entity)` 和 `defer UI.end_world_space_ui()` 这样渲染插值就会跟随该实体。
* 使用 `UI.push_world_draw_context()` 仅用于未锚定的世界绘制。
* 矩形尺寸和偏移量请使用米。
* 为了正确进行深度排序，请推入 Z 值（通常使用 `pos.y`).
* 使用 `子矩形` 仅用于百分比填充（例如生命条）。

### 示例

#### 显示玩家等级

```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, "Lvl %", {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);
    }
}
```

### 教程箭头

实体重载包含目标的插值偏移：

```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.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 值和间距。
