兼容的菜单系统
use bevy::prelude::*;
use bevy::window::WindowResized;
use crate::raven::bevy_integration::{CanvasConfig, VirtualScreenScale, ScalableUI};
pub struct MenuPlugin;
impl Plugin for MenuPlugin {
fn build(&self, app: &mut App) {
println!(" MenuPlugin 正在构建...");
app
.add_systems(OnEnter(crate::raven::bevy_integration::GameState::Menu), setup_menu)
.add_systems(Update, (handle_buttons, handle_input, menu_window_resize_system).run_if(in_state(crate::raven::bevy_integration::GameState::Menu)))
.add_systems(OnExit(crate::raven::bevy_integration::GameState::Menu), cleanup_menu);
}
}
#[derive(Component)]
struct MenuUI;
#[derive(Component)]
struct VirtualScreen;
#[derive(Component)]
struct MenuBackground;
#[derive(Component)]
struct StartButton;
#[derive(Component)]
struct ExitButton;
fn setup_menu(mut commands: Commands, camera_query: Query<Entity, With<Camera2d>>, asset_server: Res<AssetServer>) {
if camera_query.is_empty() {
commands.spawn(Camera2d);
}
commands
.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),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::FlexEnd,
..default()
},
BackgroundColor(Color::BLACK),
GlobalZIndex(100),
VirtualScreen,
MenuUI,
ScalableUI::new().with_size(1920.0, 1080.0),
))
.with_children(|virtual_screen| {
virtual_screen.spawn((
Node {
width: Val::Px(720.0),
height: Val::Px(1080.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
top: Val::Px(0.0),
left: Val::Px(0.0),
..default()
},
MenuUI,
ScalableUI::new().with_size(720.0, 1080.0),
)).with_children(|menu_container| {
menu_container.spawn((
Text::new("Raven Engine"),
TextFont { font_size: 48.0, ..default() },
TextColor(Color::WHITE),
Node {
margin: UiRect::bottom(Val::Px(30.0)),
..default()
},
MenuUI,
ScalableUI::new().with_font_size(48.0),
));
menu_container.spawn((
Button,
Node {
width: Val::Px(200.0),
height: Val::Px(50.0),
margin: UiRect::all(Val::Px(40.0)),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
border: UiRect::all(Val::Px(2.0)),
..default()
},
BackgroundColor(Color::srgb(0.2, 0.5, 0.2)),
StartButton,
MenuUI,
ScalableUI::new().with_size(200.0, 50.0),
)).with_children(|button| {
button.spawn((
Text::new("Start Game"),
TextFont { font_size: 20.0, ..default() },
TextColor(Color::WHITE),
MenuUI,
ScalableUI::new().with_font_size(20.0),
));
});
menu_container.spawn((
Button,
Node {
width: Val::Px(200.0),
height: Val::Px(50.0),
margin: UiRect::all(Val::Px(10.0)),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
border: UiRect::all(Val::Px(2.0)),
..default()
},
BackgroundColor(Color::srgb(0.5, 0.2, 0.2)),
ExitButton,
MenuUI,
ScalableUI::new().with_size(200.0, 50.0),
)).with_children(|button| {
button.spawn((
Text::new("Exit"),
TextFont { font_size: 20.0, ..default() },
TextColor(Color::WHITE),
MenuUI,
ScalableUI::new().with_font_size(20.0),
));
});
});
});
commands.spawn((
Sprite {
image: asset_server.load("gui/main_menu.png"),
custom_size: Some(Vec2::new(1920.0, 1080.0)),
..default()
},
Transform::from_translation(Vec3::new(0.0, 0.0, 0.3)),
MenuBackground,
MenuUI,
));
commands.spawn((
Sprite {
image: asset_server.load("gui/game3.png"),
custom_size: Some(Vec2::new(1920.0, 1080.0)),
..default()
},
Transform::from_translation(Vec3::new(0.0, 0.0, 0.2)),
MenuUI,
));
}
fn menu_window_resize_system(
mut resize_events: EventReader<WindowResized>,
mut camera_query: Query<&mut Projection, With<Camera2d>>,
mut ui_query: Query<(&mut Node, &ScalableUI), Without<Text>>,
mut text_query: Query<(&mut TextFont, &ScalableUI), With<Text>>,
canvas_config: Res<CanvasConfig>,
mut virtual_scale: ResMut<VirtualScreenScale>,
) {
for event in resize_events.read() {
let window_width = event.width;
let window_height = event.height;
let virtual_width = canvas_config.width;
let virtual_height = canvas_config.height;
let scale_x = window_width / virtual_width;
let scale_y = window_height / virtual_height;
let scale = scale_x.min(scale_y);
let scaled_width = virtual_width * scale;
let scaled_height = virtual_height * scale;
let offset_x = (window_width - scaled_width) / 2.0;
let offset_y = (window_height - scaled_height) / 2.0;
virtual_scale.scale = scale;
virtual_scale.offset_x = offset_x;
virtual_scale.offset_y = offset_y;
let camera_scale = 1.0 / scale;
for mut projection in camera_query.iter_mut() {
if let Projection::Orthographic(ortho) = projection.as_mut() {
ortho.scale = camera_scale;
}
}
for (mut node, scalable_ui) in ui_query.iter_mut() {
if scalable_ui.original_width > 0.0 {
node.width = Val::Px(scalable_ui.original_width * scale);
}
if scalable_ui.original_height > 0.0 {
node.height = Val::Px(scalable_ui.original_height * scale);
}
node.margin = scale_ui_rect(&scalable_ui.original_margin, scale);
node.padding = scale_ui_rect(&scalable_ui.original_padding, scale);
node.border = scale_ui_rect(&scalable_ui.original_border, scale);
if scalable_ui.original_width == virtual_width && scalable_ui.original_height == virtual_height {
node.left = Val::Px(offset_x);
node.top = Val::Px(offset_y);
}
}
for (mut text_font, scalable_ui) in text_query.iter_mut() {
if let Some(original_font_size) = scalable_ui.original_font_size {
text_font.font_size = original_font_size * scale;
}
}
println!("菜单缩放: scale={:.3}, offset=({:.1}, {:.1})", scale, offset_x, offset_y);
}
}
fn scale_ui_rect(rect: &UiRect, scale: f32) -> UiRect {
UiRect {
left: scale_val(&rect.left, scale),
right: scale_val(&rect.right, scale),
top: scale_val(&rect.top, scale),
bottom: scale_val(&rect.bottom, scale),
}
}
fn scale_val(val: &Val, scale: f32) -> Val {
match val {
Val::Px(px) => Val::Px(*px * scale),
Val::Percent(percent) => Val::Percent(*percent),
_ => *val,
}
}
fn handle_buttons(
mut interaction_query: Query<(&Interaction, Option<&StartButton>, Option<&ExitButton>), (Changed<Interaction>, With<Button>)>,
mut next_state: ResMut<NextState<crate::raven::bevy_integration::GameState>>,
) {
for (interaction, start_button, exit_button) in &mut interaction_query {
if *interaction == Interaction::Pressed {
if start_button.is_some() {
println!("🎮 开始按钮被点击");
next_state.set(crate::raven::bevy_integration::GameState::Playing);
} else if exit_button.is_some() {
println!(" 退出按钮被点击");
std::process::exit(0);
}
}
}
}
fn handle_input(
keys: Res<ButtonInput<KeyCode>>,
mut next_state: ResMut<NextState<crate::raven::bevy_integration::GameState>>,
) {
if keys.just_pressed(KeyCode::Enter) {
println!("⌨️ ENTER 键被按下,开始游戏");
next_state.set(crate::raven::bevy_integration::GameState::Playing);
} else if keys.just_pressed(KeyCode::Escape) {
println!("⌨️ ESC 键被按下,退出游戏");
std::process::exit(0);
}
}
fn cleanup_menu(
mut commands: Commands,
query: Query<Entity, With<MenuUI>>,
bg_query: Query<Entity, With<MenuBackground>>,
) {
println!("正在清理菜单...");
for entity in &query {
println!("删除菜单实体: {:?}", entity);
commands.entity(entity).despawn();
}
for entity in &bg_query {
commands.entity(entity).despawn();
}
println!(" 菜单清理完成");
}