diff --git a/README.md b/README.md index 3aacd421..0d494146 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,13 @@ This template comes with a basic project structure that you may find useful: | Path | Description | | -------------------------------------------------- | ------------------------------------------------------------------ | -| [`src/lib.rs`](./src/lib.rs) | App setup | +| [`src/main.rs`](./src/main.rs) | App setup | | [`src/asset_tracking.rs`](./src/asset_tracking.rs) | A high-level way to load collections of asset handles as resources | | [`src/audio.rs`](./src/audio.rs) | Marker components for sound effects and music | -| [`src/demo/`](./src/demo) | Example game mechanics & content (replace with your own code) | | [`src/dev_tools.rs`](./src/dev_tools.rs) | Dev tools for dev builds (press \` aka backtick to toggle) | -| [`src/screens/`](./src/screens) | Splash screen, title screen, gameplay screen, etc. | +| [`src/demo/`](./src/demo) | Example game mechanics & content (replace with your own code) | +| [`src/menus/`](./src/menus) | Main menu, pause menu, settings menu, etc. | +| [`src/screens/`](./src/screens) | Splash screen, title screen, loading screen, etc. | | [`src/theme/`](./src/theme) | Reusable UI widgets & theming | Feel free to move things around however you want, though. @@ -141,4 +142,4 @@ The CC0 license explicitly does not waive patent rights, but we confirm that we ## Credits -The [assets](./assets) in this repository are all 3rd-party. See the [credits screen](./src/screens/credits.rs) for more information. +The [assets](./assets) in this repository are all 3rd-party. See the [credits menu](./src/menus/credits.rs) for more information. diff --git a/docs/design.md b/docs/design.md index 32135922..df70dcc0 100644 --- a/docs/design.md +++ b/docs/design.md @@ -87,7 +87,6 @@ pub enum Screen { Splash, Loading, Title, - Credits, Gameplay, Victory, Leaderboard, @@ -137,7 +136,7 @@ fn enter_title_screen(mut next_state: ResMut>) { ### Reasoning -"Screen" is not meant as the physical screen, but as "what kind of screen is the game showing right now", e.g. the title screen, the loading screen, the credits screen, the victory screen, etc. +"Screen" is not meant as the physical screen, but as "what kind of screen is the game showing right now", e.g. the title screen, the loading screen, the victory screen, etc. These screens usually correspond to different logical states of your game that have different systems running. By using a dedicated `State` type for your screens, you can easily manage systems and entities that are only relevant for a specific screen and flexibly transition between diff --git a/post-generate.rhai b/post-generate.rhai index c5420605..87fc9d0d 100644 --- a/post-generate.rhai +++ b/post-generate.rhai @@ -3,7 +3,6 @@ file::rename(".github/workflows/release.yaml.template", ".github/workflows/relea file::rename("Cargo.toml.template", "Cargo.toml"); file::rename("README.md.template", "README.md"); file::rename("src/main.rs.template", "src/main.rs"); -file::rename("src/lib.rs.template", "src/lib.rs"); // Generate `Cargo.lock`. system::command("cargo", ["update", "--package", variable::get("project-name")]); diff --git a/src/lib.rs.template b/src/lib.rs.template deleted file mode 100644 index 642ce3e9..00000000 --- a/src/lib.rs.template +++ /dev/null @@ -1,90 +0,0 @@ -// Support configuring Bevy lints within code. -#![cfg_attr(bevy_lint, feature(register_tool), register_tool(bevy))] - -mod asset_tracking; -mod audio; -mod demo; -#[cfg(feature = "dev")] -mod dev_tools; -mod screens; -mod theme; - -use bevy::{ - asset::AssetMetaCheck, - audio::{AudioPlugin, Volume}, - prelude::*, -}; - -pub struct AppPlugin; - -impl Plugin for AppPlugin { - fn build(&self, app: &mut App) { - // Order new `AppSystems` variants by adding them here: - app.configure_sets( - Update, - ( - AppSystems::TickTimers, - AppSystems::RecordInput, - AppSystems::Update, - ) - .chain(), - ); - - // Spawn the main camera. - app.add_systems(Startup, spawn_camera); - - // Add Bevy plugins. - app.add_plugins( - DefaultPlugins - .set(AssetPlugin { - // Wasm builds will check for meta files (that don't exist) if this isn't set. - // This causes errors and even panics on web build on itch. - // See https://github.com/bevyengine/bevy_github_ci_template/issues/48. - meta_check: AssetMetaCheck::Never, - ..default() - }) - .set(WindowPlugin { - primary_window: Window { - title: "{{project-name | title_case}}".to_string(), - fit_canvas_to_parent: true, - ..default() - } - .into(), - ..default() - }) - .set(AudioPlugin { - global_volume: GlobalVolume { - volume: Volume::Linear(0.3), - }, - ..default() - }), - ); - - // Add other plugins. - app.add_plugins(( - asset_tracking::plugin, - demo::plugin, - #[cfg(feature = "dev")] - dev_tools::plugin, - screens::plugin, - theme::plugin, - )); - } -} - -/// High-level groupings of systems for the app in the `Update` schedule. -/// When adding a new variant, make sure to order it in the `configure_sets` -/// call above. -#[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] -enum AppSystems { - /// Tick timers. - TickTimers, - /// Record player input. - RecordInput, - /// Do everything else (consider splitting this into further variants). - Update, -} - -fn spawn_camera(mut commands: Commands) { - commands.spawn((Name::new("Camera"), Camera2d)); -} diff --git a/src/main.rs b/src/main.rs index 83ac3410..4d7b9c40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod audio; mod demo; #[cfg(feature = "dev")] mod dev_tools; +mod menus; mod screens; mod theme; @@ -62,6 +63,7 @@ impl Plugin for AppPlugin { demo::plugin, #[cfg(feature = "dev")] dev_tools::plugin, + menus::plugin, screens::plugin, theme::plugin, )); diff --git a/src/main.rs.template b/src/main.rs.template index 7378faa8..67c55ba4 100644 --- a/src/main.rs.template +++ b/src/main.rs.template @@ -1,11 +1,88 @@ -// Disable console on Windows for non-dev builds. -#![cfg_attr(not(feature = "dev"), windows_subsystem = "windows")] // Support configuring Bevy lints within code. #![cfg_attr(bevy_lint, feature(register_tool), register_tool(bevy))] +// Disable console on Windows for non-dev builds. +#![cfg_attr(not(feature = "dev"), windows_subsystem = "windows")] -use bevy::prelude::*; -use {{crate_name}}::AppPlugin; +mod asset_tracking; +mod audio; +mod demo; +#[cfg(feature = "dev")] +mod dev_tools; +mod menus; +mod screens; +mod theme; + +use bevy::{asset::AssetMetaCheck, prelude::*}; fn main() -> AppExit { App::new().add_plugins(AppPlugin).run() } + +pub struct AppPlugin; + +impl Plugin for AppPlugin { + fn build(&self, app: &mut App) { + // Order new `AppSystems` variants by adding them here: + app.configure_sets( + Update, + ( + AppSystems::TickTimers, + AppSystems::RecordInput, + AppSystems::Update, + ) + .chain(), + ); + + // Spawn the main camera. + app.add_systems(Startup, spawn_camera); + + // Add Bevy plugins. + app.add_plugins( + DefaultPlugins + .set(AssetPlugin { + // Wasm builds will check for meta files (that don't exist) if this isn't set. + // This causes errors and even panics on web build on itch. + // See https://github.com/bevyengine/bevy_github_ci_template/issues/48. + meta_check: AssetMetaCheck::Never, + ..default() + }) + .set(WindowPlugin { + primary_window: Window { + title: "{{project-name | title_case}}".to_string(), + fit_canvas_to_parent: true, + ..default() + } + .into(), + ..default() + }), + ); + + // Add other plugins. + app.add_plugins(( + asset_tracking::plugin, + demo::plugin, + #[cfg(feature = "dev")] + dev_tools::plugin, + menus::plugin, + screens::plugin, + theme::plugin, + )); + } +} + +/// High-level groupings of systems for the app in the `Update` schedule. +/// When adding a new variant, make sure to order it in the `configure_sets` +/// call above. +#[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] +enum AppSystems { + /// Tick timers. + TickTimers, + /// Record player input. + RecordInput, + /// Do everything else (consider splitting this into further variants). + Update, +} + +fn spawn_camera(mut commands: Commands) { + commands.spawn((Name::new("Camera"), Camera2d)); +} diff --git a/src/screens/credits.rs b/src/menus/credits.rs similarity index 71% rename from src/screens/credits.rs rename to src/menus/credits.rs index d0939c97..7ba95386 100644 --- a/src/screens/credits.rs +++ b/src/menus/credits.rs @@ -1,27 +1,33 @@ -//! A credits screen that can be accessed from the title screen. +//! The credits menu. -use bevy::{ecs::spawn::SpawnIter, prelude::*, ui::Val::*}; +use bevy::{ + ecs::spawn::SpawnIter, input::common_conditions::input_just_pressed, prelude::*, ui::Val::*, +}; -use crate::{asset_tracking::LoadResource, audio::music, screens::Screen, theme::prelude::*}; +use crate::{asset_tracking::LoadResource, audio::music, menus::Menu, theme::prelude::*}; pub(super) fn plugin(app: &mut App) { - app.add_systems(OnEnter(Screen::Credits), spawn_credits_screen); + app.add_systems(OnEnter(Menu::Credits), spawn_credits_menu); + app.add_systems( + Update, + go_back.run_if(in_state(Menu::Credits).and(input_just_pressed(KeyCode::Escape))), + ); app.register_type::(); app.load_resource::(); - app.add_systems(OnEnter(Screen::Credits), start_credits_music); + app.add_systems(OnEnter(Menu::Credits), start_credits_music); } -fn spawn_credits_screen(mut commands: Commands) { +fn spawn_credits_menu(mut commands: Commands) { commands.spawn(( - widget::ui_root("Credits Screen"), - StateScoped(Screen::Credits), + widget::ui_root("Credits Menu"), + StateScoped(Menu::Credits), children![ widget::header("Created by"), created_by(), widget::header("Assets"), assets(), - widget::button("Back", enter_title_screen), + widget::button("Back", go_back_on_click), ], )); } @@ -73,8 +79,12 @@ fn grid(content: Vec<[&'static str; 2]>) -> impl Bundle { ) } -fn enter_title_screen(_: Trigger>, mut next_screen: ResMut>) { - next_screen.set(Screen::Title); +fn go_back_on_click(_: Trigger>, mut next_menu: ResMut>) { + next_menu.set(Menu::Main); +} + +fn go_back(mut next_menu: ResMut>) { + next_menu.set(Menu::Main); } #[derive(Resource, Asset, Clone, Reflect)] @@ -96,7 +106,7 @@ impl FromWorld for CreditsAssets { fn start_credits_music(mut commands: Commands, credits_music: Res) { commands.spawn(( Name::new("Credits Music"), - StateScoped(Screen::Credits), + StateScoped(Menu::Credits), music(credits_music.music.clone()), )); } diff --git a/src/menus/main.rs b/src/menus/main.rs new file mode 100644 index 00000000..ccfe0c11 --- /dev/null +++ b/src/menus/main.rs @@ -0,0 +1,54 @@ +//! The main menu (seen on the title screen). + +use bevy::prelude::*; + +use crate::{asset_tracking::ResourceHandles, menus::Menu, screens::Screen, theme::widget}; + +pub(super) fn plugin(app: &mut App) { + app.add_systems(OnEnter(Menu::Main), spawn_main_menu); +} + +fn spawn_main_menu(mut commands: Commands) { + commands.spawn(( + widget::ui_root("Main Menu"), + StateScoped(Menu::Main), + #[cfg(not(target_family = "wasm"))] + children![ + widget::button("Play", enter_loading_or_gameplay_screen), + widget::button("Settings", open_settings_menu), + widget::button("Credits", open_credits_menu), + widget::button("Exit", exit_app), + ], + #[cfg(target_family = "wasm")] + children![ + widget::button("Play", enter_loading_or_gameplay_screen), + widget::button("Settings", open_settings_menu), + widget::button("Credits", open_credits_menu), + ], + )); +} + +fn enter_loading_or_gameplay_screen( + _: Trigger>, + resource_handles: Res, + mut next_screen: ResMut>, +) { + if resource_handles.is_all_done() { + next_screen.set(Screen::Gameplay); + } else { + next_screen.set(Screen::Loading); + } +} + +fn open_settings_menu(_: Trigger>, mut next_menu: ResMut>) { + next_menu.set(Menu::Settings); +} + +fn open_credits_menu(_: Trigger>, mut next_menu: ResMut>) { + next_menu.set(Menu::Credits); +} + +#[cfg(not(target_family = "wasm"))] +fn exit_app(_: Trigger>, mut app_exit: EventWriter) { + app_exit.write(AppExit::Success); +} diff --git a/src/menus/mod.rs b/src/menus/mod.rs new file mode 100644 index 00000000..97e929a4 --- /dev/null +++ b/src/menus/mod.rs @@ -0,0 +1,30 @@ +//! The game's menus and transitions between them. + +mod credits; +mod main; +mod pause; +mod settings; + +use bevy::prelude::*; + +pub(super) fn plugin(app: &mut App) { + app.init_state::(); + + app.add_plugins(( + credits::plugin, + main::plugin, + settings::plugin, + pause::plugin, + )); +} + +#[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] +#[states(scoped_entities)] +pub enum Menu { + #[default] + None, + Main, + Credits, + Settings, + Pause, +} diff --git a/src/menus/pause.rs b/src/menus/pause.rs new file mode 100644 index 00000000..5a70f4d1 --- /dev/null +++ b/src/menus/pause.rs @@ -0,0 +1,42 @@ +//! The pause menu. + +use bevy::{input::common_conditions::input_just_pressed, prelude::*}; + +use crate::{menus::Menu, screens::Screen, theme::widget}; + +pub(super) fn plugin(app: &mut App) { + app.add_systems(OnEnter(Menu::Pause), spawn_pause_menu); + app.add_systems( + Update, + go_back.run_if(in_state(Menu::Pause).and(input_just_pressed(KeyCode::Escape))), + ); +} + +fn spawn_pause_menu(mut commands: Commands) { + commands.spawn(( + widget::ui_root("Pause Menu"), + StateScoped(Menu::Pause), + children![ + widget::header("Game paused"), + widget::button("Settings", open_settings_menu), + widget::button("Continue", close_menu), + widget::button("Quit to title", quit_to_title), + ], + )); +} + +fn open_settings_menu(_: Trigger>, mut next_menu: ResMut>) { + next_menu.set(Menu::Settings); +} + +fn close_menu(_: Trigger>, mut next_menu: ResMut>) { + next_menu.set(Menu::None); +} + +fn quit_to_title(_: Trigger>, mut next_screen: ResMut>) { + next_screen.set(Screen::Title); +} + +fn go_back(mut next_menu: ResMut>) { + next_menu.set(Menu::None); +} diff --git a/src/screens/settings.rs b/src/menus/settings.rs similarity index 67% rename from src/screens/settings.rs rename to src/menus/settings.rs index aa18fbaa..9f8481ed 100644 --- a/src/screens/settings.rs +++ b/src/menus/settings.rs @@ -1,29 +1,30 @@ -//! A settings screen that can be accessed from the title screen. +//! The settings menu. //! -//! Settings and accessibility options should go here. +//! Additional settings and accessibility options should go here. -use bevy::{audio::Volume, prelude::*, ui::Val::*}; +use bevy::{audio::Volume, input::common_conditions::input_just_pressed, prelude::*, ui::Val::*}; -use crate::{screens::Screen, theme::prelude::*}; +use crate::{menus::Menu, screens::Screen, theme::prelude::*}; pub(super) fn plugin(app: &mut App) { - app.add_systems(OnEnter(Screen::Settings), spawn_settings_screen); - - app.register_type::(); + app.add_systems(OnEnter(Menu::Settings), spawn_settings_menu); app.add_systems( Update, - update_volume_label.run_if(in_state(Screen::Settings)), + go_back.run_if(in_state(Menu::Settings).and(input_just_pressed(KeyCode::Escape))), ); + + app.register_type::(); + app.add_systems(Update, update_volume_label.run_if(in_state(Menu::Settings))); } -fn spawn_settings_screen(mut commands: Commands) { +fn spawn_settings_menu(mut commands: Commands) { commands.spawn(( - widget::ui_root("Settings Screen"), - StateScoped(Screen::Settings), + widget::ui_root("Settings Menu"), + StateScoped(Menu::Settings), children![ widget::header("Settings"), settings_grid(), - widget::button("Back", enter_title_screen), + widget::button("Back", go_back_on_click), ], )); } @@ -99,6 +100,22 @@ fn update_volume_label( label.0 = format!("{percent:3.0}%"); } -fn enter_title_screen(_: Trigger>, mut next_screen: ResMut>) { - next_screen.set(Screen::Title); +fn go_back_on_click( + _: Trigger>, + screen: Res>, + mut next_menu: ResMut>, +) { + next_menu.set(if screen.get() == &Screen::Title { + Menu::Main + } else { + Menu::Pause + }); +} + +fn go_back(screen: Res>, mut next_menu: ResMut>) { + next_menu.set(if screen.get() == &Screen::Title { + Menu::Main + } else { + Menu::Pause + }); } diff --git a/src/screens/gameplay.rs b/src/screens/gameplay.rs index 080916a7..e3ba6369 100644 --- a/src/screens/gameplay.rs +++ b/src/screens/gameplay.rs @@ -2,18 +2,34 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*}; -use crate::{demo::level::spawn_level, screens::Screen}; +use crate::{demo::level::spawn_level, menus::Menu, screens::Screen}; pub(super) fn plugin(app: &mut App) { app.add_systems(OnEnter(Screen::Gameplay), spawn_level); + app.add_systems(OnExit(Screen::Gameplay), close_menu); + // Toggle pause menu on key press. app.add_systems( Update, - return_to_title_screen - .run_if(in_state(Screen::Gameplay).and(input_just_pressed(KeyCode::Escape))), + ( + open_pause_menu.run_if( + in_state(Screen::Gameplay) + .and(in_state(Menu::None)) + .and(input_just_pressed(KeyCode::Escape).or(input_just_pressed(KeyCode::KeyP))), + ), + close_menu.run_if( + in_state(Screen::Gameplay) + .and(in_state(Menu::Pause)) + .and(input_just_pressed(KeyCode::KeyP)), + ), + ), ); } -fn return_to_title_screen(mut next_screen: ResMut>) { - next_screen.set(Screen::Title); +fn open_pause_menu(mut next_menu: ResMut>) { + next_menu.set(Menu::Pause); +} + +fn close_menu(mut next_menu: ResMut>) { + next_menu.set(Menu::None); } diff --git a/src/screens/loading.rs b/src/screens/loading.rs index eb0cfb85..150e1947 100644 --- a/src/screens/loading.rs +++ b/src/screens/loading.rs @@ -1,5 +1,5 @@ -//! A loading screen during which game assets are loaded. -//! This reduces stuttering, especially for audio on WASM. +//! A loading screen during which game assets are loaded if necessary. +//! This reduces stuttering, especially for audio on Wasm. use bevy::prelude::*; diff --git a/src/screens/mod.rs b/src/screens/mod.rs index 48884c0a..30e94416 100644 --- a/src/screens/mod.rs +++ b/src/screens/mod.rs @@ -1,9 +1,7 @@ //! The game's main screen states and transitions between them. -mod credits; mod gameplay; mod loading; -mod settings; mod splash; mod title; @@ -13,24 +11,20 @@ pub(super) fn plugin(app: &mut App) { app.init_state::(); app.add_plugins(( - credits::plugin, gameplay::plugin, loading::plugin, - settings::plugin, splash::plugin, title::plugin, )); } /// The game's main screen states. -#[derive(States, Debug, Hash, PartialEq, Eq, Clone, Default)] +#[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] #[states(scoped_entities)] pub enum Screen { #[default] Splash, Title, - Credits, - Settings, Loading, Gameplay, } diff --git a/src/screens/title.rs b/src/screens/title.rs index 2987021f..cd4e29e6 100644 --- a/src/screens/title.rs +++ b/src/screens/title.rs @@ -1,53 +1,18 @@ -//! The title screen that appears when the game starts. +//! The title screen that appears after the splash screen. use bevy::prelude::*; -use crate::{asset_tracking::ResourceHandles, screens::Screen, theme::prelude::*}; +use crate::{menus::Menu, screens::Screen}; pub(super) fn plugin(app: &mut App) { - app.add_systems(OnEnter(Screen::Title), spawn_title_screen); + app.add_systems(OnEnter(Screen::Title), open_main_menu); + app.add_systems(OnExit(Screen::Title), close_menu); } -fn spawn_title_screen(mut commands: Commands) { - commands.spawn(( - widget::ui_root("Title Screen"), - StateScoped(Screen::Title), - #[cfg(not(target_family = "wasm"))] - children![ - widget::button("Play", enter_loading_or_gameplay_screen), - widget::button("Settings", enter_settings_screen), - widget::button("Credits", enter_credits_screen), - widget::button("Exit", exit_app), - ], - #[cfg(target_family = "wasm")] - children![ - widget::button("Play", enter_loading_or_gameplay_screen), - widget::button("Settings", enter_settings_screen), - widget::button("Credits", enter_credits_screen), - ], - )); +fn open_main_menu(mut next_menu: ResMut>) { + next_menu.set(Menu::Main); } -fn enter_loading_or_gameplay_screen( - _: Trigger>, - resource_handles: Res, - mut next_screen: ResMut>, -) { - if resource_handles.is_all_done() { - next_screen.set(Screen::Gameplay); - } else { - next_screen.set(Screen::Loading); - } -} - -fn enter_settings_screen(_: Trigger>, mut next_screen: ResMut>) { - next_screen.set(Screen::Settings); -} - -fn enter_credits_screen(_: Trigger>, mut next_screen: ResMut>) { - next_screen.set(Screen::Credits); -} -#[cfg(not(target_family = "wasm"))] -fn exit_app(_: Trigger>, mut app_exit: EventWriter) { - app_exit.write(AppExit::Success); +fn close_menu(mut next_menu: ResMut>) { + next_menu.set(Menu::None); }