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