启动菜单代码分析

1. 引入和常量定义

use bevy::prelude::*;

const VIRTUAL_WIDTH: f32 = 1920.0;
const VIRTUAL_HEIGHT: f32 = 1080.0;

use bevy::prelude::*; - 引入Bevy的所有常用组件和系统

定义虚拟屏幕尺寸为1920x1080,这是一个固定的参考分辨率,UI会基于这个尺寸设计,然后通过缩放适配不同的实际窗口大小

2. 资源结构体定义

#[derive(Resource)]
struct FontAssets {
    default: Handle<Font>,
}

#[derive(Resource)] - 将结构体标记为Bevy资源,可以在系统间共享

Handle - Bevy的资源句柄,指向已加载的字体文件

用于统一管理字体资源,避免重复加载

图片资源

#[derive(Resource)]
struct ImageAssets {
    background: Handle<Image>,
    main_menu: Handle<Image>,
}

管理两个图片资源:背景图和主菜单图
使用句柄系统,支持异步加载和内存管理

未整理的残片

我将逐个详细解释这个Bevy菜单系统的代码片段:

1. 引入和常量定义

use bevy::prelude::*;

const VIRTUAL_WIDTH: f32 = 1920.0;
const VIRTUAL_HEIGHT: f32 = 1080.0;
  • use bevy::prelude::*; - 引入Bevy的所有常用组件和系统
  • 定义虚拟屏幕尺寸为1920x1080,这是一个固定的参考分辨率,UI会基于这个尺寸设计,然后通过缩放适配不同的实际窗口大小

2. 资源结构体定义

字体资源

#[derive(Resource)]
struct FontAssets {
    default: Handle<Font>,
}
  • #[derive(Resource)] - 将结构体标记为Bevy资源,可以在系统间共享
  • Handle<Font> - Bevy的资源句柄,指向已加载的字体文件
  • 用于统一管理字体资源,避免重复加载

图片资源

#[derive(Resource)]
struct ImageAssets {
    background: Handle<Image>,
    main_menu: Handle<Image>,
}
  • 管理两个图片资源:背景图和主菜单图
  • 使用句柄系统,支持异步加载和内存管理

3. 状态管理

#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)]
enum AppState {
    #[default]
    Loading,
    Ready,
}
  • States - Bevy状态系统的标记trait
  • Loading - 资源加载状态(默认状态)
  • Ready - 资源加载完成,可以显示菜单
  • 状态驱动系统执行,确保只在正确时机运行对应逻辑

4. 组件标记

#[derive(Component)]
struct StartGameButton;
// ... 其他按钮组件
  • ECS架构中的组件,用作标记(Marker Components)
  • 不包含数据,仅用于标识特定实体的类型
  • 便于查询系统中找到特定按钮

5. 字体系统设计

字体大小枚举

#[derive(Clone, Copy)]
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,
}
  • 封装完整的字体配置信息
  • 提供默认实现,简化使用

6. 主函数和应用设置

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() - 状态切换时的一次性系统

7. 资源加载系统

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世界
  • 创建摄像机和简单的加载界面
  • 资源加载是非阻塞的,需要后续检查

8. 资源加载检查

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);
    }
}

工作流程:

  1. 检查每个资源的加载状态
  2. 使用matches!宏简化状态比较
  3. 所有资源加载完成后清理加载UI
  4. 切换到Ready状态

9. 主菜单UI设置

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()
}
  • 使用虚拟分辨率(1920x1080)
  • 垂直布局
  • 内容居中

背景图片层

// 背景图片 - 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((/* 类似结构 */));
  • 使用绝对定位
  • 两层图片叠加效果
  • 铺满整个虚拟屏幕

UI菜单层

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()
}

10. 按钮创建模式

每个按钮都遵循相同的模式:

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)),
    ));
});

设计要点:

  • 透明背景,悬停时会变色
  • 统一的尺寸和间距
  • 父子结构:按钮容器+文本内容

11. 按钮交互处理

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!("开始游戏按钮被点击!");
    }
}

12. UI自适应缩放

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;
    }
}

缩放逻辑:

  • 获取当前窗口尺寸
  • 计算X和Y方向的缩放比例
  • 取较小值确保UI完整显示
  • 保持1920x1080的宽高比

总结

这个菜单系统展现了现代游戏UI设计的几个关键特点:

  1. 状态驱动 - 清晰的加载→就绪状态转换
  2. 资源管理 - 统一的资源句柄系统
  3. 响应式设计 - 自适应不同分辨率
  4. 层次化布局 - 背景→UI→交互的清晰分层
  5. 组件化设计 - ECS架构的充分利用
  6. 用户体验 - 加载画面、悬停效果等细节

整个系统既保持了代码的清晰性,又提供了丰富的视觉效果和良好的用户体验。