ImageNode 与 UI 文本
矩形和色块能搭出布局骨架,但真正的 UI 需要图片和文字。Bevy 提供 ImageNode 显示图片,用 Text + TextFont + TextColor 显示文字。
ImageNode
ImageNode 是一个需要图片资源的 UI 组件。它自动要求 Node,所以不需要手动添加:
commands.spawn(ImageNode {
image: asset_server.load("icons/heart.png"),
..default()
});ImageNode 的 color 字段默认是 Color::WHITE——白色调色与图片像素相乘,不改变原图颜色。改成其他颜色可以做着色效果(比如灰色变灰暗)。
如果不需要图片,只想用纯色方块,用 BackgroundColor 就够了——不需要 ImageNode。
UI 文本
Bevy 的 UI 文本由三个组件协作:
Text::new("内容")——文本内容TextFont——字体、字号TextColor——文字颜色
commands.spawn((
Text::new("得分:12450"),
TextFont {
font_size: FontSize::Px(24.0),
..default()
},
TextColor(Color::WHITE),
));TextFont 的 font 字段可以指定自定义字体(通过 asset_server.load())。不指定时使用 Bevy 内置的默认字体(FiraMono 的一个子集,只包含 ASCII)。
文本与 Node 的关系
Text 组件本身不要求 Node——它可以独立存在,由文本布局系统自动计算尺寸。但如果你需要控制文本的位置(比如绝对定位到右上角),就要同时给它一个 Node:
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 下添加多个段落,每个段落有独立的字体和颜色:
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 的内容可以在运行时通过系统修改:
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 做半透明面板:
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系统——通过修改Node的width字段动态改变血条长度。Bevy 的布局系统每帧重新计算,所以直接改值就行。
小结:ImageNode 显示图片,Text + TextFont + TextColor 显示文字。文本可以用 TextSpan 拆分多段。HUD 典型模式是 PositionType::Absolute + Vw/Vh 响应式尺寸 + 半透明背景。