Skip to content

ImageNode 与 UI 文本

矩形和色块能搭出布局骨架,但真正的 UI 需要图片和文字。Bevy 提供 ImageNode 显示图片,用 Text + TextFont + TextColor 显示文字。

ImageNode

ImageNode 是一个需要图片资源的 UI 组件。它自动要求 Node,所以不需要手动添加:

rust
commands.spawn(ImageNode {
    image: asset_server.load("icons/heart.png"),
    ..default()
});

ImageNodecolor 字段默认是 Color::WHITE——白色调色与图片像素相乘,不改变原图颜色。改成其他颜色可以做着色效果(比如灰色变灰暗)。

如果不需要图片,只想用纯色方块,用 BackgroundColor 就够了——不需要 ImageNode

UI 文本

Bevy 的 UI 文本由三个组件协作:

  • Text::new("内容")——文本内容
  • TextFont——字体、字号
  • TextColor——文字颜色
rust
commands.spawn((
    Text::new("得分:12450"),
    TextFont {
        font_size: FontSize::Px(24.0),
        ..default()
    },
    TextColor(Color::WHITE),
));

TextFontfont 字段可以指定自定义字体(通过 asset_server.load())。不指定时使用 Bevy 内置的默认字体(FiraMono 的一个子集,只包含 ASCII)。

文本与 Node 的关系

Text 组件本身不要求 Node——它可以独立存在,由文本布局系统自动计算尺寸。但如果你需要控制文本的位置(比如绝对定位到右上角),就要同时给它一个 Node

rust
commands.spawn((
    Text::new("FPS: 60"),
    TextFont { font_size: FontSize::Px(14.0), ..default() },
    TextColor(Color::srgb(0.7, 0.7, 0.7)),
    Node {
        position_type: PositionType::Absolute,
        top: Val::Px(8.0),
        right: Val::Px(8.0),
        ..default()
    },
));

文本段落与多段文本

TextSpan 可以在一个 Text 下添加多个段落,每个段落有独立的字体和颜色:

rust
commands
    .spawn((
        Text::new("FPS: "),
        TextFont { font_size: FontSize::Px(24.0), ..default() },
    ))
    .with_child((
        TextSpan::default(),
        TextFont { font_size: FontSize::Px(24.0), ..default() },
        TextColor(Color::srgb(1.0, 0.8, 0.0)),
        FpsText,  // 标记组件,用于后续更新
    ));

TextSpan 的内容可以在运行时通过系统修改:

rust
fn update_fps(mut query: Query<&mut TextSpan, With<FpsText>>) {
    for mut span in &mut query {
        **span = format!("{:.1}", fps_value);
    }
}

完整 HUD 示例

下面的例子把前面所有知识点组合起来:用 PositionType::Absolute 做锚定定位,用 Vw/Vh 做响应式尺寸,用 Text 显示信息,用 BackgroundColor 做半透明面板:

rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, update_health_bar)
        .run();
}

#[derive(Component)]
struct HealthBar;

#[derive(Resource)]
struct PlayerHealth {
    current: f32,
    max: f32,
}

impl Default for PlayerHealth {
    fn default() -> Self {
        Self {
            current: 75.0,
            max: 100.0,
        }
    }
}

fn setup(mut commands: Commands) {
    commands.insert_resource(PlayerHealth::default());
    commands.spawn(Camera2d);

    // Root HUD container
    commands
        .spawn(Node {
            width: Val::Percent(100.0),
            height: Val::Percent(100.0),
            padding: UiRect::all(Val::Vw(2.0)),
            ..default()
        })
        .with_children(|parent| {
            // Bottom-left HUD panel
            parent
                .spawn((
                    Node {
                        position_type: PositionType::Absolute,
                        left: Val::Vw(2.0),
                        bottom: Val::Vw(2.0),
                        width: Val::Vw(28.0),
                        flex_direction: FlexDirection::Column,
                        padding: UiRect::all(Val::Px(12.0)),
                        row_gap: Val::Px(8.0),
                        border_radius: BorderRadius::all(Val::Px(8.0)),
                        ..default()
                    },
                    BackgroundColor(Color::srgb(0.0, 0.0, 0.0).with_alpha(0.6)),
                ))
                .with_children(|parent| {
                    // Health label
                    parent.spawn((
                        Text::new("HP"),
                        TextFont {
                            font_size: FontSize::Px(20.0),
                            ..default()
                        },
                        TextColor(Color::srgb(0.9, 0.9, 0.9)),
                    ));

                    // Health bar background
                    parent
                        .spawn(Node {
                            width: Val::Percent(100.0),
                            height: Val::Px(16.0),
                            border_radius: BorderRadius::all(Val::Px(4.0)),
                            ..default()
                        })
                        .with_children(|parent| {
                            // Health bar fill
                            parent.spawn((
                                Node {
                                    width: Val::Percent(75.0),
                                    height: Val::Percent(100.0),
                                    border_radius: BorderRadius::all(Val::Px(4.0)),
                                    ..default()
                                },
                                BackgroundColor(Color::srgb(0.2, 0.8, 0.3)),
                                HealthBar,
                            ));
                        });
                });

            // Top-right info panel
            parent
                .spawn((
                    Node {
                        position_type: PositionType::Absolute,
                        right: Val::Vw(2.0),
                        top: Val::Vw(2.0),
                        flex_direction: FlexDirection::Column,
                        padding: UiRect::all(Val::Px(12.0)),
                        row_gap: Val::Px(4.0),
                        border_radius: BorderRadius::all(Val::Px(8.0)),
                        ..default()
                    },
                    BackgroundColor(Color::srgb(0.0, 0.0, 0.0).with_alpha(0.6)),
                ))
                .with_children(|parent| {
                    parent.spawn((
                        Text::new("Score: 12450"),
                        TextFont {
                            font_size: FontSize::Px(18.0),
                            ..default()
                        },
                        TextColor(Color::WHITE),
                    ));
                    parent.spawn((
                        Text::new("Level: 7"),
                        TextFont {
                            font_size: FontSize::Px(14.0),
                            ..default()
                        },
                        TextColor(Color::srgb(0.7, 0.7, 0.7)),
                    ));
                });
        });
}

fn update_health_bar(
    health: Res<PlayerHealth>,
    mut query: Query<&mut Node, With<HealthBar>>,
) {
    let ratio = health.current / health.max;
    for mut node in &mut query {
        node.width = Val::Percent(ratio * 100.0);
    }
}

Listing 28-8:响应式 HUD——血条、得分、等级面板(examples/listing-28-08.rs)

几个要点:

  • 半透明背景——Color::srgb(0.0, 0.0, 0.0).with_alpha(0.6) 创建 60% 不透明度的黑色面板,文字浮在游戏画面上方。
  • flex_direction: FlexDirection::Column——面板内部垂直排列标签和血条。
  • flex_grow: 1.0——血条背景在水平方向填满面板,血条填充宽度用 Val::Percent 控制。
  • update_health_bar 系统——通过修改 Nodewidth 字段动态改变血条长度。Bevy 的布局系统每帧重新计算,所以直接改值就行。

小结:ImageNode 显示图片,Text + TextFont + TextColor 显示文字。文本可以用 TextSpan 拆分多段。HUD 典型模式是 PositionType::Absolute + Vw/Vh 响应式尺寸 + 半透明背景。