From ec825214b53cdd642c2e36a0583b1cd3b940a518 Mon Sep 17 00:00:00 2001 From: Andrew William Watson Date: Tue, 20 Feb 2024 12:53:38 -0500 Subject: [PATCH 1/8] game_menu example: pull common patterns into methods --- examples/games/game_menu.rs | 746 ++++++++++++++---------------------- 1 file changed, 292 insertions(+), 454 deletions(-) diff --git a/examples/games/game_menu.rs b/examples/games/game_menu.rs index b6f1f533de8bc..07cdf6a4f60e1 100644 --- a/examples/games/game_menu.rs +++ b/examples/games/game_menu.rs @@ -4,7 +4,7 @@ use bevy::prelude::*; -const TEXT_COLOR: Color = Color::srgb(0.9, 0.9, 0.9); +const TEXT_COLOR: Color = Color::rgb(0.9, 0.9, 0.9); // Enum that will be used as a global state for the game #[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)] @@ -45,10 +45,62 @@ fn setup(mut commands: Commands) { commands.spawn(Camera2dBundle::default()); } +// This outermost node encompasses the entire Window (100% width and height) +// and centers all content vertically (align_items) and horizontally (justify_content) +fn outer_node( + commands: &mut Commands, + marker: impl Component, + spawn_children: impl FnOnce(&mut ChildBuilder), +) { + commands + .spawn(( + NodeBundle { + style: Style { + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: Val::Percent(100.0), + height: Val::Percent(100.0), + ..default() + }, + ..default() + }, + marker, + )) + .with_children(spawn_children); +} + +// This node is always a direct child of the `outer` node, and arranges contents in a column, from top to bottom +fn inner_node>( + mut commands: &mut Commands, + marker: impl Component, + color: T, + spawn_children: impl FnOnce(&mut ChildBuilder), +) { + outer_node(&mut commands, marker, { + |parent| { + parent + .spawn(NodeBundle { + style: Style { + // This will display its children in a column, from top to bottom + flex_direction: FlexDirection::Column, + // `align_items` will align children on the cross axis. Here the main axis is + // vertical (column), so the cross axis is horizontal. This will center the + // children + align_items: AlignItems::Center, + ..default() + }, + background_color: color.into(), + ..default() + }) + .with_children(spawn_children); + } + }); +} + mod splash { use bevy::prelude::*; - use super::{despawn_screen, GameState}; + use super::{despawn_screen, outer_node, GameState}; // This plugin will display a splash screen with Bevy logo for 1 second before switching to the menu pub fn splash_plugin(app: &mut App) { @@ -72,22 +124,10 @@ mod splash { fn splash_setup(mut commands: Commands, asset_server: Res) { let icon = asset_server.load("branding/icon.png"); + // Display the logo - commands - .spawn(( - NodeBundle { - style: Style { - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - width: Val::Percent(100.0), - height: Val::Percent(100.0), - ..default() - }, - ..default() - }, - OnSplashScreen, - )) - .with_children(|parent| { + outer_node(&mut commands, OnSplashScreen, { + |parent| { parent.spawn(ImageBundle { style: Style { // This will set the logo to be 200px wide, and auto adjust its height @@ -97,7 +137,9 @@ mod splash { image: UiImage::new(icon), ..default() }); - }); + } + }); + // Insert the timer as a resource commands.insert_resource(SplashTimer(Timer::from_seconds(1.0, TimerMode::Once))); } @@ -115,12 +157,9 @@ mod splash { } mod game { - use bevy::{ - color::palettes::basic::{BLUE, LIME}, - prelude::*, - }; + use bevy::prelude::*; - use super::{despawn_screen, DisplayQuality, GameState, Volume, TEXT_COLOR}; + use super::{despawn_screen, inner_node, DisplayQuality, GameState, Volume, TEXT_COLOR}; // This plugin will contain the game. In this case, it's just be a screen that will // display the current settings for 5 seconds before returning to the menu @@ -142,87 +181,58 @@ mod game { display_quality: Res, volume: Res, ) { - commands - .spawn(( - NodeBundle { - style: Style { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - // center children - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - ..default() - }, - ..default() - }, - OnGameScreen, - )) - .with_children(|parent| { - // First create a `NodeBundle` for centering what we want to display - parent - .spawn(NodeBundle { - style: Style { - // This will display its children in a column, from top to bottom - flex_direction: FlexDirection::Column, - // `align_items` will align children on the cross axis. Here the main axis is - // vertical (column), so the cross axis is horizontal. This will center the - // children - align_items: AlignItems::Center, + inner_node(&mut commands, OnGameScreen, Color::BLACK, { + |parent| { + // Display two lines of text, the second one with the current settings + parent.spawn( + TextBundle::from_section( + "Will be back to the menu shortly...", + TextStyle { + font_size: 80.0, + color: TEXT_COLOR, ..default() }, - background_color: Color::BLACK.into(), + ) + .with_style(Style { + margin: UiRect::all(Val::Px(50.0)), ..default() - }) - .with_children(|parent| { - // Display two lines of text, the second one with the current settings - parent.spawn( - TextBundle::from_section( - "Will be back to the menu shortly...", - TextStyle { - font_size: 80.0, - color: TEXT_COLOR, - ..default() - }, - ) - .with_style(Style { - margin: UiRect::all(Val::Px(50.0)), + }), + ); + parent.spawn( + TextBundle::from_sections([ + TextSection::new( + format!("quality: {:?}", *display_quality), + TextStyle { + font_size: 60.0, + color: Color::BLUE, ..default() - }), - ); - parent.spawn( - TextBundle::from_sections([ - TextSection::new( - format!("quality: {:?}", *display_quality), - TextStyle { - font_size: 60.0, - color: BLUE.into(), - ..default() - }, - ), - TextSection::new( - " - ", - TextStyle { - font_size: 60.0, - color: TEXT_COLOR, - ..default() - }, - ), - TextSection::new( - format!("volume: {:?}", *volume), - TextStyle { - font_size: 60.0, - color: LIME.into(), - ..default() - }, - ), - ]) - .with_style(Style { - margin: UiRect::all(Val::Px(50.0)), + }, + ), + TextSection::new( + " - ", + TextStyle { + font_size: 60.0, + color: TEXT_COLOR, ..default() - }), - ); - }); - }); + }, + ), + TextSection::new( + format!("volume: {:?}", *volume), + TextStyle { + font_size: 60.0, + color: Color::GREEN, + ..default() + }, + ), + ]) + .with_style(Style { + margin: UiRect::all(Val::Px(50.0)), + ..default() + }), + ); + } + }); + // Spawn a 5 seconds timer to trigger going back to the menu commands.insert_resource(GameTimer(Timer::from_seconds(5.0, TimerMode::Once))); } @@ -240,9 +250,9 @@ mod game { } mod menu { - use bevy::{app::AppExit, color::palettes::css::CRIMSON, prelude::*}; + use bevy::{app::AppExit, prelude::*}; - use super::{despawn_screen, DisplayQuality, GameState, Volume, TEXT_COLOR}; + use super::{despawn_screen, inner_node, DisplayQuality, GameState, Volume, TEXT_COLOR}; // This plugin manages the menu, with 5 different screens: // - a main menu with "New Game", "Settings", "Quit" @@ -321,10 +331,10 @@ mod menu { #[derive(Component)] struct OnSoundSettingsMenuScreen; - const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); - const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); - const HOVERED_PRESSED_BUTTON: Color = Color::srgb(0.25, 0.65, 0.25); - const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); + const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15); + const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25); + const HOVERED_PRESSED_BUTTON: Color = Color::rgb(0.25, 0.65, 0.25); + const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.75, 0.35); // Tag component used to mark which setting is currently selected #[derive(Component)] @@ -345,16 +355,16 @@ mod menu { // This system handles changing all buttons color based on mouse interaction fn button_system( mut interaction_query: Query< - (&Interaction, &mut UiImage, Option<&SelectedOption>), + (&Interaction, &mut BackgroundColor, Option<&SelectedOption>), (Changed, With