From 3bc6d4d9e56dc663d00eed067577b9c19cff722a Mon Sep 17 00:00:00 2001 From: eri Date: Fri, 1 Dec 2023 21:26:41 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20modular=20ui=20=F0=9F=92=95=20fix:=20up?= =?UTF-8?q?load=20to=20itch=20change:=20now=20the=20binary=20contains=20as?= =?UTF-8?q?set=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yaml | 15 +-- Cargo.lock | 19 +++ Cargo.toml | 1 + build/wasm/index.html | 3 +- examples/dvd.rs | 15 ++- examples/jump.rs | 20 +++- src/config.rs | 6 +- src/debug.rs | 2 +- src/input.rs | 4 +- src/load.rs | 8 +- src/main.rs | 7 +- src/menu.rs | 169 ++++++++++++++++++--------- src/ui.rs | 208 ++++++++++++++++++--------------- 13 files changed, 299 insertions(+), 178 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f17c90e..6924494 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -119,7 +119,6 @@ jobs: mkdir linux cp target/x86_64-unknown-linux-gnu/release/${{ env.binary }} linux/ strip linux/${{ env.binary }} - cp -r assets linux/ - name: Package as a zip working-directory: ./linux @@ -168,7 +167,6 @@ jobs: run: | mkdir windows cp target/x86_64-pc-windows-msvc/release/${{ env.binary }}.exe windows/ - cp -r assets windows/ - name: Package as a zip run: | @@ -231,7 +229,6 @@ jobs: run: | mkdir -p ${{ env.binary }}.app/Contents/MacOS cp target/release/${{ env.binary }} ${{ env.binary }}.app/Contents/MacOS/ - cp -r assets ${{ env.binary }}.app/Contents/MacOS/ strip ${{ env.binary }}.app/Contents/MacOS/${{ env.binary }} hdiutil create -fs HFS+ -volname "${{ env.binary }}" -srcfolder ${{ env.binary }}.app ${{ env.binary }}.dmg @@ -259,20 +256,19 @@ jobs: - id: check-env run: | if [[ -z "$itch_target" ]]; then - echo "has-itch-target == no >> $GITHUB_OUTPUT" + echo "has-itch-target=no" >> $GITHUB_OUTPUT else - echo "has-itch-target == yes >> $GITHUB_OUTPUT" + echo "has-itch-target=yes" >> $GITHUB_OUTPUT fi upload-to-itch: runs-on: ubuntu-latest needs: - - get-version - check-upload-to-itch - release-wasm - release-linux - - release-windows - - release-macos + # - release-windows # [CHANGE]: Uncomment this if you want to automaticall deploy mac and windows builds to itch + # - release-macos # It is disabled by default because they take a loong time if: ${{ needs.check-upload-to-itch.outputs.should-upload == 'yes' }} env: @@ -282,7 +278,8 @@ jobs: - name: Download artifacts uses: actions/download-artifact@v3 with: - path: ./builds + name: wasm + path: ./builds/wasm - name: Install butler run: | diff --git a/Cargo.lock b/Cargo.lock index ff576c3..84b250a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -657,6 +657,18 @@ dependencies = [ "webbrowser", ] +[[package]] +name = "bevy_embedded_assets" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbaa2716dd665f2197071268f5fd720bc23b0207894451f0a0912b3e80e86a1" +dependencies = [ + "bevy", + "cargo-emit", + "futures-io", + "futures-lite 2.0.1", +] + [[package]] name = "bevy_encase_derive" version = "0.12.1" @@ -1356,6 +1368,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "cargo-emit" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1582e1c9e755dd6ad6b224dcffb135d199399a4568d454bd89fe515ca8425695" + [[package]] name = "cc" version = "1.0.83" @@ -2199,6 +2217,7 @@ dependencies = [ "bevy-inspector-egui", "bevy-persistent", "bevy_asset_loader", + "bevy_embedded_assets", "bevy_kira_audio", "bevy_mod_debugdump", "iyes_progress", diff --git a/Cargo.toml b/Cargo.toml index 69d9031..29f6a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ bevy = { version = "0.12", default-features = false, features = [ "tonemapping_luts", "default_font", "webgl2", ]} bevy_asset_loader = { version = "0.18", features = [ "progress_tracking" ] } # Better asset loader +bevy_embedded_assets = { version = "0.9" } # Embed assets in binary bevy_kira_audio = { version = "0.18" } # Improved audio library iyes_progress = { version = "0.10", features = [ "assets" ] } # Track loading and game state bevy-inspector-egui = { version = "0.21" } # Inspector diff --git a/build/wasm/index.html b/build/wasm/index.html index 797242d..1b868fb 100644 --- a/build/wasm/index.html +++ b/build/wasm/index.html @@ -5,9 +5,8 @@ - Game + Hello Bevy - diff --git a/examples/dvd.rs b/examples/dvd.rs index 0e15667..a88b9fa 100644 --- a/examples/dvd.rs +++ b/examples/dvd.rs @@ -1,6 +1,9 @@ use bevy::{prelude::*, window::WindowResolution}; +use bevy_embedded_assets::{EmbeddedAssetPlugin, PluginMode}; use bevy_kira_audio::prelude::*; +use bevy_persistent::Persistent; use hello_bevy::{ + config::GameOptions, load::{GameAssets, SampleAssets}, GamePlugin, GameState, }; @@ -8,6 +11,9 @@ use hello_bevy::{ fn main() { App::new() .add_plugins(( + EmbeddedAssetPlugin { + mode: PluginMode::ReplaceDefault, + }, DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "DVD Screensaver".to_string(), @@ -75,7 +81,12 @@ struct CollisionEvent(Entity); // Systems // ······· -fn init_sample(mut cmd: Commands, assets: Res, info: Option>) { +fn init_sample( + mut cmd: Commands, + assets: Res, + opts: Res>, + info: Option>, +) { cmd.spawn((Camera2dBundle::default(), GameCamera)); if info.is_some() { @@ -110,7 +121,7 @@ fn init_sample(mut cmd: Commands, assets: Res, info: Option, info: Option>) { +fn init_sample( + mut cmd: Commands, + assets: Res, + opts: Res>, + info: Option>, +) { cmd.spawn((Camera2dBundle::default(), GameCamera)); if info.is_some() { @@ -104,7 +118,7 @@ fn init_sample(mut cmd: Commands, assets: Res, info: Option String { +impl ToString for Bind { + fn to_string(&self) -> String { match self { Bind::Key(key) => format!("{:?}", key), Bind::Mouse(button) => format!("{:?}", button), diff --git a/src/load.rs b/src/load.rs index 96c7b0a..2f66ee8 100644 --- a/src/load.rs +++ b/src/load.rs @@ -24,11 +24,9 @@ impl Plugin for LoadPlugin { app.add_loading_state(LoadingState::new(GameState::Loading)) .init_collection::() .add_collection_to_loading_state::<_, SampleAssets>(GameState::Loading) - .add_plugins( - ProgressPlugin::new(GameState::Loading) - .continue_to(GameState::Menu) - .track_assets(), - ) + .add_plugins((ProgressPlugin::new(GameState::Loading) + .continue_to(GameState::Menu) + .track_assets(),)) .add_systems(OnEnter(GameState::Loading), init_splash) .add_systems(OnExit(GameState::Loading), clear_loading) .add_systems( diff --git a/src/main.rs b/src/main.rs index 54e8919..7bb2d3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ use bevy::{prelude::*, window::WindowResolution}; +use bevy_embedded_assets::{EmbeddedAssetPlugin, PluginMode}; use hello_bevy::GamePlugin; fn main() { App::new() .add_plugins(( + EmbeddedAssetPlugin { + // Embed assets in binary (else itch.io is broken right now) + mode: PluginMode::ReplaceDefault, + }, DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Hello Bevy!".to_string(), // [CHANGE]: Game title resolution: WindowResolution::new(600., 600.), - resizable: false, // or use fit_canvas_to_parent: true for resizing on the web + resizable: false, // Or use fit_canvas_to_parent: true for resizing on the web canvas: Some("#bevy".to_string()), prevent_default_event_handling: false, ..default() diff --git a/src/menu.rs b/src/menu.rs index f1a3b3b..54e43c1 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -9,9 +9,7 @@ use crate::{ use bevy::prelude::*; use bevy::reflect::Struct; -// TODO: Extract styles into external functions (maybe create ui package) -// TODO: Change the create functions to be more modular -// TODO: Single UI camera (for debug fps as well +// TODO: Single UI camera (for debug fps as well) // TODO: Tweening and animation // ······ @@ -37,12 +35,16 @@ impl Plugin for MenuPlugin { clean_menu.run_if(in_state(GameState::Menu)), ) .add_systems( - OnEnter(MenuState::Options), + OnEnter(MenuState::Settings), clean_menu.run_if(in_state(GameState::Menu)), ) .add_systems( OnEnter(MenuState::Keybinds), clean_menu.run_if(in_state(GameState::Menu)), + ) + .add_systems( + OnEnter(MenuState::Visual), + clean_menu.run_if(in_state(GameState::Menu)), ); } } @@ -51,8 +53,9 @@ impl Plugin for MenuPlugin { pub enum MenuState { #[default] Main, - Options, + Settings, Keybinds, + Visual, } // ·········· @@ -72,10 +75,12 @@ struct MenuText; enum MenuButton { Play, GoMain, - GoOptions, + GoSettings, GoKeybinds, - OptionsColor(String), + GoVisual, RemapKeybind(String, Vec), + ChangeFont(String), + ChangeColor(String, String), } // ······· @@ -94,7 +99,7 @@ fn init_menu(mut cmd: Commands, opts: Res>, style: Res { menu_state.set(MenuState::Main); } - MenuButton::GoOptions => { - menu_state.set(MenuState::Options); + MenuButton::GoSettings => { + menu_state.set(MenuState::Settings); } MenuButton::GoKeybinds => { menu_state.set(MenuState::Keybinds); } - MenuButton::OptionsColor(field) => { - // TODO: Add color picker - info!("color {}", field); + MenuButton::GoVisual => { + menu_state.set(MenuState::Visual); } - MenuButton::RemapKeybind(field, _) => { + MenuButton::RemapKeybind(_, _) => { // TODO: Remap keymaps - info!("remap {}", field); + } + MenuButton::ChangeFont(_) => { + // TODO: Change font size + } + MenuButton::ChangeColor(_, _) => { + // TODO: Change color } } } @@ -175,8 +184,9 @@ fn clean_menu( match state.get() { MenuState::Main => layout_main(cmd, node, &style), - MenuState::Options => layout_options(cmd, node, &style, &opts), + MenuState::Settings => layout_options(cmd, node, &style), MenuState::Keybinds => layout_keybinds(cmd, node, &style, &keybinds), + MenuState::Visual => layout_visual(cmd, node, &style, &opts), } } @@ -203,8 +213,11 @@ fn return_to_menu( let input = InputState::new(&keyboard, &mouse, &gamepad); if input.just_pressed(&keybinds.pause).unwrap_or(false) { - if *current_menu_state.get() != MenuState::Main { - next_menu_state.set(MenuState::Main); + match *current_menu_state.get() { + MenuState::Settings => next_menu_state.set(MenuState::Main), + MenuState::Keybinds => next_menu_state.set(MenuState::Settings), + MenuState::Visual => next_menu_state.set(MenuState::Settings), + _ => {} } game_state.set(GameState::Menu); } @@ -216,63 +229,107 @@ fn return_to_menu( fn layout_main(mut cmd: Commands, node: Entity, style: &UIStyle) { cmd.entity(node).with_children(|parent| { - create_title(parent, style, "Hello Bevy"); + UIText::new(style, "Hello Bevy").with_title().add(parent); - create_button(parent, style, "Play", MenuButton::Play); - create_button(parent, style, "Options", MenuButton::GoOptions); + UIButton::new(style, "Play", MenuButton::Play).add(parent); + UIButton::new(style, "Settings", MenuButton::GoSettings).add(parent); }); } -fn layout_options(mut cmd: Commands, node: Entity, style: &UIStyle, opts: &GameOptions) { +fn layout_options(mut cmd: Commands, node: Entity, style: &UIStyle) { cmd.entity(node).with_children(|parent| { - create_title(parent, style, "Options"); + UIText::new(style, "Settings").with_title().add(parent); - create_button(parent, style, "Keybinds", MenuButton::GoKeybinds); + UIButton::new(style, "Keybinds", MenuButton::GoKeybinds).add(parent); + UIButton::new(style, "Visual", MenuButton::GoVisual).add(parent); - for (i, value) in opts.color.iter_fields().enumerate() { - let field_name = opts.color.name_at(i).unwrap(); - if let Some(value) = value.downcast_ref::() { - let r = value.r(); - let g = value.g(); - let b = value.b(); - create_button( - parent, - style, - &format!( - "{}: {:.0},{:.0},{:.0}", - field_name, - r * 255., - g * 255., - b * 255. - ), - MenuButton::OptionsColor(field_name.to_string()), - ); - } - } - - create_button(parent, style, "Back", MenuButton::GoMain); + UIButton::new(style, "Back", MenuButton::GoMain).add(parent); }); } fn layout_keybinds(mut cmd: Commands, node: Entity, style: &UIStyle, keybinds: &Keybinds) { cmd.entity(node).with_children(|parent| { - create_title(parent, style, "Keybinds"); + UIText::new(style, "Options").with_title().add(parent); - // TODO: Scrollable section + // TODO: Scrollable section (Requires #8104 to be merged in 0.13) for (i, value) in keybinds.iter_fields().enumerate() { let field_name = keybinds.name_at(i).unwrap(); if let Some(value) = value.downcast_ref::>() { - create_keybind_remap( - parent, - style, - field_name, - MenuButton::RemapKeybind(field_name.to_string(), value.clone()), - value, - ); + UIOption::new(style, field_name).add(parent, |row| { + let keys = value + .iter() + .map(|bind| bind.to_string()) + .collect::>() + .join(" "); + + UIButton::new( + style, + &keys, + MenuButton::RemapKeybind(field_name.to_string(), value.clone()), + ) + .with_width(Val::Px(64.)) + .add(row); + }); + } + } + + UIButton::new(style, "Back", MenuButton::GoSettings).add(parent); + }); +} + +fn layout_visual(mut cmd: Commands, node: Entity, style: &UIStyle, opts: &GameOptions) { + cmd.entity(node).with_children(|parent| { + UIText::new(style, "Visual settings") + .with_title() + .add(parent); + + for (i, value) in opts.font_size.iter_fields().enumerate() { + let field_name = opts.font_size.name_at(i).unwrap().to_string(); + if let Some(value) = value.downcast_ref::() { + UIOption::new(style, &format!("font_{}", field_name)).add(parent, |row| { + UIButton::new( + style, + &format!("{}", value), + MenuButton::ChangeFont(field_name), + ) + .with_width(Val::Px(40.)) + .add(row); + }); + } + } + + for (i, value) in opts.color.iter_fields().enumerate() { + let field_name = format!("color_{}", opts.color.name_at(i).unwrap()); + if let Some(value) = value.downcast_ref::() { + UIOption::new(style, &field_name).add(parent, |row| { + UIButton::new( + style, + &format!("{:.0}", value.r() * 255.), + MenuButton::ChangeColor(field_name.clone(), "r".to_string()), + ) + .with_width(Val::Px(40.)) + .add(row); + + UIButton::new( + style, + &format!("{:.0}", value.g() * 255.), + MenuButton::ChangeColor(field_name.clone(), "g".to_string()), + ) + .with_width(Val::Px(40.)) + .add(row); + + UIButton::new( + style, + &format!("{:.0}", value.b() * 255.), + MenuButton::ChangeColor(field_name.clone(), "b".to_string()), + ) + .with_width(Val::Px(40.)) + .add(row); + }); } } - create_button(parent, style, "Back", MenuButton::GoOptions); + UIButton::new(style, "Back", MenuButton::GoSettings).add(parent); }); } diff --git a/src/ui.rs b/src/ui.rs index f994386..24c93e0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,11 @@ use bevy::prelude::*; use bevy_persistent::Persistent; -use crate::{config::GameOptions, input::Bind, load::GameAssets}; +use crate::{config::GameOptions, load::GameAssets}; -// TODO: Input field -// TODO: Color picker +const MENU_WIDTH: Val = Val::Px(300.); +const MENU_ITEM_HEIGHT: Val = Val::Px(40.); +const MENU_ITEM_GAP: Val = Val::Px(10.); // ······ // Plugin @@ -29,8 +30,9 @@ impl Plugin for UIPlugin { pub struct UIStyle { pub title: TextStyle, pub text: TextStyle, - pub button: Style, pub button_text: TextStyle, + + pub button: Style, pub button_bg: BackgroundColor, } @@ -55,20 +57,20 @@ pub fn change_style( color: opts.color.mid, }; + style.button_text = TextStyle { + font: assets.font.clone(), + font_size: opts.font_size.button_text, + color: opts.color.dark, + }; + style.button = Style { - width: Val::Px(196.), - height: Val::Px(48.), + width: MENU_WIDTH, + height: MENU_ITEM_HEIGHT, justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }; - style.button_text = TextStyle { - font: assets.font.clone(), - font_size: 24., - color: opts.color.dark, - }; - style.button_bg = opts.color.light.into(); } @@ -76,98 +78,118 @@ pub fn change_style( // Extra // ····· -pub fn create_title(parent: &mut ChildBuilder, style: &UIStyle, text: &str) { - parent.spawn(TextBundle::from_section(text, style.title.clone())); +// Text + +pub struct UIText<'a> { + text: TextBundle, + style: &'a UIStyle, +} + +impl<'a> UIText<'a> { + pub fn new(style: &'a UIStyle, text: &str) -> Self { + Self { + text: TextBundle::from_section(text, style.text.clone()), + style, + } + } + + pub fn with_title(mut self) -> Self { + self.text.text.sections[0].style = self.style.title.clone(); + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.text.style = style; + self + } + + pub fn add(self, parent: &mut ChildBuilder) { + parent.spawn(self.text); + } } -pub fn create_button( - parent: &mut ChildBuilder, - style: &UIStyle, - text: &str, +// Button + +pub struct UIButton { + button: ButtonBundle, + text: TextBundle, action: T, -) { - parent - .spawn(( - ButtonBundle { +} + +impl UIButton { + pub fn new(style: &UIStyle, text: &str, action: T) -> Self { + Self { + button: ButtonBundle { style: style.button.clone(), background_color: style.button_bg, ..default() }, + text: TextBundle::from_section(text, style.button_text.clone()), action, - )) - .with_children(|parent| { - parent.spawn(TextBundle::from_section(text, style.button_text.clone())); - }); + } + } + + pub fn with_width(mut self, width: Val) -> Self { + self.button.style.width = width; + self + } + + pub fn add(self, parent: &mut ChildBuilder) { + parent + .spawn((self.button, self.action)) + .with_children(|button| { + button.spawn(self.text); + }); + } } -pub fn create_keybind_remap( - parent: &mut ChildBuilder, - style: &UIStyle, - text: &str, - action: T, - bind: &[Bind], -) { - parent - .spawn(NodeBundle { - style: Style { - min_width: Val::Px(196.), - flex_direction: FlexDirection::Row, - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - column_gap: Val::Px(12.), +// Option row (label text + widget) + +pub struct UIOption<'a> { + row: NodeBundle, + label: UIText<'a>, +} + +impl<'a> UIOption<'a> { + pub fn new(style: &'a UIStyle, label: &str) -> Self { + Self { + row: NodeBundle { + style: Style { + width: MENU_WIDTH, + column_gap: MENU_ITEM_GAP, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, ..default() }, - ..default() - }) - .with_children(|parent| { - let name = text - .chars() - .enumerate() - .map(|(i, c)| { - if i == 0 { - c.to_uppercase().next().unwrap() - } else if c == '_' { - ' ' - } else { - c - } - }) - .collect::(); - - parent.spawn( - TextBundle::from_section(name, style.text.clone()).with_style(Style { - flex_grow: 1., - ..default() - }), - ); - - parent - .spawn(( - ButtonBundle { - style: Style { - width: Val::Px(96.), - ..style.button.clone() - }, - background_color: style.button_bg, - ..default() - }, - action, - )) - .with_children(|parent| { - let name = bind - .iter() - .map(|bind| bind.name()) - .collect::>() - .join(", "); - let font_size = if name.len() > 1 { 16. } else { 24. }; - parent.spawn(TextBundle::from_section( - name, - TextStyle { - font: style.button_text.font.clone(), - font_size, - color: style.button_text.color, - }, - )); - }); + label: UIText::new(style, &snake_to_upper(label)).with_style(Style { + flex_grow: 1., + ..default() + }), + } + } + + pub fn add(self, parent: &mut ChildBuilder, children: impl FnOnce(&mut ChildBuilder)) { + parent.spawn(self.row).with_children(|row| { + self.label.add(row); + children(row); }); + } +} + +pub fn snake_to_upper(text: &str) -> String { + text.chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().next().unwrap() + } else if c == '_' { + ' ' + } else { + c + } + }) + .collect::() }