Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/games/game_menu.rs
6592 views
1
//! This example will display a simple menu using Bevy UI where you can start a new game,
2
//! change some settings or quit. There is no actual game, it will just display the current
3
//! settings for 5 seconds before going back to the menu.
4
5
use bevy::prelude::*;
6
const TEXT_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
7
8
// Enum that will be used as a global state for the game
9
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
10
enum GameState {
11
#[default]
12
Splash,
13
Menu,
14
Game,
15
}
16
17
// One of the two settings that can be set through the menu. It will be a resource in the app
18
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
19
enum DisplayQuality {
20
Low,
21
Medium,
22
High,
23
}
24
25
// One of the two settings that can be set through the menu. It will be a resource in the app
26
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
27
struct Volume(u32);
28
29
fn main() {
30
App::new()
31
.add_plugins(DefaultPlugins)
32
// Insert as resource the initial value for the settings resources
33
.insert_resource(DisplayQuality::Medium)
34
.insert_resource(Volume(7))
35
// Declare the game state, whose starting value is determined by the `Default` trait
36
.init_state::<GameState>()
37
.add_systems(Startup, setup)
38
// Adds the plugins for each state
39
.add_plugins((splash::splash_plugin, menu::menu_plugin, game::game_plugin))
40
.run();
41
}
42
43
fn setup(mut commands: Commands) {
44
commands.spawn(Camera2d);
45
}
46
47
mod splash {
48
use bevy::prelude::*;
49
50
use super::GameState;
51
52
// This plugin will display a splash screen with Bevy logo for 1 second before switching to the menu
53
pub fn splash_plugin(app: &mut App) {
54
// As this plugin is managing the splash screen, it will focus on the state `GameState::Splash`
55
app
56
// When entering the state, spawn everything needed for this screen
57
.add_systems(OnEnter(GameState::Splash), splash_setup)
58
// While in this state, run the `countdown` system
59
.add_systems(Update, countdown.run_if(in_state(GameState::Splash)));
60
}
61
62
// Tag component used to tag entities added on the splash screen
63
#[derive(Component)]
64
struct OnSplashScreen;
65
66
// Newtype to use a `Timer` for this screen as a resource
67
#[derive(Resource, Deref, DerefMut)]
68
struct SplashTimer(Timer);
69
70
fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
71
let icon = asset_server.load("branding/icon.png");
72
// Display the logo
73
commands.spawn((
74
// This entity will be despawned when exiting the state
75
DespawnOnExit(GameState::Splash),
76
Node {
77
align_items: AlignItems::Center,
78
justify_content: JustifyContent::Center,
79
width: percent(100),
80
height: percent(100),
81
..default()
82
},
83
OnSplashScreen,
84
children![(
85
ImageNode::new(icon),
86
Node {
87
// This will set the logo to be 200px wide, and auto adjust its height
88
width: px(200),
89
..default()
90
},
91
)],
92
));
93
// Insert the timer as a resource
94
commands.insert_resource(SplashTimer(Timer::from_seconds(1.0, TimerMode::Once)));
95
}
96
97
// Tick the timer, and change state when finished
98
fn countdown(
99
mut game_state: ResMut<NextState<GameState>>,
100
time: Res<Time>,
101
mut timer: ResMut<SplashTimer>,
102
) {
103
if timer.tick(time.delta()).is_finished() {
104
game_state.set(GameState::Menu);
105
}
106
}
107
}
108
109
mod game {
110
use bevy::{
111
color::palettes::basic::{BLUE, LIME},
112
prelude::*,
113
};
114
115
use super::{DisplayQuality, GameState, Volume, TEXT_COLOR};
116
117
// This plugin will contain the game. In this case, it's just be a screen that will
118
// display the current settings for 5 seconds before returning to the menu
119
pub fn game_plugin(app: &mut App) {
120
app.add_systems(OnEnter(GameState::Game), game_setup)
121
.add_systems(Update, game.run_if(in_state(GameState::Game)));
122
}
123
124
// Tag component used to tag entities added on the game screen
125
#[derive(Component)]
126
struct OnGameScreen;
127
128
#[derive(Resource, Deref, DerefMut)]
129
struct GameTimer(Timer);
130
131
fn game_setup(
132
mut commands: Commands,
133
display_quality: Res<DisplayQuality>,
134
volume: Res<Volume>,
135
) {
136
commands.spawn((
137
DespawnOnExit(GameState::Game),
138
Node {
139
width: percent(100),
140
height: percent(100),
141
// center children
142
align_items: AlignItems::Center,
143
justify_content: JustifyContent::Center,
144
..default()
145
},
146
OnGameScreen,
147
children![(
148
Node {
149
// This will display its children in a column, from top to bottom
150
flex_direction: FlexDirection::Column,
151
// `align_items` will align children on the cross axis. Here the main axis is
152
// vertical (column), so the cross axis is horizontal. This will center the
153
// children
154
align_items: AlignItems::Center,
155
..default()
156
},
157
BackgroundColor(Color::BLACK),
158
children![
159
(
160
Text::new("Will be back to the menu shortly..."),
161
TextFont {
162
font_size: 67.0,
163
..default()
164
},
165
TextColor(TEXT_COLOR),
166
Node {
167
margin: UiRect::all(px(50)),
168
..default()
169
},
170
),
171
(
172
Text::default(),
173
Node {
174
margin: UiRect::all(px(50)),
175
..default()
176
},
177
children![
178
(
179
TextSpan(format!("quality: {:?}", *display_quality)),
180
TextFont {
181
font_size: 50.0,
182
..default()
183
},
184
TextColor(BLUE.into()),
185
),
186
(
187
TextSpan::new(" - "),
188
TextFont {
189
font_size: 50.0,
190
..default()
191
},
192
TextColor(TEXT_COLOR),
193
),
194
(
195
TextSpan(format!("volume: {:?}", *volume)),
196
TextFont {
197
font_size: 50.0,
198
..default()
199
},
200
TextColor(LIME.into()),
201
),
202
]
203
),
204
]
205
)],
206
));
207
// Spawn a 5 seconds timer to trigger going back to the menu
208
commands.insert_resource(GameTimer(Timer::from_seconds(5.0, TimerMode::Once)));
209
}
210
211
// Tick the timer, and change state when finished
212
fn game(
213
time: Res<Time>,
214
mut game_state: ResMut<NextState<GameState>>,
215
mut timer: ResMut<GameTimer>,
216
) {
217
if timer.tick(time.delta()).is_finished() {
218
game_state.set(GameState::Menu);
219
}
220
}
221
}
222
223
mod menu {
224
use bevy::{
225
app::AppExit,
226
color::palettes::css::CRIMSON,
227
ecs::spawn::{SpawnIter, SpawnWith},
228
prelude::*,
229
};
230
231
use super::{DisplayQuality, GameState, Volume, TEXT_COLOR};
232
233
// This plugin manages the menu, with 5 different screens:
234
// - a main menu with "New Game", "Settings", "Quit"
235
// - a settings menu with two submenus and a back button
236
// - two settings screen with a setting that can be set and a back button
237
pub fn menu_plugin(app: &mut App) {
238
app
239
// At start, the menu is not enabled. This will be changed in `menu_setup` when
240
// entering the `GameState::Menu` state.
241
// Current screen in the menu is handled by an independent state from `GameState`
242
.init_state::<MenuState>()
243
.add_systems(OnEnter(GameState::Menu), menu_setup)
244
// Systems to handle the main menu screen
245
.add_systems(OnEnter(MenuState::Main), main_menu_setup)
246
// Systems to handle the settings menu screen
247
.add_systems(OnEnter(MenuState::Settings), settings_menu_setup)
248
// Systems to handle the display settings screen
249
.add_systems(
250
OnEnter(MenuState::SettingsDisplay),
251
display_settings_menu_setup,
252
)
253
.add_systems(
254
Update,
255
(setting_button::<DisplayQuality>.run_if(in_state(MenuState::SettingsDisplay)),),
256
)
257
// Systems to handle the sound settings screen
258
.add_systems(OnEnter(MenuState::SettingsSound), sound_settings_menu_setup)
259
.add_systems(
260
Update,
261
setting_button::<Volume>.run_if(in_state(MenuState::SettingsSound)),
262
)
263
// Common systems to all screens that handles buttons behavior
264
.add_systems(
265
Update,
266
(menu_action, button_system).run_if(in_state(GameState::Menu)),
267
);
268
}
269
270
// State used for the current menu screen
271
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
272
enum MenuState {
273
Main,
274
Settings,
275
SettingsDisplay,
276
SettingsSound,
277
#[default]
278
Disabled,
279
}
280
281
// Tag component used to tag entities added on the main menu screen
282
#[derive(Component)]
283
struct OnMainMenuScreen;
284
285
// Tag component used to tag entities added on the settings menu screen
286
#[derive(Component)]
287
struct OnSettingsMenuScreen;
288
289
// Tag component used to tag entities added on the display settings menu screen
290
#[derive(Component)]
291
struct OnDisplaySettingsMenuScreen;
292
293
// Tag component used to tag entities added on the sound settings menu screen
294
#[derive(Component)]
295
struct OnSoundSettingsMenuScreen;
296
297
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
298
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
299
const HOVERED_PRESSED_BUTTON: Color = Color::srgb(0.25, 0.65, 0.25);
300
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
301
302
// Tag component used to mark which setting is currently selected
303
#[derive(Component)]
304
struct SelectedOption;
305
306
// All actions that can be triggered from a button click
307
#[derive(Component)]
308
enum MenuButtonAction {
309
Play,
310
Settings,
311
SettingsDisplay,
312
SettingsSound,
313
BackToMainMenu,
314
BackToSettings,
315
Quit,
316
}
317
318
// This system handles changing all buttons color based on mouse interaction
319
fn button_system(
320
mut interaction_query: Query<
321
(&Interaction, &mut BackgroundColor, Option<&SelectedOption>),
322
(Changed<Interaction>, With<Button>),
323
>,
324
) {
325
for (interaction, mut background_color, selected) in &mut interaction_query {
326
*background_color = match (*interaction, selected) {
327
(Interaction::Pressed, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON.into(),
328
(Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON.into(),
329
(Interaction::Hovered, None) => HOVERED_BUTTON.into(),
330
(Interaction::None, None) => NORMAL_BUTTON.into(),
331
}
332
}
333
}
334
335
// This system updates the settings when a new value for a setting is selected, and marks
336
// the button as the one currently selected
337
fn setting_button<T: Resource + Component + PartialEq + Copy>(
338
interaction_query: Query<(&Interaction, &T, Entity), (Changed<Interaction>, With<Button>)>,
339
selected_query: Single<(Entity, &mut BackgroundColor), With<SelectedOption>>,
340
mut commands: Commands,
341
mut setting: ResMut<T>,
342
) {
343
let (previous_button, mut previous_button_color) = selected_query.into_inner();
344
for (interaction, button_setting, entity) in &interaction_query {
345
if *interaction == Interaction::Pressed && *setting != *button_setting {
346
*previous_button_color = NORMAL_BUTTON.into();
347
commands.entity(previous_button).remove::<SelectedOption>();
348
commands.entity(entity).insert(SelectedOption);
349
*setting = *button_setting;
350
}
351
}
352
}
353
354
fn menu_setup(mut menu_state: ResMut<NextState<MenuState>>) {
355
menu_state.set(MenuState::Main);
356
}
357
358
fn main_menu_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
359
// Common style for all buttons on the screen
360
let button_node = Node {
361
width: px(300),
362
height: px(65),
363
margin: UiRect::all(px(20)),
364
justify_content: JustifyContent::Center,
365
align_items: AlignItems::Center,
366
..default()
367
};
368
let button_icon_node = Node {
369
width: px(30),
370
// This takes the icons out of the flexbox flow, to be positioned exactly
371
position_type: PositionType::Absolute,
372
// The icon will be close to the left border of the button
373
left: px(10),
374
..default()
375
};
376
let button_text_font = TextFont {
377
font_size: 33.0,
378
..default()
379
};
380
381
let right_icon = asset_server.load("textures/Game Icons/right.png");
382
let wrench_icon = asset_server.load("textures/Game Icons/wrench.png");
383
let exit_icon = asset_server.load("textures/Game Icons/exitRight.png");
384
385
commands.spawn((
386
DespawnOnExit(MenuState::Main),
387
Node {
388
width: percent(100),
389
height: percent(100),
390
align_items: AlignItems::Center,
391
justify_content: JustifyContent::Center,
392
..default()
393
},
394
OnMainMenuScreen,
395
children![(
396
Node {
397
flex_direction: FlexDirection::Column,
398
align_items: AlignItems::Center,
399
..default()
400
},
401
BackgroundColor(CRIMSON.into()),
402
children![
403
// Display the game name
404
(
405
Text::new("Bevy Game Menu UI"),
406
TextFont {
407
font_size: 67.0,
408
..default()
409
},
410
TextColor(TEXT_COLOR),
411
Node {
412
margin: UiRect::all(px(50)),
413
..default()
414
},
415
),
416
// Display three buttons for each action available from the main menu:
417
// - new game
418
// - settings
419
// - quit
420
(
421
Button,
422
button_node.clone(),
423
BackgroundColor(NORMAL_BUTTON),
424
MenuButtonAction::Play,
425
children![
426
(ImageNode::new(right_icon), button_icon_node.clone()),
427
(
428
Text::new("New Game"),
429
button_text_font.clone(),
430
TextColor(TEXT_COLOR),
431
),
432
]
433
),
434
(
435
Button,
436
button_node.clone(),
437
BackgroundColor(NORMAL_BUTTON),
438
MenuButtonAction::Settings,
439
children![
440
(ImageNode::new(wrench_icon), button_icon_node.clone()),
441
(
442
Text::new("Settings"),
443
button_text_font.clone(),
444
TextColor(TEXT_COLOR),
445
),
446
]
447
),
448
(
449
Button,
450
button_node,
451
BackgroundColor(NORMAL_BUTTON),
452
MenuButtonAction::Quit,
453
children![
454
(ImageNode::new(exit_icon), button_icon_node),
455
(Text::new("Quit"), button_text_font, TextColor(TEXT_COLOR),),
456
]
457
),
458
]
459
)],
460
));
461
}
462
463
fn settings_menu_setup(mut commands: Commands) {
464
let button_node = Node {
465
width: px(200),
466
height: px(65),
467
margin: UiRect::all(px(20)),
468
justify_content: JustifyContent::Center,
469
align_items: AlignItems::Center,
470
..default()
471
};
472
473
let button_text_style = (
474
TextFont {
475
font_size: 33.0,
476
..default()
477
},
478
TextColor(TEXT_COLOR),
479
);
480
481
commands.spawn((
482
DespawnOnExit(MenuState::Settings),
483
Node {
484
width: percent(100),
485
height: percent(100),
486
align_items: AlignItems::Center,
487
justify_content: JustifyContent::Center,
488
..default()
489
},
490
OnSettingsMenuScreen,
491
children![(
492
Node {
493
flex_direction: FlexDirection::Column,
494
align_items: AlignItems::Center,
495
..default()
496
},
497
BackgroundColor(CRIMSON.into()),
498
Children::spawn(SpawnIter(
499
[
500
(MenuButtonAction::SettingsDisplay, "Display"),
501
(MenuButtonAction::SettingsSound, "Sound"),
502
(MenuButtonAction::BackToMainMenu, "Back"),
503
]
504
.into_iter()
505
.map(move |(action, text)| {
506
(
507
Button,
508
button_node.clone(),
509
BackgroundColor(NORMAL_BUTTON),
510
action,
511
children![(Text::new(text), button_text_style.clone())],
512
)
513
})
514
))
515
)],
516
));
517
}
518
519
fn display_settings_menu_setup(mut commands: Commands, display_quality: Res<DisplayQuality>) {
520
fn button_node() -> Node {
521
Node {
522
width: px(200),
523
height: px(65),
524
margin: UiRect::all(px(20)),
525
justify_content: JustifyContent::Center,
526
align_items: AlignItems::Center,
527
..default()
528
}
529
}
530
fn button_text_style() -> impl Bundle {
531
(
532
TextFont {
533
font_size: 33.0,
534
..default()
535
},
536
TextColor(TEXT_COLOR),
537
)
538
}
539
540
let display_quality = *display_quality;
541
commands.spawn((
542
DespawnOnExit(MenuState::SettingsDisplay),
543
Node {
544
width: percent(100),
545
height: percent(100),
546
align_items: AlignItems::Center,
547
justify_content: JustifyContent::Center,
548
..default()
549
},
550
OnDisplaySettingsMenuScreen,
551
children![(
552
Node {
553
flex_direction: FlexDirection::Column,
554
align_items: AlignItems::Center,
555
..default()
556
},
557
BackgroundColor(CRIMSON.into()),
558
children![
559
// Create a new `Node`, this time not setting its `flex_direction`. It will
560
// use the default value, `FlexDirection::Row`, from left to right.
561
(
562
Node {
563
align_items: AlignItems::Center,
564
..default()
565
},
566
BackgroundColor(CRIMSON.into()),
567
Children::spawn((
568
// Display a label for the current setting
569
Spawn((Text::new("Display Quality"), button_text_style())),
570
SpawnWith(move |parent: &mut ChildSpawner| {
571
for quality_setting in [
572
DisplayQuality::Low,
573
DisplayQuality::Medium,
574
DisplayQuality::High,
575
] {
576
let mut entity = parent.spawn((
577
Button,
578
Node {
579
width: px(150),
580
height: px(65),
581
..button_node()
582
},
583
BackgroundColor(NORMAL_BUTTON),
584
quality_setting,
585
children![(
586
Text::new(format!("{quality_setting:?}")),
587
button_text_style(),
588
)],
589
));
590
if display_quality == quality_setting {
591
entity.insert(SelectedOption);
592
}
593
}
594
})
595
))
596
),
597
// Display the back button to return to the settings screen
598
(
599
Button,
600
button_node(),
601
BackgroundColor(NORMAL_BUTTON),
602
MenuButtonAction::BackToSettings,
603
children![(Text::new("Back"), button_text_style())]
604
)
605
]
606
)],
607
));
608
}
609
610
fn sound_settings_menu_setup(mut commands: Commands, volume: Res<Volume>) {
611
let button_node = Node {
612
width: px(200),
613
height: px(65),
614
margin: UiRect::all(px(20)),
615
justify_content: JustifyContent::Center,
616
align_items: AlignItems::Center,
617
..default()
618
};
619
let button_text_style = (
620
TextFont {
621
font_size: 33.0,
622
..default()
623
},
624
TextColor(TEXT_COLOR),
625
);
626
627
let volume = *volume;
628
let button_node_clone = button_node.clone();
629
commands.spawn((
630
DespawnOnExit(MenuState::SettingsSound),
631
Node {
632
width: percent(100),
633
height: percent(100),
634
align_items: AlignItems::Center,
635
justify_content: JustifyContent::Center,
636
..default()
637
},
638
OnSoundSettingsMenuScreen,
639
children![(
640
Node {
641
flex_direction: FlexDirection::Column,
642
align_items: AlignItems::Center,
643
..default()
644
},
645
BackgroundColor(CRIMSON.into()),
646
children![
647
(
648
Node {
649
align_items: AlignItems::Center,
650
..default()
651
},
652
BackgroundColor(CRIMSON.into()),
653
Children::spawn((
654
Spawn((Text::new("Volume"), button_text_style.clone())),
655
SpawnWith(move |parent: &mut ChildSpawner| {
656
for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
657
let mut entity = parent.spawn((
658
Button,
659
Node {
660
width: px(30),
661
height: px(65),
662
..button_node_clone.clone()
663
},
664
BackgroundColor(NORMAL_BUTTON),
665
Volume(volume_setting),
666
));
667
if volume == Volume(volume_setting) {
668
entity.insert(SelectedOption);
669
}
670
}
671
})
672
))
673
),
674
(
675
Button,
676
button_node,
677
BackgroundColor(NORMAL_BUTTON),
678
MenuButtonAction::BackToSettings,
679
children![(Text::new("Back"), button_text_style)]
680
)
681
]
682
)],
683
));
684
}
685
686
fn menu_action(
687
interaction_query: Query<
688
(&Interaction, &MenuButtonAction),
689
(Changed<Interaction>, With<Button>),
690
>,
691
mut app_exit_events: EventWriter<AppExit>,
692
mut menu_state: ResMut<NextState<MenuState>>,
693
mut game_state: ResMut<NextState<GameState>>,
694
) {
695
for (interaction, menu_button_action) in &interaction_query {
696
if *interaction == Interaction::Pressed {
697
match menu_button_action {
698
MenuButtonAction::Quit => {
699
app_exit_events.write(AppExit::Success);
700
}
701
MenuButtonAction::Play => {
702
game_state.set(GameState::Game);
703
menu_state.set(MenuState::Disabled);
704
}
705
MenuButtonAction::Settings => menu_state.set(MenuState::Settings),
706
MenuButtonAction::SettingsDisplay => {
707
menu_state.set(MenuState::SettingsDisplay);
708
}
709
MenuButtonAction::SettingsSound => {
710
menu_state.set(MenuState::SettingsSound);
711
}
712
MenuButtonAction::BackToMainMenu => menu_state.set(MenuState::Main),
713
MenuButtonAction::BackToSettings => {
714
menu_state.set(MenuState::Settings);
715
}
716
}
717
}
718
}
719
}
720
}
721
722