Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/state/computed_states.rs
6595 views
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
19
use bevy::{dev_tools::states::*, prelude::*};
20
21
use ui::*;
22
23
// To begin, we want to define our state objects.
24
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
25
enum 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)]
40
enum 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)]
51
struct InGame;
52
53
impl 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)]
79
struct TurboMode;
80
81
impl 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)]
103
enum IsPaused {
104
NotPaused,
105
Paused,
106
}
107
108
impl 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)]
128
enum Tutorial {
129
MovementInstructions,
130
PauseInstructions,
131
}
132
133
impl 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
167
fn 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
216
fn 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
269
fn 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
284
fn 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
299
fn 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
305
mod 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
}
604
605