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