From caa4cf24ac29ce748593b7222ee268a856bfe1ce Mon Sep 17 00:00:00 2001 From: Zicklag Date: Wed, 13 Jul 2022 16:02:01 -0500 Subject: [PATCH 01/14] Split Main and Pause Menus Into Their Own Modules --- src/ui.rs | 205 ++----------------------------------------- src/ui/main_menu.rs | 112 +++++++++++++++++++++++ src/ui/pause_menu.rs | 92 +++++++++++++++++++ 3 files changed, 213 insertions(+), 196 deletions(-) create mode 100644 src/ui/main_menu.rs create mode 100644 src/ui/pause_menu.rs diff --git a/src/ui.rs b/src/ui.rs index f8b39818..84ca6945 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,35 +1,25 @@ use bevy::{prelude::*, utils::HashMap, window::WindowId}; -use bevy_egui::{ - egui::{self, style::Margin}, - EguiContext, EguiInput, EguiPlugin, EguiSettings, -}; -use bevy_fluent::Localization; +use bevy_egui::{egui, EguiContext, EguiInput, EguiPlugin, EguiSettings}; use iyes_loopless::prelude::*; use leafwing_input_manager::prelude::ActionState; -use crate::{ - assets::EguiFont, - audio::*, - config::EngineConfig, - input::MenuAction, - metadata::{localization::LocalizationExt, ButtonStyle, FontStyle, GameMeta}, - GameState, -}; - -use self::widgets::{bordered_button::BorderedButton, bordered_frame::BorderedFrame, EguiUIExt}; +use crate::{assets::EguiFont, audio::*, input::MenuAction, metadata::GameMeta, GameState}; pub mod hud; pub mod widgets; +pub mod main_menu; +pub mod pause_menu; + pub struct UIPlugin; impl Plugin for UIPlugin { fn build(&self, app: &mut App) { app.add_plugin(EguiPlugin) .add_system(handle_menu_input.run_if_resource_exists::()) - .add_enter_system(GameState::MainMenu, spawn_main_menu_background) + .add_enter_system(GameState::MainMenu, main_menu::spawn_main_menu_background) .add_enter_system(GameState::MainMenu, play_menu_music) - .add_exit_system(GameState::MainMenu, despawn_main_menu_background) + .add_exit_system(GameState::MainMenu, main_menu::despawn_main_menu_background) .add_exit_system(GameState::MainMenu, stop_menu_music) .add_system(hud::render_hud.run_in_state(GameState::InGame)) .add_system(update_egui_fonts) @@ -37,13 +27,13 @@ impl Plugin for UIPlugin { .add_system_set( ConditionSet::new() .run_in_state(GameState::Paused) - .with_system(pause_menu) + .with_system(pause_menu::pause_menu) .into(), ) .add_system_set( ConditionSet::new() .run_in_state(GameState::MainMenu) - .with_system(main_menu) + .with_system(main_menu::main_menu_ui) .into(), ); } @@ -182,180 +172,3 @@ fn update_ui_scale( } } } - -fn pause_menu( - mut commands: Commands, - mut egui_context: ResMut, - game: Res, - non_camera_entities: Query>, - mut camera_transform: Query<&mut Transform, With>, - localization: Res, -) { - let ui_theme = &game.ui_theme; - - egui::CentralPanel::default() - .frame(egui::Frame::none()) - .show(egui_context.ctx_mut(), |ui| { - let screen_rect = ui.max_rect(); - - let pause_menu_width = 300.0; - let x_margin = (screen_rect.width() - pause_menu_width) / 2.0; - let outer_margin = egui::style::Margin::symmetric(x_margin, screen_rect.height() * 0.2); - - BorderedFrame::new(&ui_theme.panel.border) - .margin(outer_margin) - .padding(ui_theme.panel.padding.into()) - .show(ui, |ui| { - ui.set_min_width(ui.available_width()); - - let heading_font = ui_theme - .font_styles - .get(&FontStyle::Heading) - .expect("Missing 'heading' font style") - .colored(ui_theme.panel.font_color); - - ui.vertical_centered(|ui| { - ui.themed_label(&heading_font, &localization.get("paused")); - - ui.add_space(10.0); - - let width = ui.available_width(); - - let continue_button = BorderedButton::themed( - ui_theme, - &ButtonStyle::Normal, - &localization.get("continue"), - ) - .min_size(egui::vec2(width, 0.0)) - .show(ui); - - // Focus continue button by default - if ui.memory().focus().is_none() { - continue_button.request_focus(); - } - - if continue_button.clicked() { - commands.insert_resource(NextState(GameState::InGame)); - } - - if BorderedButton::themed( - ui_theme, - &ButtonStyle::Normal, - &localization.get("main-menu"), - ) - .min_size(egui::vec2(width, 0.0)) - .show(ui) - .clicked() - { - // Clean up all entities other than the camera - for entity in non_camera_entities.iter() { - commands.entity(entity).despawn(); - } - // Reset camera position - let mut camera_transform = camera_transform.single_mut(); - camera_transform.translation.x = 0.0; - camera_transform.translation.y = 0.0; - - // Show the main menu - commands.insert_resource(NextState(GameState::MainMenu)); - } - }); - }) - }); -} - -#[derive(Component)] -struct MainMenuBackground; - -/// Spawns the background image for the main menu -fn spawn_main_menu_background(mut commands: Commands, game: Res, windows: Res) { - let window = windows.primary(); - let bg_handle = game.main_menu.background_image.image_handle.clone(); - let img_size = game.main_menu.background_image.image_size; - let ratio = img_size.x / img_size.y; - let height = window.height(); - let width = height * ratio; - commands - .spawn_bundle(SpriteBundle { - texture: bg_handle, - sprite: Sprite { - custom_size: Some(Vec2::new(width, height)), - ..default() - }, - ..default() - }) - .insert(MainMenuBackground); -} - -/// Despawns the background image for the main menu -fn despawn_main_menu_background( - mut commands: Commands, - background: Query>, -) { - let bg = background.single(); - commands.entity(bg).despawn(); -} - -/// Render the main menu UI -fn main_menu( - mut commands: Commands, - mut egui_context: ResMut, - game: Res, - localization: Res, - engine_config: Res, -) { - let ui_theme = &game.ui_theme; - - egui::CentralPanel::default() - .frame(egui::Frame::none()) - .show(egui_context.ctx_mut(), |ui| { - let screen_rect = ui.max_rect(); - - // Calculate a margin of 20% of the screen size - let outer_margin = screen_rect.size() * 0.20; - let outer_margin = Margin { - left: outer_margin.x, - right: outer_margin.x, - // Make top and bottom margins smaller - top: outer_margin.y / 1.5, - bottom: outer_margin.y / 1.5, - }; - - BorderedFrame::new(&ui_theme.panel.border) - .margin(outer_margin) - .padding(ui_theme.panel.padding.into()) - .show(ui, |ui| { - // Make sure the frame ocupies the entire rect that we allocated for it. - // - // Without this it would only take up enough size to fit it's content. - ui.set_min_size(ui.available_size()); - - // Create a vertical list of items, centered horizontally - ui.vertical_centered(|ui| { - ui.themed_label(&game.main_menu.title_font, &localization.get("title")); - - // Now switch the layout to bottom_up so that we can start adding widgets - // from the bottom of the frame. - ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { - let start_button = BorderedButton::themed( - ui_theme, - &ButtonStyle::Jumbo, - &localization.get("start-game"), - ) - .show(ui); - - // Focus the start button if nothing else is focused. That way you can - // play the game just by pressing Enter. - if ui.memory().focus().is_none() { - start_button.request_focus(); - } - - if start_button.clicked() || engine_config.auto_start { - commands.insert_resource(game.start_level_handle.clone()); - commands.insert_resource(NextState(GameState::LoadingLevel)); - } - }); - }); - }) - }); -} diff --git a/src/ui/main_menu.rs b/src/ui/main_menu.rs new file mode 100644 index 00000000..deb06312 --- /dev/null +++ b/src/ui/main_menu.rs @@ -0,0 +1,112 @@ +use bevy::prelude::*; +use bevy_egui::{egui::style::Margin, *}; +use bevy_fluent::Localization; +use iyes_loopless::state::NextState; + +use crate::{ + config::EngineConfig, + metadata::{localization::LocalizationExt, ButtonStyle, GameMeta}, + GameState, +}; + +use super::widgets::{bordered_button::BorderedButton, bordered_frame::BorderedFrame, EguiUIExt}; + +#[derive(Component)] +pub struct MainMenuBackground; + +/// Spawns the background image for the main menu +pub fn spawn_main_menu_background( + mut commands: Commands, + game: Res, + windows: Res, +) { + let window = windows.primary(); + let bg_handle = game.main_menu.background_image.image_handle.clone(); + let img_size = game.main_menu.background_image.image_size; + let ratio = img_size.x / img_size.y; + let height = window.height(); + let width = height * ratio; + commands + .spawn_bundle(SpriteBundle { + texture: bg_handle, + sprite: Sprite { + custom_size: Some(Vec2::new(width, height)), + ..default() + }, + ..default() + }) + .insert(MainMenuBackground); +} + +/// Despawns the background image for the main menu +pub fn despawn_main_menu_background( + mut commands: Commands, + background: Query>, +) { + let bg = background.single(); + commands.entity(bg).despawn(); +} + +/// Render the main menu UI +pub fn main_menu_ui( + mut commands: Commands, + mut egui_context: ResMut, + game: Res, + localization: Res, + engine_config: Res, +) { + let ui_theme = &game.ui_theme; + + egui::CentralPanel::default() + .frame(egui::Frame::none()) + .show(egui_context.ctx_mut(), |ui| { + let screen_rect = ui.max_rect(); + + // Calculate a margin of 20% of the screen size + let outer_margin = screen_rect.size() * 0.20; + let outer_margin = Margin { + left: outer_margin.x, + right: outer_margin.x, + // Make top and bottom margins smaller + top: outer_margin.y / 1.5, + bottom: outer_margin.y / 1.5, + }; + + BorderedFrame::new(&ui_theme.panel.border) + .margin(outer_margin) + .padding(ui_theme.panel.padding.into()) + .show(ui, |ui| { + // Make sure the frame ocupies the entire rect that we allocated for it. + // + // Without this it would only take up enough size to fit it's content. + ui.set_min_size(ui.available_size()); + + // Create a vertical list of items, centered horizontally + ui.vertical_centered(|ui| { + ui.themed_label(&game.main_menu.title_font, &localization.get("title")); + + // Now switch the layout to bottom_up so that we can start adding widgets + // from the bottom of the frame. + ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { + let start_button = BorderedButton::themed( + ui_theme, + &ButtonStyle::Jumbo, + &localization.get("start-game"), + ) + .show(ui); + + // Focus the start button if nothing else is focused. That way you can + // play the game just by pressing Enter. + if ui.memory().focus().is_none() { + start_button.request_focus(); + } + + if start_button.clicked() || engine_config.auto_start { + commands.insert_resource(game.start_level_handle.clone()); + commands.insert_resource(NextState(GameState::LoadingLevel)); + } + }); + }); + }) + }); +} diff --git a/src/ui/pause_menu.rs b/src/ui/pause_menu.rs new file mode 100644 index 00000000..081d31c4 --- /dev/null +++ b/src/ui/pause_menu.rs @@ -0,0 +1,92 @@ +use bevy::prelude::*; +use bevy_egui::*; +use bevy_fluent::Localization; +use iyes_loopless::state::NextState; + +use crate::{ + metadata::{localization::LocalizationExt, ButtonStyle, FontStyle, GameMeta}, + GameState, +}; + +use super::widgets::{bordered_button::BorderedButton, bordered_frame::BorderedFrame, EguiUIExt}; + +pub fn pause_menu( + mut commands: Commands, + mut egui_context: ResMut, + game: Res, + non_camera_entities: Query>, + mut camera_transform: Query<&mut Transform, With>, + localization: Res, +) { + let ui_theme = &game.ui_theme; + + egui::CentralPanel::default() + .frame(egui::Frame::none()) + .show(egui_context.ctx_mut(), |ui| { + let screen_rect = ui.max_rect(); + + let pause_menu_width = 300.0; + let x_margin = (screen_rect.width() - pause_menu_width) / 2.0; + let outer_margin = egui::style::Margin::symmetric(x_margin, screen_rect.height() * 0.2); + + BorderedFrame::new(&ui_theme.panel.border) + .margin(outer_margin) + .padding(ui_theme.panel.padding.into()) + .show(ui, |ui| { + ui.set_min_width(ui.available_width()); + + let heading_font = ui_theme + .font_styles + .get(&FontStyle::Heading) + .expect("Missing 'heading' font style") + .colored(ui_theme.panel.font_color); + + ui.vertical_centered(|ui| { + ui.themed_label(&heading_font, &localization.get("paused")); + + ui.add_space(10.0); + + let width = ui.available_width(); + + let continue_button = BorderedButton::themed( + ui_theme, + &ButtonStyle::Normal, + &localization.get("continue"), + ) + .min_size(egui::vec2(width, 0.0)) + .show(ui); + + // Focus continue button by default + if ui.memory().focus().is_none() { + continue_button.request_focus(); + } + + if continue_button.clicked() { + commands.insert_resource(NextState(GameState::InGame)); + } + + if BorderedButton::themed( + ui_theme, + &ButtonStyle::Normal, + &localization.get("main-menu"), + ) + .min_size(egui::vec2(width, 0.0)) + .show(ui) + .clicked() + { + // Clean up all entities other than the camera + for entity in non_camera_entities.iter() { + commands.entity(entity).despawn(); + } + // Reset camera position + let mut camera_transform = camera_transform.single_mut(); + camera_transform.translation.x = 0.0; + camera_transform.translation.y = 0.0; + + // Show the main menu + commands.insert_resource(NextState(GameState::MainMenu)); + } + }); + }) + }); +} From 39864b324b9895b34cb89707e678a3a3badf1c3f Mon Sep 17 00:00:00 2001 From: Zicklag Date: Wed, 13 Jul 2022 16:03:22 -0500 Subject: [PATCH 02/14] Add Doc Comment --- src/platform.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform.rs b/src/platform.rs index 959a91f3..051e2200 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -250,6 +250,7 @@ impl Storage { pub struct SaveTask(Receiver<()>); impl SaveTask { + /// Get whether or not saving has been completed. pub fn is_complete(&mut self) -> bool { !self.0.is_empty() } From 29c4271f8f9e68ebb4d0028b914d14feea898257 Mon Sep 17 00:00:00 2001 From: Zicklag Date: Thu, 14 Jul 2022 18:08:15 -0500 Subject: [PATCH 03/14] Establish Basic Settings Menu Structure --- Cargo.lock | 10 + Cargo.toml | 1 + assets/default.game.yaml | 62 ++--- assets/locales/en-US/main.ftl | 6 + src/config.rs | 11 + src/input.rs | 5 +- src/metadata/ui.rs | 6 +- src/ui.rs | 24 +- src/ui/main_menu.rs | 367 +++++++++++++++++++++++++++--- src/ui/pause_menu.rs | 6 +- src/ui/widgets/bordered_button.rs | 5 +- 11 files changed, 412 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c0372d6..5b5138e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1549,6 +1549,15 @@ dependencies = [ "nohash-hasher", ] +[[package]] +name = "egui_extras" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bfcce06463cdbcfd7f4efd57608b1384d6d9ae03b33e503fbba1d1a899a52" +dependencies = [ + "egui", +] + [[package]] name = "either" version = "1.7.0" @@ -3156,6 +3165,7 @@ dependencies = [ "bevy_mod_debugdump", "bevy_rapier2d", "directories", + "egui_extras", "fluent", "getrandom", "iyes_loopless", diff --git a/Cargo.toml b/Cargo.toml index b013b034..a10b234a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ anyhow = "1.0.58" bevy = "0.7.0" bevy-parallax = "0.1.2" bevy_egui = "0.14" +egui_extras = "0.18.0" bevy_kira_audio = { version = "0.10.0", features = ["mp3"] } bevy_rapier2d = { version = "0.14.1", features = ["debug-render"] } iyes_loopless = "0.6.1" diff --git a/assets/default.game.yaml b/assets/default.game.yaml index da116f8c..d5482865 100644 --- a/assets/default.game.yaml +++ b/assets/default.game.yaml @@ -8,7 +8,7 @@ main_menu: title_font: family: ark color: [0, 0, 0] - size: 55 + size: 40 background_image: image: ui/main-menu-background.png @@ -137,7 +137,14 @@ default_input_maps: GamepadButton: South - Single: GamepadButton: Start - Forward: + + Back: + - Single: + Keyboard: Escape + - Single: + GamepadButton: East + + Next: - Single: Keyboard: Down - Single: @@ -158,7 +165,7 @@ default_input_maps: axis: LeftStickX negative_low: -1.0 positive_low: 0.5 - Backward: + Previous: - Single: Keyboard: Left - Single: @@ -199,6 +206,14 @@ ui_theme: family: ark size: 30 color: [0, 0, 0] + bigger: + family: ark + size: 20 + color: [0, 0, 0] + normal: + family: ark + size: 15 + color: [0, 0, 0] hud: font: @@ -247,48 +262,9 @@ ui_theme: bottom: 11 left: 11 right: 11 - scale: 4.0 + scale: 3.0 button_styles: - jumbo: - font: - family: ark - color: [255, 255, 255] - size: 30 - padding: - top: 12 - left: 12 - right: 12 - bottom: 16 - borders: - default: - image: ui/green-button.png - image_size: [14, 14] - border_size: - top: 5 - bottom: 5 - right: 5 - left: 5 - scale: 3 - focused: - image: ui/green-button-focused.png - image_size: [14, 14] - border_size: - top: 5 - bottom: 5 - right: 5 - left: 5 - scale: 3 - clicked: - image: ui/green-button-down.png - image_size: [14, 14] - border_size: - top: 5 - bottom: 5 - right: 5 - left: 5 - scale: 3 - normal: font: family: ark diff --git a/assets/locales/en-US/main.ftl b/assets/locales/en-US/main.ftl index c4ee956c..950c34d9 100644 --- a/assets/locales/en-US/main.ftl +++ b/assets/locales/en-US/main.ftl @@ -3,6 +3,12 @@ title = Fish Fight Punchy start-game = Start Game +settings = Settings +quit = Quit +controls = Controls +sound = Sound +cancel = Cancel +save = Save # Pause Menu paused = Paused diff --git a/src/config.rs b/src/config.rs index 475c1b13..5844e762 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use structopt::StructOpt; #[derive(Clone, Debug, StructOpt)] @@ -27,6 +28,16 @@ pub struct EngineConfig { pub log_level: String, } +/// Global settings, stored and accessed through [`crate::platform::Storage`] +#[derive(Deserialize, Serialize, Default)] +pub struct Settings { + pub test: String, +} + +impl Settings { + pub const STORAGE_KEY: &'static str = "settings"; +} + impl EngineConfig { #[cfg(target_arch = "wasm32")] pub fn from_web_params() -> Self { diff --git a/src/input.rs b/src/input.rs index 6494b4ab..509b4d03 100644 --- a/src/input.rs +++ b/src/input.rs @@ -23,8 +23,9 @@ pub enum CameraAction { #[derive(Debug, Copy, Clone, Actionlike, Deserialize, Eq, PartialEq, Hash)] pub enum MenuAction { Confirm, - Forward, - Backward, + Back, + Next, + Previous, Pause, ToggleFullscreen, } diff --git a/src/metadata/ui.rs b/src/metadata/ui.rs index 56b23716..2a903763 100644 --- a/src/metadata/ui.rs +++ b/src/metadata/ui.rs @@ -21,6 +21,7 @@ pub struct UIThemeMeta { pub enum FontStyle { Heading, Normal, + Bigger, } impl TryFrom for FontStyle { @@ -30,6 +31,7 @@ impl TryFrom for FontStyle { use FontStyle::*; Ok(match value.as_str() { "heading" => Heading, + "bigger" => Bigger, "normal" => Normal, _ => { return Err("Invalid font style"); @@ -77,7 +79,6 @@ impl FontMeta { #[serde(deny_unknown_fields)] #[serde(try_from = "String")] pub enum ButtonStyle { - Jumbo, Normal, } @@ -87,10 +88,9 @@ impl TryFrom for ButtonStyle { fn try_from(value: String) -> Result { use ButtonStyle::*; Ok(match value.as_str() { - "jumbo" => Jumbo, "normal" => Normal, _ => { - return Err("Invalid font style"); + return Err("Invalid button style"); } }) } diff --git a/src/ui.rs b/src/ui.rs index 84ca6945..981f5a89 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -33,12 +33,30 @@ impl Plugin for UIPlugin { .add_system_set( ConditionSet::new() .run_in_state(GameState::MainMenu) - .with_system(main_menu::main_menu_ui) + .with_system(main_menu::main_menu_system) .into(), ); } } +/// Extension trait with helpers the egui context and UI types +pub trait EguiUiExt { + /// Clear the UI focus + fn clear_focus(self); +} + +impl EguiUiExt for &egui::Context { + fn clear_focus(self) { + self.memory().request_focus(egui::Id::null()); + } +} + +impl EguiUiExt for &mut egui::Ui { + fn clear_focus(self) { + self.ctx().clear_focus(); + } +} + fn handle_menu_input( mut windows: ResMut, input: Query<&ActionState>, @@ -73,7 +91,7 @@ fn handle_menu_input( }); } - if input.just_pressed(MenuAction::Forward) { + if input.just_pressed(MenuAction::Next) { events.push(egui::Event::Key { key: egui::Key::Tab, pressed: true, @@ -81,7 +99,7 @@ fn handle_menu_input( }); } - if input.just_pressed(MenuAction::Backward) { + if input.just_pressed(MenuAction::Previous) { events.push(egui::Event::Key { key: egui::Key::Tab, pressed: true, diff --git a/src/ui/main_menu.rs b/src/ui/main_menu.rs index deb06312..37f5705e 100644 --- a/src/ui/main_menu.rs +++ b/src/ui/main_menu.rs @@ -1,15 +1,21 @@ -use bevy::prelude::*; +use bevy::{app::AppExit, prelude::*}; use bevy_egui::{egui::style::Margin, *}; use bevy_fluent::Localization; use iyes_loopless::state::NextState; +use leafwing_input_manager::prelude::ActionState; use crate::{ - config::EngineConfig, - metadata::{localization::LocalizationExt, ButtonStyle, GameMeta}, + config::{EngineConfig, Settings}, + input::MenuAction, + metadata::{localization::LocalizationExt, ButtonStyle, FontStyle, GameMeta}, + platform::Storage, GameState, }; -use super::widgets::{bordered_button::BorderedButton, bordered_frame::BorderedFrame, EguiUIExt}; +use super::{ + widgets::{bordered_button::BorderedButton, bordered_frame::BorderedFrame, EguiUIExt}, + EguiUiExt, +}; #[derive(Component)] pub struct MainMenuBackground; @@ -47,23 +53,65 @@ pub fn despawn_main_menu_background( commands.entity(bg).despawn(); } +#[derive(Clone, Copy)] +pub enum MenuPage { + Main, + Settings { tab: SettingsTab }, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SettingsTab { + Controls, + Sound, +} + +impl Default for MenuPage { + fn default() -> Self { + Self::Main + } +} + +impl Default for SettingsTab { + fn default() -> Self { + Self::Controls + } +} + +impl SettingsTab { + const TABS: &'static [(Self, &'static str)] = + &[(Self::Controls, "controls"), (Self::Sound, "sound")]; +} + /// Render the main menu UI -pub fn main_menu_ui( +pub fn main_menu_system( + mut menu_state: Local, + mut settings: Local, mut commands: Commands, mut egui_context: ResMut, game: Res, localization: Res, engine_config: Res, + menu_input: Query<&ActionState>, + mut app_exit: EventWriter, + mut storage: ResMut, ) { - let ui_theme = &game.ui_theme; + let menu_input = menu_input.single(); + + // Go to previous menu if back button is pressed + if menu_input.pressed(MenuAction::Back) { + if let MenuPage::Settings { .. } = *menu_state { + *menu_state = MenuPage::Main; + egui_context.ctx_mut().clear_focus(); + } + } egui::CentralPanel::default() .frame(egui::Frame::none()) .show(egui_context.ctx_mut(), |ui| { let screen_rect = ui.max_rect(); - // Calculate a margin of 20% of the screen size - let outer_margin = screen_rect.size() * 0.20; + // Calculate a margin + let outer_margin = screen_rect.size() * 0.10; let outer_margin = Margin { left: outer_margin.x, right: outer_margin.x, @@ -72,41 +120,286 @@ pub fn main_menu_ui( bottom: outer_margin.y / 1.5, }; - BorderedFrame::new(&ui_theme.panel.border) + BorderedFrame::new(&game.ui_theme.panel.border) .margin(outer_margin) - .padding(ui_theme.panel.padding.into()) + .padding(game.ui_theme.panel.padding.into()) .show(ui, |ui| { // Make sure the frame ocupies the entire rect that we allocated for it. // // Without this it would only take up enough size to fit it's content. ui.set_min_size(ui.available_size()); - // Create a vertical list of items, centered horizontally - ui.vertical_centered(|ui| { - ui.themed_label(&game.main_menu.title_font, &localization.get("title")); - - // Now switch the layout to bottom_up so that we can start adding widgets - // from the bottom of the frame. - ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { - let start_button = BorderedButton::themed( - ui_theme, - &ButtonStyle::Jumbo, - &localization.get("start-game"), - ) - .show(ui); - - // Focus the start button if nothing else is focused. That way you can - // play the game just by pressing Enter. - if ui.memory().focus().is_none() { - start_button.request_focus(); - } - - if start_button.clicked() || engine_config.auto_start { - commands.insert_resource(game.start_level_handle.clone()); - commands.insert_resource(NextState(GameState::LoadingLevel)); - } - }); - }); - }) + // Render the menu based on the current menu selection + match *menu_state { + MenuPage::Main => main_menu_ui( + ui, + &mut settings, + &mut menu_state, + &mut commands, + &mut app_exit, + &mut storage, + &localization, + &game, + &engine_config, + ), + MenuPage::Settings { tab } => settings_menu_ui( + ui, + &mut settings, + &mut menu_state, + &mut storage, + tab, + &localization, + &game, + ), + } + }); + }); +} + +fn main_menu_ui( + ui: &mut egui::Ui, + settings: &mut Settings, + menu_page: &mut MenuPage, + commands: &mut Commands, + app_exit: &mut EventWriter, + storage: &mut Storage, + localization: &Localization, + game: &GameMeta, + engine_config: &EngineConfig, +) { + let ui_theme = &game.ui_theme; + + // Create a vertical list of items, centered horizontally + ui.vertical_centered(|ui| { + ui.themed_label(&game.main_menu.title_font, &localization.get("title")); + ui.add_space(game.main_menu.title_font.size); + + let min_button_size = egui::vec2(ui.available_width() / 2.0, 0.0); + + // Start button + let start_button = BorderedButton::themed( + ui_theme, + &ButtonStyle::Normal, + &localization.get("start-game"), + ) + .min_size(min_button_size) + .show(ui); + + // Focus the start button if nothing else is focused. That way you can + // play the game just by pressing Enter. + if ui.memory().focus().is_none() { + start_button.request_focus(); + } + + if start_button.clicked() || engine_config.auto_start { + commands.insert_resource(game.start_level_handle.clone()); + commands.insert_resource(NextState(GameState::LoadingLevel)); + } + + // Settings button + if BorderedButton::themed( + ui_theme, + &ButtonStyle::Normal, + &localization.get("settings"), + ) + .min_size(min_button_size) + .show(ui) + .clicked() + { + *menu_page = MenuPage::Settings { tab: default() }; + *settings = storage.get(Settings::STORAGE_KEY).unwrap_or_default(); + } + + // Quit button + if BorderedButton::themed(ui_theme, &ButtonStyle::Normal, &localization.get("quit")) + .min_size(min_button_size) + .show(ui) + .clicked() + { + app_exit.send(AppExit); + } + }); +} + +fn settings_menu_ui( + ui: &mut egui::Ui, + settings: &mut Settings, + menu_page: &mut MenuPage, + storage: &mut Storage, + current_tab: SettingsTab, + localization: &Localization, + game: &GameMeta, +) { + let ui_theme = &game.ui_theme; + + ui.vertical_centered(|ui| { + // Settings Heading + ui.themed_label( + game.ui_theme.font_styles.get(&FontStyle::Heading).unwrap(), + &localization.get("settings"), + ); + + // Add tab list to the top of the panel + ui.horizontal(|ui| { + for (tab, name) in SettingsTab::TABS { + let name = &localization.get(*name); + let mut name = egui::RichText::new(name); + + if tab == ¤t_tab { + name = name.underline(); + } + + if BorderedButton::themed(ui_theme, &ButtonStyle::Normal, name) + .show(ui) + .clicked() + { + *menu_page = MenuPage::Settings { tab: *tab }; + } + } + }); + + // Add buttons to the bottom + ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { + ui.horizontal(|ui| { + // Calculate button size and spacing + let width = ui.available_width(); + let button_width = 0.3 * width; + let button_min_size = egui::vec2(button_width, 0.0); + let button_spacing = (width - 2.0 * button_width) / 3.0; + + ui.add_space(button_spacing); + + // Cancel button + if BorderedButton::themed( + ui_theme, + &ButtonStyle::Normal, + &localization.get("cancel"), + ) + .min_size(button_min_size) + .show(ui) + .clicked() + { + *menu_page = MenuPage::Main; + ui.clear_focus(); + } + + ui.add_space(button_spacing); + + // Save button + if BorderedButton::themed(ui_theme, &ButtonStyle::Normal, &localization.get("save")) + .min_size(button_min_size) + .show(ui) + .clicked() + { + // Save the new settings + storage.set(Settings::STORAGE_KEY, settings); + storage.save(); + + *menu_page = MenuPage::Main; + ui.clear_focus(); + } + }); + + ui.vertical(|ui| { + // Render selected tab + match current_tab { + SettingsTab::Controls => controls_settings_ui(ui, game), + SettingsTab::Sound => sound_settings_ui(ui, game), + } + }); + }); + }); +} + +fn controls_settings_ui(ui: &mut egui::Ui, game: &GameMeta) { + use egui_extras::Size; + + let ui_theme = &game.ui_theme; + + let bigger_font = ui_theme + .font_styles + .get(&FontStyle::Bigger) + .unwrap() + .colored(ui_theme.panel.font_color); + let label_font = ui_theme + .font_styles + .get(&FontStyle::Normal) + .unwrap() + .colored(ui_theme.panel.font_color); + + ui.add_space(2.0); + + let row_height = label_font.size * 1.5; + + egui_extras::TableBuilder::new(ui) + .cell_layout(egui::Layout::left_to_right().with_cross_align(egui::Align::Center)) + .column(Size::exact(label_font.size * 7.0)) + .column(Size::remainder()) + .column(Size::remainder()) + .column(Size::remainder()) + .header(bigger_font.size, |mut row| { + row.col(|ui| { + ui.themed_label(&bigger_font, "Action"); + }); + row.col(|ui| { + ui.themed_label(&bigger_font, "Keyboard 1"); + }); + row.col(|ui| { + ui.themed_label(&bigger_font, "Keyboard 2"); + }); + row.col(|ui| { + ui.themed_label(&bigger_font, "Gampead"); + }); + }) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.themed_label(&label_font, "Move Up"); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.themed_label(&label_font, "Move Down"); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.themed_label(&label_font, "Move Left"); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.themed_label(&label_font, "Move Right"); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.themed_label(&label_font, "Flop Attack"); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.themed_label(&label_font, "Throw"); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.themed_label(&label_font, "Shoot"); + }); + }); }); } + +fn sound_settings_ui(ui: &mut egui::Ui, game: &GameMeta) { + let ui_theme = &game.ui_theme; + + let font = ui_theme + .font_styles + .get(&FontStyle::Heading) + .unwrap() + .colored(ui_theme.panel.font_color); + + ui.centered_and_justified(|ui| { + ui.themed_label(&font, "Coming Soon!") + }); +} diff --git a/src/ui/pause_menu.rs b/src/ui/pause_menu.rs index 081d31c4..c988eb6c 100644 --- a/src/ui/pause_menu.rs +++ b/src/ui/pause_menu.rs @@ -8,7 +8,10 @@ use crate::{ GameState, }; -use super::widgets::{bordered_button::BorderedButton, bordered_frame::BorderedFrame, EguiUIExt}; +use super::{ + widgets::{bordered_button::BorderedButton, bordered_frame::BorderedFrame, EguiUIExt}, + EguiUiExt, +}; pub fn pause_menu( mut commands: Commands, @@ -85,6 +88,7 @@ pub fn pause_menu( // Show the main menu commands.insert_resource(NextState(GameState::MainMenu)); + ui.clear_focus(); } }); }) diff --git a/src/ui/widgets/bordered_button.rs b/src/ui/widgets/bordered_button.rs index 74dfd642..62238a0d 100644 --- a/src/ui/widgets/bordered_button.rs +++ b/src/ui/widgets/bordered_button.rs @@ -43,7 +43,7 @@ impl<'a> BorderedButton<'a> { pub fn themed( ui_theme: &'a UIThemeMeta, button_style: &'a ButtonStyle, - label: &'a str, + label: impl Into, ) -> BorderedButton<'a> { let style = ui_theme .button_styles @@ -51,7 +51,8 @@ impl<'a> BorderedButton<'a> { .expect("Missing button theme"); BorderedButton::new( - egui::RichText::new(label) + label + .into() .font(style.font.font_id()) .color(style.font.color), ) From b1d97fdde31645ef49b2f257218fb13b797634aa Mon Sep 17 00:00:00 2001 From: Zicklag Date: Fri, 15 Jul 2022 11:43:21 -0500 Subject: [PATCH 04/14] Refactor Input System - Simplify Controls Metadata and Move to Settings - Remove camera controls - Update leafwing input manager to get support for virtual dpads with axes in them. --- Cargo.lock | 4 +- Cargo.toml | 2 +- assets/default.game.yaml | 245 +++++++++++---------------------------- src/camera.rs | 58 +-------- src/config.rs | 11 -- src/game_init.rs | 78 +++++++++++-- src/input.rs | 10 -- src/main.rs | 4 +- src/metadata.rs | 53 +-------- src/metadata/settings.rs | 55 +++++++++ src/movement.rs | 2 +- src/player.rs | 7 +- src/ui/main_menu.rs | 22 ++-- 13 files changed, 221 insertions(+), 330 deletions(-) create mode 100644 src/metadata/settings.rs diff --git a/Cargo.lock b/Cargo.lock index 5b5138e5..ab0e35b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,7 +2291,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "leafwing_input_manager" version = "0.5.0" -source = "git+https://github.com/Leafwing-Studios/leafwing-input-manager.git?rev=e8bd6e0#e8bd6e00886f45cd37c3289cf2a726b514058fda" +source = "git+https://github.com/zicklag/leafwing-input-manager.git?branch=backport-leafwing-dev-to-bevy-0.7#000cee2ae7bc1bf206d63c3a2dfbacbb07fa23f2" dependencies = [ "bevy_app", "bevy_core", @@ -2312,7 +2312,7 @@ dependencies = [ [[package]] name = "leafwing_input_manager_macros" version = "0.5.0" -source = "git+https://github.com/Leafwing-Studios/leafwing-input-manager.git?rev=e8bd6e0#e8bd6e00886f45cd37c3289cf2a726b514058fda" +source = "git+https://github.com/zicklag/leafwing-input-manager.git?branch=backport-leafwing-dev-to-bevy-0.7#000cee2ae7bc1bf206d63c3a2dfbacbb07fa23f2" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index a10b234a..e09db2e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ getrandom = { version = "0.2", features = ["js"] } bevy_mod_debugdump = { version = "0.4", optional = true } bevy-inspector-egui = { version = "0.11", optional = true } bevy-inspector-egui-rapier = { version = "0.4", optional = true, features = ["rapier2d"] } -leafwing_input_manager = { git = "https://github.com/Leafwing-Studios/leafwing-input-manager.git", rev = "e8bd6e0" } +leafwing_input_manager = { git = "https://github.com/zicklag/leafwing-input-manager.git", branch = "backport-leafwing-dev-to-bevy-0.7" } unic-langid = "0.9.0" bevy_fluent = { git = "https://github.com/kgv/bevy_fluent", rev = "d41f514" } sys-locale = "0.2.1" diff --git a/assets/default.game.yaml b/assets/default.game.yaml index d5482865..5aec4afe 100644 --- a/assets/default.game.yaml +++ b/assets/default.game.yaml @@ -16,186 +16,77 @@ main_menu: music: ui/Fishycuffs [title screen].ogg -default_input_maps: - players: - # Player 1 - - Move: - - VirtualDPad: - up: - Keyboard: W - down: - Keyboard: S - left: - Keyboard: A - right: - Keyboard: D - - VirtualDPad: - up: - GamepadButton: DPadUp - down: - GamepadButton: DPadDown - left: - GamepadButton: DPadLeft - right: - GamepadButton: DPadRight - - Single: - DualGamepadAxis: - x_axis: LeftStickX - y_axis: LeftStickY - x_positive_low: 0.1 - x_negative_low: -0.1 - y_positive_low: 0.1 - y_negative_low: -0.1 - FlopAttack: - - Single: - Keyboard: Space - - Single: - GamepadButton: South - Throw: - - Single: - Keyboard: C - - Single: - GamepadButton: West - Shoot: - - Single: - Keyboard: V - - Single: - GamepadButton: East - # Player 2 - - Move: - - VirtualDPad: - up: - Keyboard: Up - down: - Keyboard: Down - left: - Keyboard: Left - right: - Keyboard: Right - - Single: - DualGamepadAxis: - x_axis: LeftStickX - y_axis: LeftStickY - x_positive_low: 0.1 - x_negative_low: -0.1 - y_positive_low: 0.1 - y_negative_low: -0.1 - - VirtualDPad: - up: - GamepadButton: DPadUp - down: - GamepadButton: DPadDown - left: - GamepadButton: DPadLeft - right: - GamepadButton: DPadRight - FlopAttack: - - Single: - Keyboard: Comma - - Single: - GamepadButton: South - Throw: - - Single: - Keyboard: Period - - Single: - GamepadButton: West - Shoot: - - Single: - Keyboard: RShift - - Single: - GamepadButton: East - - camera: - Up: - - Chord: - - Keyboard: Up - - Keyboard: LControl - Down: - - Chord: - - Keyboard: Down - - Keyboard: LControl - Left: - - Chord: - - Keyboard: Left - - Keyboard: LControl - Right: - - Chord: - - Keyboard: Right - - Keyboard: LControl - ZoomIn: - - Chord: - - Keyboard: Equals - - Keyboard: LControl - ZoomOut: - - Chord: - - Keyboard: Minus - - Keyboard: LControl - - menu: - Confirm: - - Single: - GamepadButton: South - - Single: - GamepadButton: Start - - Back: - - Single: - Keyboard: Escape - - Single: - GamepadButton: East - - Next: - - Single: - Keyboard: Down - - Single: - Keyboard: Right - - Single: - GamepadButton: DPadDown - - Single: - GamepadButton: DPadRight - - Single: - GamepadButton: Select - - Single: - SingleGamepadAxis: - axis: LeftStickY +default_settings: + player_controls: + # Gamepad controls + gamepad: + movement: + up: + SingleAxis: + axis_type: + Gamepad: LeftStickY + positive_low: 0.1 + negative_low: -1.0 + left: + SingleAxis: + axis_type: + Gamepad: LeftStickX + positive_low: 1.0 + negative_low: -0.1 + down: + SingleAxis: + axis_type: + Gamepad: LeftStickY positive_low: 1.0 - negative_low: -0.5 - - Single: - SingleGamepadAxis: - axis: LeftStickX + negative_low: -0.1 + right: + SingleAxis: + axis_type: + Gamepad: LeftStickX + positive_low: 0.1 negative_low: -1.0 - positive_low: 0.5 - Previous: - - Single: - Keyboard: Left - - Single: + flop_attack: + GamepadButton: South + shoot: + GamepadButton: East + throw: + GamepadButton: West + + # Controls for the first keyboard player ( left side ) + keyboard1: + movement: + up: + Keyboard: W + down: + Keyboard: S + left: + Keyboard: A + right: + Keyboard: D + flop_attack: + Keyboard: Space + shoot: + Keyboard: V + throw: + Keyboard: C + + # Controls for the second keyboard player ( right side ) + keyboard2: + movement: + up: Keyboard: Up - - Single: - GamepadButton: DPadLeft - - Single: - GamepadButton: DPadUp - - Single: - SingleGamepadAxis: - axis: LeftStickY - negative_low: -1.0 - positive_low: 0.5 - - Single: - SingleGamepadAxis: - axis: LeftStickX - negative_low: -0.5 - positive_low: 1.0 - Pause: - - Single: - Keyboard: Escape - - Single: - Keyboard: P - - Single: - GamepadButton: Start - ToggleFullscreen: - - Single: - Keyboard: F11 - - Single: - GamepadButton: Mode + down: + Keyboard: Down + left: + Keyboard: Left + right: + Keyboard: Right + flop_attack: + Keyboard: Comma + shoot: + Keyboard: RShift + throw: + Keyboard: Period ui_theme: font_families: diff --git a/src/camera.rs b/src/camera.rs index 16ebc786..931c3180 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,17 +1,10 @@ use bevy::{ - core::Time, math::Vec2, - prelude::{ - Camera, Component, EventWriter, OrthographicProjection, Query, Res, ResMut, Transform, - With, Without, - }, - render::camera::CameraProjection, - window::Windows, + prelude::{Camera, Component, EventWriter, Query, Res, Transform, With, Without}, }; use bevy_parallax::ParallaxMoveEvent; -use leafwing_input_manager::prelude::ActionState; -use crate::{consts, input::CameraAction, metadata::GameMeta, Player}; +use crate::{consts, metadata::GameMeta, Player}; #[cfg_attr(feature = "debug", derive(bevy_inspector_egui::Inspectable))] #[derive(Component)] @@ -19,53 +12,6 @@ pub struct Panning { pub offset: Vec2, } -pub fn helper_camera_controller( - mut query: Query<( - &mut Camera, - &mut OrthographicProjection, - &mut Panning, - &ActionState, - )>, - time: Res