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