Skip to main content

computed_states/
computed_states.rs

1//! This example illustrates the use of [`ComputedStates`] for more complex state handling patterns.
2//!
3//! In this case, we'll be implementing the following pattern:
4//! - The game will start in a `Menu` state, which we can return to with `Esc`
5//! - From there, we can enter the game - where our bevy symbol moves around and changes color
6//! - While in game, we can pause and unpause the game using `Space`
7//! - We can also toggle "Turbo Mode" with the `T` key - where the movement and color changes are all faster. This
8//!   is retained between pauses, but not if we exit to the main menu.
9//!
10//! In addition, we want to enable a "tutorial" mode, which will involve its own state that is toggled in the main menu.
11//! This will display instructions about movement and turbo mode when in game and unpaused, and instructions on how to unpause when paused.
12//!
13//! To implement this, we will create 2 root-level states: `AppState` and `TutorialState`.
14//! We will then create some computed states that derive from `AppState`: `InGame` and `TurboMode` are marker states implemented
15//! as Zero-Sized Structs (ZSTs), while `IsPaused` is an enum with 2 distinct states.
16//! And lastly, we'll add `Tutorial`, a computed state deriving from `TutorialState`, `InGame` and `IsPaused`, with 2 distinct
17//! states to display the 2 tutorial texts.
18
19use bevy::{dev_tools::states::*, input::keyboard::Key, prelude::*};
20
21use ui::*;
22
23// To begin, we want to define our state objects.
24#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
25enum AppState {
26    #[default]
27    Menu,
28    // Unlike in the `states` example, we're adding more data in this
29    // version of our AppState. In this case, we actually have
30    // 4 distinct "InGame" states - unpaused and no turbo, paused and no
31    // turbo, unpaused and turbo and paused and turbo.
32    InGame {
33        paused: bool,
34        turbo: bool,
35    },
36}
37
38// The tutorial state object, on the other hand, is a fairly simple enum.
39#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
40enum TutorialState {
41    #[default]
42    Active,
43    Inactive,
44}
45
46// Because we have 4 distinct values of `AppState` that mean we're "InGame", we're going to define
47// a separate "InGame" type and implement `ComputedStates` for it.
48// This allows us to only need to check against one type
49// when otherwise we'd need to check against multiple.
50#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
51struct InGame;
52
53impl ComputedStates for InGame {
54    // Our computed state depends on `AppState`, so we need to specify it as the SourceStates type.
55    type SourceStates = AppState;
56
57    // This is necessary to prevent `setup_game` from running when the app is already in `AppState::InGame`
58    // and only `paused` and `turbo` are changed
59    const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
60    // The compute function takes in the `SourceStates`
61    fn compute(sources: AppState) -> Option<Self> {
62        // You might notice that InGame has no values - instead, in this case, the `State<InGame>` resource only exists
63        // if the `compute` function would return `Some` - so only when we are in game.
64        match sources {
65            // No matter what the value of `paused` or `turbo` is, we're still in the game rather than a menu
66            AppState::InGame { .. } => Some(Self),
67            _ => None,
68        }
69    }
70}
71
72// Similarly, we want to have the TurboMode state - so we'll define that now.
73//
74// Having it separate from [`InGame`] and [`AppState`] like this allows us to check each of them separately, rather than
75// needing to compare against every version of the AppState that could involve them.
76//
77// In addition, it allows us to still maintain a strict type representation - you can't Turbo
78// if you aren't in game, for example - while still having the
79// flexibility to check for the states as if they were completely unrelated.
80
81#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
82struct TurboMode;
83
84impl ComputedStates for TurboMode {
85    type SourceStates = AppState;
86    const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
87
88    fn compute(sources: AppState) -> Option<Self> {
89        match sources {
90            AppState::InGame { turbo: true, .. } => Some(Self),
91            _ => None,
92        }
93    }
94}
95
96// For the [`IsPaused`] state, we'll actually use an `enum` - because the difference between `Paused` and `NotPaused`
97// involve activating different systems.
98//
99// To clarify the difference, `InGame` and `TurboMode` both activate systems if they exist, and there is
100// no variation within them. So we defined them as Zero-Sized Structs.
101//
102// In contrast, pausing actually involve 3 distinct potential situations:
103// - it doesn't exist - this is when being paused is meaningless, like in the menu.
104// - it is `NotPaused` - in which elements like the movement system are active.
105// - it is `Paused` - in which those game systems are inactive, and a pause screen is shown.
106#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
107enum IsPaused {
108    NotPaused,
109    Paused,
110}
111
112impl ComputedStates for IsPaused {
113    type SourceStates = AppState;
114    const ALLOW_SAME_STATE_TRANSITIONS: bool = false;
115
116    fn compute(sources: AppState) -> Option<Self> {
117        // Here we convert from our [`AppState`] to all potential [`IsPaused`] versions.
118        match sources {
119            AppState::InGame { paused: true, .. } => Some(Self::Paused),
120            AppState::InGame { paused: false, .. } => Some(Self::NotPaused),
121            // If `AppState` is not `InGame`, pausing is meaningless, and so we set it to `None`.
122            _ => None,
123        }
124    }
125}
126
127// Lastly, we have our tutorial, which actually has a more complex derivation.
128//
129// Like `IsPaused`, the tutorial has a few fully distinct possible states, so we want to represent them
130// as an Enum. However - in this case they are all dependent on multiple states: the root [`TutorialState`],
131// and both [`InGame`] and [`IsPaused`] - which are in turn derived from [`AppState`].
132#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
133enum Tutorial {
134    MovementInstructions,
135    PauseInstructions,
136}
137
138impl ComputedStates for Tutorial {
139    // We can also use tuples of types that implement [`States`] as our [`SourceStates`].
140    // That includes other [`ComputedStates`] - though circular dependencies are not supported
141    // and will produce a compile error.
142    //
143    // We could define this as relying on [`TutorialState`] and [`AppState`] instead, but
144    // then we would need to duplicate the derivation logic for [`InGame`] and [`IsPaused`].
145    // In this example that is not a significant undertaking, but as a rule it is likely more
146    // effective to rely on the already derived states to avoid the logic drifting apart.
147    //
148    // Notice that you can wrap any of the [`States`] here in [`Option`]s. If you do so,
149    // the computation will get called even if the state does not exist.
150    type SourceStates = (TutorialState, InGame, Option<IsPaused>);
151
152    // Notice that we aren't using InGame - we're just using it as a source state to
153    // prevent the computation from executing if we're not in game. Instead - this
154    // ComputedState will just not exist in that situation.
155    fn compute(
156        (tutorial_state, _in_game, is_paused): (TutorialState, InGame, Option<IsPaused>),
157    ) -> Option<Self> {
158        // If the tutorial is inactive we don't need to worry about it.
159        if !matches!(tutorial_state, TutorialState::Active) {
160            return None;
161        }
162
163        // If we're paused, we're in the PauseInstructions tutorial
164        // Otherwise, we're in the MovementInstructions tutorial
165        match is_paused? {
166            IsPaused::NotPaused => Some(Tutorial::MovementInstructions),
167            IsPaused::Paused => Some(Tutorial::PauseInstructions),
168        }
169    }
170}
171
172fn main() {
173    // We start the setup like we did in the states example.
174    App::new()
175        .add_plugins(DefaultPlugins)
176        .init_state::<AppState>()
177        .init_state::<TutorialState>()
178        // After initializing the normal states, we'll use `.add_computed_state::<CS>()` to initialize our `ComputedStates`
179        .add_computed_state::<InGame>()
180        .add_computed_state::<IsPaused>()
181        .add_computed_state::<TurboMode>()
182        .add_computed_state::<Tutorial>()
183        // we can then resume adding systems just like we would in any other case,
184        // using our states as normal.
185        .add_systems(Startup, setup)
186        .add_systems(OnEnter(AppState::Menu), setup_menu)
187        .add_systems(Update, menu.run_if(in_state(AppState::Menu)))
188        .add_systems(OnExit(AppState::Menu), cleanup_menu)
189        // We only want to run the [`setup_game`] function when we enter the [`AppState::InGame`] state, regardless
190        // of whether the game is paused or not.
191        .add_systems(OnEnter(InGame), setup_game)
192        // We want the color change, toggle_pause and quit_to_menu systems to ignore the paused condition, so we can use the [`InGame`] derived
193        // state here as well.
194        .add_systems(
195            Update,
196            (toggle_pause, change_color, quit_to_menu).run_if(in_state(InGame)),
197        )
198        // However, we only want to move or toggle turbo mode if we are not in a paused state.
199        .add_systems(
200            Update,
201            (toggle_turbo, movement).run_if(in_state(IsPaused::NotPaused)),
202        )
203        // We can continue setting things up, following all the same patterns used above and in the `states` example.
204        .add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
205        .add_systems(OnEnter(TurboMode), setup_turbo_text)
206        .add_systems(
207            OnEnter(Tutorial::MovementInstructions),
208            movement_instructions,
209        )
210        .add_systems(OnEnter(Tutorial::PauseInstructions), pause_instructions)
211        .add_systems(
212            Update,
213            (
214                log_transitions::<AppState>,
215                log_transitions::<TutorialState>,
216            ),
217        )
218        .run();
219}
220
221fn menu(
222    mut next_state: ResMut<NextState<AppState>>,
223    tutorial_state: Res<State<TutorialState>>,
224    mut next_tutorial: ResMut<NextState<TutorialState>>,
225    mut interaction_query: Query<
226        (&Interaction, &mut BackgroundColor, &MenuButton),
227        (Changed<Interaction>, With<Button>),
228    >,
229) {
230    for (interaction, mut color, menu_button) in &mut interaction_query {
231        match *interaction {
232            Interaction::Pressed => {
233                *color = if menu_button == &MenuButton::Tutorial
234                    && tutorial_state.get() == &TutorialState::Active
235                {
236                    PRESSED_ACTIVE_BUTTON.into()
237                } else {
238                    PRESSED_BUTTON.into()
239                };
240
241                match menu_button {
242                    MenuButton::Play => next_state.set(AppState::InGame {
243                        paused: false,
244                        turbo: false,
245                    }),
246                    MenuButton::Tutorial => next_tutorial.set(match tutorial_state.get() {
247                        TutorialState::Active => TutorialState::Inactive,
248                        TutorialState::Inactive => TutorialState::Active,
249                    }),
250                };
251            }
252            Interaction::Hovered => {
253                if menu_button == &MenuButton::Tutorial
254                    && tutorial_state.get() == &TutorialState::Active
255                {
256                    *color = HOVERED_ACTIVE_BUTTON.into();
257                } else {
258                    *color = HOVERED_BUTTON.into();
259                }
260            }
261            Interaction::None => {
262                if menu_button == &MenuButton::Tutorial
263                    && tutorial_state.get() == &TutorialState::Active
264                {
265                    *color = ACTIVE_BUTTON.into();
266                } else {
267                    *color = NORMAL_BUTTON.into();
268                }
269            }
270        }
271    }
272}
273
274fn toggle_pause(
275    input: Res<ButtonInput<KeyCode>>,
276    current_state: Res<State<AppState>>,
277    mut next_state: ResMut<NextState<AppState>>,
278) {
279    if input.just_pressed(KeyCode::Space)
280        && let AppState::InGame { paused, turbo } = current_state.get()
281    {
282        next_state.set(AppState::InGame {
283            paused: !*paused,
284            turbo: *turbo,
285        });
286    }
287}
288
289fn toggle_turbo(
290    input: Res<ButtonInput<Key>>,
291    current_state: Res<State<AppState>>,
292    mut next_state: ResMut<NextState<AppState>>,
293) {
294    if input.just_pressed(Key::Character("t".into()))
295        && let AppState::InGame { paused, turbo } = current_state.get()
296    {
297        next_state.set(AppState::InGame {
298            paused: *paused,
299            turbo: !*turbo,
300        });
301    }
302}
303
304fn quit_to_menu(input: Res<ButtonInput<KeyCode>>, mut next_state: ResMut<NextState<AppState>>) {
305    if input.just_pressed(KeyCode::Escape) {
306        next_state.set(AppState::Menu);
307    }
308}
309
310mod ui {
311    use crate::*;
312
313    #[derive(Resource)]
314    pub struct MenuData {
315        pub root_entity: Entity,
316    }
317
318    #[derive(Component, PartialEq, Eq)]
319    pub enum MenuButton {
320        Play,
321        Tutorial,
322    }
323
324    pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
325    pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
326    pub const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
327
328    pub const ACTIVE_BUTTON: Color = Color::srgb(0.15, 0.85, 0.15);
329    pub const HOVERED_ACTIVE_BUTTON: Color = Color::srgb(0.25, 0.55, 0.25);
330    pub const PRESSED_ACTIVE_BUTTON: Color = Color::srgb(0.35, 0.95, 0.35);
331
332    pub fn setup(mut commands: Commands) {
333        commands.spawn(Camera2d);
334    }
335
336    pub fn setup_menu(mut commands: Commands, tutorial_state: Res<State<TutorialState>>) {
337        let button_entity = commands
338            .spawn((
339                Node {
340                    // center button
341                    width: percent(100),
342                    height: percent(100),
343                    justify_content: JustifyContent::Center,
344                    align_items: AlignItems::Center,
345                    flex_direction: FlexDirection::Column,
346                    row_gap: px(10),
347                    ..default()
348                },
349                children![
350                    (
351                        Button,
352                        Node {
353                            width: px(200),
354                            height: px(65),
355                            // horizontally center child text
356                            justify_content: JustifyContent::Center,
357                            // vertically center child text
358                            align_items: AlignItems::Center,
359                            ..default()
360                        },
361                        BackgroundColor(NORMAL_BUTTON),
362                        MenuButton::Play,
363                        children![(
364                            Text::new("Play"),
365                            TextFont {
366                                font_size: FontSize::Px(33.0),
367                                ..default()
368                            },
369                            TextColor(Color::srgb(0.9, 0.9, 0.9)),
370                        )],
371                    ),
372                    (
373                        Button,
374                        Node {
375                            width: px(200),
376                            height: px(65),
377                            // horizontally center child text
378                            justify_content: JustifyContent::Center,
379                            // vertically center child text
380                            align_items: AlignItems::Center,
381                            ..default()
382                        },
383                        BackgroundColor(match tutorial_state.get() {
384                            TutorialState::Active => ACTIVE_BUTTON,
385                            TutorialState::Inactive => NORMAL_BUTTON,
386                        }),
387                        MenuButton::Tutorial,
388                        children![(
389                            Text::new("Tutorial"),
390                            TextFont {
391                                font_size: FontSize::Px(33.0),
392                                ..default()
393                            },
394                            TextColor(Color::srgb(0.9, 0.9, 0.9)),
395                        )]
396                    ),
397                ],
398            ))
399            .id();
400        commands.insert_resource(MenuData {
401            root_entity: button_entity,
402        });
403    }
404
405    pub fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
406        commands.entity(menu_data.root_entity).despawn();
407    }
408
409    pub fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
410        commands.spawn((
411            DespawnOnExit(InGame),
412            Sprite::from_image(asset_server.load("branding/icon.png")),
413        ));
414    }
415
416    const SPEED: f32 = 100.0;
417    const TURBO_SPEED: f32 = 300.0;
418
419    pub fn movement(
420        time: Res<Time>,
421        input: Res<ButtonInput<KeyCode>>,
422        turbo: Option<Res<State<TurboMode>>>,
423        mut query: Query<&mut Transform, With<Sprite>>,
424    ) {
425        for mut transform in &mut query {
426            let mut direction = Vec3::ZERO;
427            if input.pressed(KeyCode::ArrowLeft) {
428                direction.x -= 1.0;
429            }
430            if input.pressed(KeyCode::ArrowRight) {
431                direction.x += 1.0;
432            }
433            if input.pressed(KeyCode::ArrowUp) {
434                direction.y += 1.0;
435            }
436            if input.pressed(KeyCode::ArrowDown) {
437                direction.y -= 1.0;
438            }
439
440            if direction != Vec3::ZERO {
441                transform.translation += direction.normalize()
442                    * if turbo.is_some() { TURBO_SPEED } else { SPEED }
443                    * time.delta_secs();
444            }
445        }
446    }
447
448    pub fn setup_paused_screen(mut commands: Commands) {
449        info!("Printing Pause");
450        commands.spawn((
451            DespawnOnExit(IsPaused::Paused),
452            Node {
453                // center button
454                width: percent(100),
455                height: percent(100),
456                justify_content: JustifyContent::Center,
457                align_items: AlignItems::Center,
458                flex_direction: FlexDirection::Column,
459                row_gap: px(10),
460                position_type: PositionType::Absolute,
461                ..default()
462            },
463            children![(
464                Node {
465                    width: px(400),
466                    height: px(400),
467                    // horizontally center child text
468                    justify_content: JustifyContent::Center,
469                    // vertically center child text
470                    align_items: AlignItems::Center,
471                    ..default()
472                },
473                BackgroundColor(NORMAL_BUTTON),
474                MenuButton::Play,
475                children![(
476                    Text::new("Paused"),
477                    TextFont {
478                        font_size: FontSize::Px(33.0),
479                        ..default()
480                    },
481                    TextColor(Color::srgb(0.9, 0.9, 0.9)),
482                )],
483            ),],
484        ));
485    }
486
487    pub fn setup_turbo_text(mut commands: Commands) {
488        commands.spawn((
489            DespawnOnExit(TurboMode),
490            Node {
491                // center button
492                width: percent(100),
493                height: percent(100),
494                justify_content: JustifyContent::Start,
495                align_items: AlignItems::Center,
496                flex_direction: FlexDirection::Column,
497                row_gap: px(10),
498                position_type: PositionType::Absolute,
499                ..default()
500            },
501            children![(
502                Text::new("TURBO MODE"),
503                TextFont {
504                    font_size: FontSize::Px(33.0),
505                    ..default()
506                },
507                TextColor(Color::srgb(0.9, 0.3, 0.1)),
508            )],
509        ));
510    }
511
512    pub fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
513        for mut sprite in &mut query {
514            let new_color = LinearRgba {
515                blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
516                ..LinearRgba::from(sprite.color)
517            };
518
519            sprite.color = new_color.into();
520        }
521    }
522
523    pub fn movement_instructions(mut commands: Commands) {
524        commands.spawn((
525            DespawnOnExit(Tutorial::MovementInstructions),
526            Node {
527                // center button
528                width: percent(100),
529                height: percent(100),
530                justify_content: JustifyContent::End,
531                align_items: AlignItems::Center,
532                flex_direction: FlexDirection::Column,
533                row_gap: px(10),
534                position_type: PositionType::Absolute,
535                ..default()
536            },
537            children![
538                (
539                    Text::new("Move the bevy logo with the arrow keys"),
540                    TextFont {
541                        font_size: FontSize::Px(33.0),
542                        ..default()
543                    },
544                    TextColor(Color::srgb(0.3, 0.3, 0.7)),
545                ),
546                (
547                    Text::new("Press T to enter TURBO MODE"),
548                    TextFont {
549                        font_size: FontSize::Px(33.0),
550                        ..default()
551                    },
552                    TextColor(Color::srgb(0.3, 0.3, 0.7)),
553                ),
554                (
555                    Text::new("Press SPACE to pause"),
556                    TextFont {
557                        font_size: FontSize::Px(33.0),
558                        ..default()
559                    },
560                    TextColor(Color::srgb(0.3, 0.3, 0.7)),
561                ),
562                (
563                    Text::new("Press ESCAPE to return to the menu"),
564                    TextFont {
565                        font_size: FontSize::Px(33.0),
566                        ..default()
567                    },
568                    TextColor(Color::srgb(0.3, 0.3, 0.7)),
569                ),
570            ],
571        ));
572    }
573
574    pub fn pause_instructions(mut commands: Commands) {
575        commands.spawn((
576            DespawnOnExit(Tutorial::PauseInstructions),
577            Node {
578                // center button
579                width: percent(100),
580                height: percent(100),
581                justify_content: JustifyContent::End,
582                align_items: AlignItems::Center,
583                flex_direction: FlexDirection::Column,
584                row_gap: px(10),
585                position_type: PositionType::Absolute,
586                ..default()
587            },
588            children![
589                (
590                    Text::new("Press SPACE to resume"),
591                    TextFont {
592                        font_size: FontSize::Px(33.0),
593                        ..default()
594                    },
595                    TextColor(Color::srgb(0.3, 0.3, 0.7)),
596                ),
597                (
598                    Text::new("Press ESCAPE to return to the menu"),
599                    TextFont {
600                        font_size: FontSize::Px(33.0),
601                        ..default()
602                    },
603                    TextColor(Color::srgb(0.3, 0.3, 0.7)),
604                ),
605            ],
606        ));
607    }
608}