use bevy::prelude::*;
const VIRTUAL_WIDTH: f32 = 1920.0;
const VIRTUAL_HEIGHT: f32 = 1080.0;
use bevy::prelude::*; - 引入Bevy的所有常用组件和系统
定义虚拟屏幕尺寸为1920x1080,这是一个固定的参考分辨率,UI会基于这个尺寸设计,然后通过缩放适配不同的实际窗口大小
struct FontAssets {
default: Handle<Font>,
}
#[derive(Resource)] - 将结构体标记为Bevy资源,可以在系统间共享
Handle - Bevy的资源句柄,指向已加载的字体文件
用于统一管理字体资源,避免重复加载
struct ImageAssets {
background: Handle<Image>,
main_menu: Handle<Image>,
}
管理两个图片资源:背景图和主菜单图
使用句柄系统,支持异步加载和内存管理
我将逐个详细解释这个Bevy菜单系统的代码片段:
use bevy::prelude::*;
const VIRTUAL_WIDTH: f32 = 1920.0;
const VIRTUAL_HEIGHT: f32 = 1080.0;
use bevy::prelude::*; - 引入Bevy的所有常用组件和系统
struct FontAssets {
default: Handle<Font>,
}
#[derive(Resource)] - 将结构体标记为Bevy资源,可以在系统间共享Handle<Font> - Bevy的资源句柄,指向已加载的字体文件
struct ImageAssets {
background: Handle<Image>,
main_menu: Handle<Image>,
}
enum AppState {
Loading,
Ready,
}
States - Bevy状态系统的标记traitLoading - 资源加载状态(默认状态)Ready - 资源加载完成,可以显示菜单
struct StartGameButton;
// ... 其他按钮组件
enum FontSize {
Small, Normal, Large, XLarge, XXLarge,
}
impl FontSize {
fn value(self) -> f32 {
match self {
FontSize::Small => 16.0,
// ...
}
}
}
Clone, Copy 使其可以轻松传递和复制struct FontConfig {
style: FontStyle,
size: FontSize,
color: Color,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_state::<AppState>()
.add_systems(Startup, load_assets)
.add_systems(Update, (
check_assets_loaded.run_if(in_state(AppState::Loading)),
update_ui_scale.run_if(in_state(AppState::Ready)),
handle_button_clicks.run_if(in_state(AppState::Ready)),
))
.add_systems(OnEnter(AppState::Ready), setup)
.run();
}
详细分析:
DefaultPlugins - 加载Bevy的核心插件(渲染、输入、音频等)init_state::<AppState>() - 初始化状态系统Startup - 在应用启动时运行一次Update - 每帧运行的系统run_if() - 条件运行,只在特定状态下执行OnEnter() - 状态切换时的一次性系统fn load_assets(
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
// 加载字体
let font_assets = FontAssets {
default: asset_server.load("fonts/SarasaFixedHC-Light.ttf"),
};
// 加载图片
let image_assets = ImageAssets {
background: asset_server.load("gui/game3.png"),
main_menu: asset_server.load("gui/main_menu.png"),
};
commands.insert_resource(font_assets);
commands.insert_resource(image_assets);
// 显示加载画面
commands.spawn((Camera2d, Camera::default()));
commands.spawn((/* 加载UI */));
}
关键点:
asset_server.load() - 异步加载资源commands.insert_resource() - 将资源注入ECS世界fn check_assets_loaded(
asset_server: Res<AssetServer>,
font_assets: Res<FontAssets>,
image_assets: Res<ImageAssets>,
mut next_state: ResMut<NextState<AppState>>,
mut commands: Commands,
loading_ui: Query<Entity, With<Node>>,
) {
let font_loaded = matches!(
asset_server.load_state(font_assets.default.id()),
bevy::asset::LoadState::Loaded
);
// ... 检查其他资源
if font_loaded && background_loaded && main_menu_loaded {
// 清除加载画面
for entity in loading_ui.iter() {
commands.entity(entity).despawn();
}
next_state.set(AppState::Ready);
}
}
工作流程:
matches!宏简化状态比较fn setup(
mut commands: Commands,
_font_assets: Res<FontAssets>,
image_assets: Res<ImageAssets>,
asset_server: Res<AssetServer>,
) {
// Logo设置
let logo_text = "Raven engine";
let logo_font_size = 48.0;
let logo_text_color = Color::srgb(1.0, 0.8, 0.0); // 金色
UI层次结构:
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::BLACK),
))
Node {
width: Val::Px(VIRTUAL_WIDTH),
height: Val::Px(VIRTUAL_HEIGHT),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
}
// 背景图片 - game3.png (底层)
parent.spawn((
Node {
width: Val::Px(1920.0),
height: Val::Px(1080.0),
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
..default()
},
ImageNode::new(image_assets.background.clone()),
));
// 主菜单图片 - main_menu.png (覆盖层)
parent.spawn((/* 类似结构 */));
parent.spawn((
SceneEntity,
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Row,
..default()
},
GlobalZIndex(100), // 确保在最上层
))
Node {
position_type: PositionType::Absolute,
width: Val::Percent(50.0), // 占左半屏
height: Val::Percent(100.0),
align_items: AlignItems::Start,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
padding: UiRect {
left: Val::Px(50.0),
right: Val::Px(30.0),
top: Val::Px(0.0),
bottom: Val::Px(70.0),
},
row_gap: Val::Px(20.0), // 子元素间距
..default()
}
每个按钮都遵循相同的模式:
parent.spawn((
StartGameButton, // 标记组件
Button, // Bevy按钮组件
Node { /* 布局 */ },
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.0)), // 透明背景
GlobalZIndex(55),
))
.with_children(|parent| {
parent.spawn((
Text::new("开始游戏"),
TextFont { /* 字体设置 */ },
TextColor(Color::srgb(0.9, 0.9, 0.9)),
));
});
设计要点:
fn handle_button_clicks(
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor),
(Changed<Interaction>, With<Button>),
>,
start_button_query: Query<&Interaction, (Changed<Interaction>, With<StartGameButton>)>,
// ... 其他按钮查询
) {
两层处理机制:
for (interaction, mut color) in &mut interaction_query {
match *interaction {
Interaction::Hovered => {
*color = BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.1));
}
Interaction::None => {
*color = BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.0));
}
Interaction::Pressed => {
*color = BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.2));
}
}
}
for interaction in &start_button_query {
if *interaction == Interaction::Pressed {
println!("开始游戏按钮被点击!");
}
}
fn update_ui_scale(
windows: Query<&Window>,
mut ui_scale: ResMut<UiScale>,
) {
if let Some(window) = windows.iter().next() {
let window_width = window.width();
let window_height = window.height();
// 计算缩放比例,保持宽高比
let scale_x = window_width / VIRTUAL_WIDTH;
let scale_y = window_height / VIRTUAL_HEIGHT;
let scale = scale_x.min(scale_y); // 取较小值,避免拉伸
ui_scale.0 = scale;
}
}
缩放逻辑:
这个菜单系统展现了现代游戏UI设计的几个关键特点:
整个系统既保持了代码的清晰性,又提供了丰富的视觉效果和良好的用户体验。