From 4fdf8e078fd7d9bc4abc69bd09cb3c7fc4975afd Mon Sep 17 00:00:00 2001 From: Martin Indra Date: Sun, 23 Jul 2023 15:48:50 +0200 Subject: [PATCH] Improve state transitioning (#637) --- Cargo.lock | 1 + crates/core/Cargo.toml | 1 + crates/core/src/gamestate.rs | 50 ++++++++--------------- crates/core/src/lib.rs | 4 +- crates/core/src/transition.rs | 76 +++++++++++++++++++++++++++++++++++ crates/menu/src/lib.rs | 58 +++++++------------------- 6 files changed, 111 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6df570b7..b659739b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2239,6 +2239,7 @@ dependencies = [ "nalgebra", "parry2d", "parry3d", + "paste", "serde", "thiserror", ] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 56587f09..9d852af0 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -22,5 +22,6 @@ iyes_progress.workspace = true nalgebra.workspace = true parry2d.workspace = true parry3d.workspace = true +paste.workspace = true serde.workspace = true thiserror.workspace = true diff --git a/crates/core/src/gamestate.rs b/crates/core/src/gamestate.rs index ade7b3d6..3461f42a 100644 --- a/crates/core/src/gamestate.rs +++ b/crates/core/src/gamestate.rs @@ -1,50 +1,32 @@ use bevy::prelude::*; -use iyes_progress::prelude::*; +use iyes_progress::ProgressPlugin; -use crate::{ - gconfig::GameConfig, - state::AppState, - transition::{DeStateTransition, StateWithSet}, -}; +use crate::{gconfig::GameConfig, nested_state, state::AppState}; -pub(super) struct GameStatePlugin; +pub(crate) struct GameStateSetupPlugin; -impl Plugin for GameStatePlugin { +impl Plugin for GameStateSetupPlugin { fn build(&self, app: &mut App) { - app.add_state_with_set::() - .configure_sets((AppState::state_set(), GameState::state_set()).chain()) - .add_plugin(ProgressPlugin::new(GameState::Loading).continue_to(GameState::Playing)) - .add_system(setup.in_schedule(OnEnter(AppState::InGame))) - .add_system(cleanup.in_schedule(OnExit(AppState::InGame))); + app.add_plugin(GameStatePlugin) + .add_plugin(ProgressPlugin::new(GameState::Loading).continue_to(GameState::Playing)); } } -/// Phase of an already started game. The game might be still loading or -/// finishing. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, States)] -pub enum GameState { - #[default] - None, - Loading, - Playing, -} - -impl StateWithSet for GameState { - type Set = GameStateSet; - - fn state_set() -> Self::Set { - GameStateSet +nested_state!( + AppState::InGame -> GameState, + doc = "Phase of an already started game. The game might be still loading or finishing.", + enter = setup, + exit = cleanup, + variants = { + Loading, + Playing, } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, SystemSet)] -pub struct GameStateSet; +); fn setup(mut next_state: ResMut>) { next_state.set(GameState::Loading); } -fn cleanup(mut commands: Commands, mut next_state: ResMut>) { +fn cleanup(mut commands: Commands) { commands.remove_resource::(); - next_state.set(GameState::None); } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 9a7d9a4e..83ae84de 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,7 +1,7 @@ use baseset::GameSetsPlugin; use bevy::{app::PluginGroupBuilder, prelude::PluginGroup}; use cleanup::CleanupPlugin; -use gamestate::GameStatePlugin; +use gamestate::GameStateSetupPlugin; use iyes_progress::prelude::*; use state::AppState; use visibility::VisibilityPlugin; @@ -33,7 +33,7 @@ impl PluginGroup for CorePluginGroup { PluginGroupBuilder::start::() .add(ProgressPlugin::new(AppState::AppLoading).continue_to(AppState::InMenu)) .add(GameSetsPlugin) - .add(GameStatePlugin) + .add(GameStateSetupPlugin) .add(VisibilityPlugin) .add(CleanupPlugin) } diff --git a/crates/core/src/transition.rs b/crates/core/src/transition.rs index 1db2c879..1bb13554 100644 --- a/crates/core/src/transition.rs +++ b/crates/core/src/transition.rs @@ -2,12 +2,15 @@ use bevy::{ ecs::schedule::{run_enter_schedule, FreeSystemSet}, prelude::*, }; +pub use paste; pub trait DeStateTransition { /// This method is almost equal to Bevy's [`App::add_state`]. The only /// difference is that the state transition is added to an associated /// state. See [`StateWithSet`]. fn add_state_with_set(&mut self) -> &mut Self; + + fn add_child_state(&mut self) -> &mut Self; } pub trait StateWithSet { @@ -53,4 +56,77 @@ impl DeStateTransition for App { self } + + fn add_child_state(&mut self) -> &mut Self { + self.add_state_with_set::(); + self.configure_sets((P::state_set(), S::state_set()).chain()); + self + } +} + +/// Creates a Bevy state and a Bevy plugin. +/// +/// The child state is bound to a given parent state with this syntax: +/// `ParentState::ParentVariant -> ChildState`. +/// +/// Transitions of the child state are scheduled after transitions of the +/// parent state. +/// +/// Variant `None` is automatically added to the child state. Additional +/// variants are configurable with `variants` argument. +/// +/// `None` variant of the child state is entered when the given variant of the +/// parent state is exited. The `enter` system is called on enter of the parent +/// variant and the `exit` system is called on exit of the parent variant. +#[macro_export] +macro_rules! nested_state { + ( + $parent:ident::$parent_variant:ident -> $name:ident, + doc = $doc:expr, + $(enter = $enter:ident,)? + $(exit = $exit:ident,)? + variants = { + $($variant:ident),* $(,)? + } + ) => { + use $crate::transition::{DeStateTransition, paste::paste, StateWithSet}; + + paste! { + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, SystemSet)] + pub struct [<$name Plugin>]; + + impl Plugin for [<$name Plugin>] { + fn build(&self, app: &mut App) { + app.add_child_state::<$parent, $name>() + .add_system(go_to_none.in_schedule(OnExit($parent::$parent_variant))) + $(.add_system($enter.in_schedule(OnEnter($parent::$parent_variant))))? + $(.add_system($exit.in_schedule(OnExit($parent::$parent_variant))))?; + } + } + + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, States)] + #[doc = $doc] + pub enum $name { + #[default] + None, + $($variant),* + } + + impl StateWithSet for $name { + type Set = [<$name Set>]; + + fn state_set() -> Self::Set { + [<$name Set>] + } + } + + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, SystemSet)] + pub struct [<$name Set>]; + + fn go_to_none(mut next_state: ResMut>) { + next_state.set($name::None); + } + } + }; } diff --git a/crates/menu/src/lib.rs b/crates/menu/src/lib.rs index f6c38057..0de69792 100644 --- a/crates/menu/src/lib.rs +++ b/crates/menu/src/lib.rs @@ -1,11 +1,7 @@ use aftergame::AfterGamePlugin; use bevy::{app::PluginGroupBuilder, prelude::*}; use create::CreateGamePlugin; -use de_core::{ - gresult::GameResult, - state::AppState, - transition::{DeStateTransition, StateWithSet}, -}; +use de_core::{gresult::GameResult, nested_state, state::AppState}; use gamelisting::GameListingPlugin; use mainmenu::MainMenuPlugin; use mapselection::MapSelectionPlugin; @@ -28,7 +24,7 @@ pub struct MenuPluginGroup; impl PluginGroup for MenuPluginGroup { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() - .add(MenuSetupPlugin) + .add(MenuStatePlugin) .add(MenuPlugin) .add(MainMenuPlugin) .add(MapSelectionPlugin) @@ -40,40 +36,20 @@ impl PluginGroup for MenuPluginGroup { } } -struct MenuSetupPlugin; - -impl Plugin for MenuSetupPlugin { - fn build(&self, app: &mut App) { - app.add_state_with_set::() - .configure_sets((AppState::state_set(), MenuState::state_set()).chain()) - .add_system(menu_entered_system.in_schedule(OnEnter(AppState::InMenu))) - .add_system(menu_exited_system.in_schedule(OnExit(AppState::InMenu))); +nested_state!( + AppState::InMenu -> MenuState, + doc = "Top-level menu state. Each variant corresponds to a single menu screen.", + enter = menu_entered_system, + variants = { + MainMenu, + SinglePlayerGame, + SignIn, + GameListing, + GameCreation, + MultiPlayerGame, + AfterGame, } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, States)] -pub(crate) enum MenuState { - #[default] - None, - MainMenu, - SinglePlayerGame, - SignIn, - GameListing, - GameCreation, - MultiPlayerGame, - AfterGame, -} - -impl StateWithSet for MenuState { - type Set = MenuStateSet; - - fn state_set() -> Self::Set { - MenuStateSet - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, SystemSet)] -pub struct MenuStateSet; +); fn menu_entered_system( mut next_state: ResMut>, @@ -85,7 +61,3 @@ fn menu_entered_system( next_state.set(MenuState::MainMenu); } } - -fn menu_exited_system(mut next_state: ResMut>) { - next_state.set(MenuState::None); -}