Skip to main content

sub_states/
sub_states.rs

1//! This example illustrates the use of [`SubStates`] for more complex state handling patterns.
2//!
3//! [`SubStates`] are [`States`] that only exist while the App is in another [`State`]. They can
4//! be used to create more complex patterns while relying on simple enums, or to de-couple certain
5//! elements of complex state objects.
6//!
7//! In this case, we're transitioning from a `Menu` state to an `InGame` state, at which point we create
8//! a substate called `IsPaused` to track whether the game is paused or not.
9
10use bevy::{
11    dev_tools::states::*,
12    picking::hover::Hovered,
13    prelude::*,
14    ui_widgets::{Activate, ActivateOnPress, Button},
15};
16
17use ui::*;
18
19#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
20enum AppState {
21    #[default]
22    Menu,
23    InGame,
24}
25
26// In this case, instead of deriving `States`, we derive `SubStates`
27#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, SubStates)]
28// And we need to add an attribute to let us know what the source state is
29// and what value it needs to have. This will ensure that unless we're
30// in [`AppState::InGame`], the [`IsPaused`] state resource
31// will not exist.
32#[source(AppState = AppState::InGame)]
33enum IsPaused {
34    #[default]
35    Running,
36    Paused,
37}
38
39fn main() {
40    App::new()
41        .add_plugins(DefaultPlugins)
42        .init_state::<AppState>()
43        .add_sub_state::<IsPaused>() // We set the substate up here.
44        // Most of these remain the same
45        .add_systems(Startup, setup)
46        .add_systems(OnEnter(AppState::Menu), setup_menu)
47        .add_observer(on_activate_start_game.run_if(in_state(AppState::Menu)))
48        .add_systems(Update, hover_style)
49        .add_systems(OnExit(AppState::Menu), cleanup_menu)
50        .add_systems(OnEnter(AppState::InGame), setup_game)
51        .add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
52        .add_systems(
53            Update,
54            (
55                // Instead of relying on [`AppState::InGame`] here, we're relying on
56                // [`IsPaused::Running`], since we don't want movement or color changes
57                // if we're paused
58                (movement, change_color).run_if(in_state(IsPaused::Running)),
59                // The pause toggle, on the other hand, needs to work whether we're
60                // paused or not, so it uses [`AppState::InGame`] instead.
61                toggle_pause.run_if(in_state(AppState::InGame)),
62            ),
63        )
64        .add_systems(Update, log_transitions::<AppState>)
65        .run();
66}
67
68fn on_activate_start_game(_: On<Activate>, mut next_state: ResMut<NextState<AppState>>) {
69    next_state.set(AppState::InGame);
70}
71
72fn hover_style(
73    mut button_query: Query<(&Hovered, &mut BackgroundColor), (Changed<Hovered>, With<Button>)>,
74) {
75    for (hovered, mut color) in &mut button_query {
76        if hovered.get() {
77            *color = HOVERED_BUTTON.into();
78        } else {
79            *color = NORMAL_BUTTON.into();
80        }
81    }
82}
83
84fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
85    commands.entity(menu_data.button_entity).despawn();
86}
87
88const SPEED: f32 = 100.0;
89fn movement(
90    time: Res<Time>,
91    input: Res<ButtonInput<KeyCode>>,
92    mut query: Query<&mut Transform, With<Sprite>>,
93) {
94    for mut transform in &mut query {
95        let mut direction = Vec3::ZERO;
96        if input.pressed(KeyCode::ArrowLeft) {
97            direction.x -= 1.0;
98        }
99        if input.pressed(KeyCode::ArrowRight) {
100            direction.x += 1.0;
101        }
102        if input.pressed(KeyCode::ArrowUp) {
103            direction.y += 1.0;
104        }
105        if input.pressed(KeyCode::ArrowDown) {
106            direction.y -= 1.0;
107        }
108
109        if direction != Vec3::ZERO {
110            transform.translation += direction.normalize() * SPEED * time.delta_secs();
111        }
112    }
113}
114
115fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
116    for mut sprite in &mut query {
117        let new_color = LinearRgba {
118            blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
119            ..LinearRgba::from(sprite.color)
120        };
121
122        sprite.color = new_color.into();
123    }
124}
125
126fn toggle_pause(
127    input: Res<ButtonInput<KeyCode>>,
128    current_state: Res<State<IsPaused>>,
129    mut next_state: ResMut<NextState<IsPaused>>,
130) {
131    if input.just_pressed(KeyCode::Space) {
132        next_state.set(match current_state.get() {
133            IsPaused::Running => IsPaused::Paused,
134            IsPaused::Paused => IsPaused::Running,
135        });
136    }
137}
138
139mod ui {
140    use crate::*;
141
142    #[derive(Resource)]
143    pub struct MenuData {
144        pub button_entity: Entity,
145    }
146
147    pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
148    pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
149
150    pub fn setup(mut commands: Commands) {
151        commands.spawn(Camera2d);
152    }
153
154    pub fn setup_menu(mut commands: Commands) {
155        let button_entity = commands
156            .spawn((
157                Node {
158                    // center button
159                    width: percent(100),
160                    height: percent(100),
161                    justify_content: JustifyContent::Center,
162                    align_items: AlignItems::Center,
163                    ..default()
164                },
165                children![(
166                    Button,
167                    ActivateOnPress,
168                    Hovered::default(),
169                    Node {
170                        width: px(150),
171                        height: px(65),
172                        // horizontally center child text
173                        justify_content: JustifyContent::Center,
174                        // vertically center child text
175                        align_items: AlignItems::Center,
176                        ..default()
177                    },
178                    BackgroundColor(NORMAL_BUTTON),
179                    children![(
180                        Text::new("Play"),
181                        TextFont {
182                            font_size: FontSize::Px(33.0),
183                            ..default()
184                        },
185                        TextColor(Color::srgb(0.9, 0.9, 0.9)),
186                    )]
187                )],
188            ))
189            .id();
190        commands.insert_resource(MenuData { button_entity });
191    }
192
193    pub fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
194        commands.spawn(Sprite::from_image(asset_server.load("branding/icon.png")));
195
196        commands.spawn_scene(bsn! {
197            Node {
198                margin: px(10),
199            }
200            Text("Move with arrow keys.\nPress SPACEBAR to pause.")
201        });
202    }
203
204    pub fn setup_paused_screen(mut commands: Commands) {
205        commands.spawn((
206            DespawnOnExit(IsPaused::Paused),
207            Node {
208                // center button
209                width: percent(100),
210                height: percent(100),
211                justify_content: JustifyContent::Center,
212                align_items: AlignItems::Center,
213                flex_direction: FlexDirection::Column,
214                row_gap: px(10),
215                ..default()
216            },
217            children![(
218                Node {
219                    width: px(400),
220                    height: px(400),
221                    // horizontally center child text
222                    justify_content: JustifyContent::Center,
223                    // vertically center child text
224                    align_items: AlignItems::Center,
225                    ..default()
226                },
227                BackgroundColor(NORMAL_BUTTON),
228                children![(
229                    Text::new("Paused"),
230                    TextFont {
231                        font_size: FontSize::Px(33.0),
232                        ..default()
233                    },
234                    TextColor(Color::srgb(0.9, 0.9, 0.9)),
235                )]
236            )],
237        ));
238    }
239}