docs/bevy/examples/state/sub_states.rs 7.7 K · 239 lines · raw · history

1 //! This example illustrates the use of [`SubStates`] for more complex state handling patterns.
2 //!
3 //! [`SubStates`] are [`States`] that only exist while the App is in another [`State`]. They can
4 //! be used to create more complex patterns while relying on simple enums, or to de-couple certain
5 //! elements of complex state objects.
6 //!
7 //! In this case, we're transitioning from a `Menu` state to an `InGame` state, at which point we create
8 //! a substate called `IsPaused` to track whether the game is paused or not.
9
10 use bevy::{
11 dev_tools::states::*,
12 picking::hover::Hovered,
13 prelude::*,
14 ui_widgets::{Activate, ActivateOnPress, Button},
15 };
16
17 use ui::*;
18
19 #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
20 enum AppState {
21 #[default]
22 Menu,
23 InGame,
24 }
25
26 // In this case, instead of deriving `States`, we derive `SubStates`
27 #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, SubStates)]
28 // And we need to add an attribute to let us know what the source state is
29 // and what value it needs to have. This will ensure that unless we're
30 // in [`AppState::InGame`], the [`IsPaused`] state resource
31 // will not exist.
32 #[source(AppState = AppState::InGame)]
33 enum IsPaused {
34 #[default]
35 Running,
36 Paused,
37 }
38
39 fn main() {
40 App::new()
41 .add_plugins(DefaultPlugins)
42 .init_state::<AppState>()
43 .add_sub_state::<IsPaused>() // We set the substate up here.
44 // Most of these remain the same
45 .add_systems(Startup, setup)
46 .add_systems(OnEnter(AppState::Menu), setup_menu)
47 .add_observer(on_activate_start_game.run_if(in_state(AppState::Menu)))
48 .add_systems(Update, hover_style)
49 .add_systems(OnExit(AppState::Menu), cleanup_menu)
50 .add_systems(OnEnter(AppState::InGame), setup_game)
51 .add_systems(OnEnter(IsPaused::Paused), setup_paused_screen)
52 .add_systems(
53 Update,
54 (
55 // Instead of relying on [`AppState::InGame`] here, we're relying on
56 // [`IsPaused::Running`], since we don't want movement or color changes
57 // if we're paused
58 (movement, change_color).run_if(in_state(IsPaused::Running)),
59 // The pause toggle, on the other hand, needs to work whether we're
60 // paused or not, so it uses [`AppState::InGame`] instead.
61 toggle_pause.run_if(in_state(AppState::InGame)),
62 ),
63 )
64 .add_systems(Update, log_transitions::<AppState>)
65 .run();
66 }
67
68 fn on_activate_start_game(_: On<Activate>, mut next_state: ResMut<NextState<AppState>>) {
69 next_state.set(AppState::InGame);
70 }
71
72 fn hover_style(
73 mut button_query: Query<(&Hovered, &mut BackgroundColor), (Changed<Hovered>, With<Button>)>,
74 ) {
75 for (hovered, mut color) in &mut button_query {
76 if hovered.get() {
77 *color = HOVERED_BUTTON.into();
78 } else {
79 *color = NORMAL_BUTTON.into();
80 }
81 }
82 }
83
84 fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
85 commands.entity(menu_data.button_entity).despawn();
86 }
87
88 const SPEED: f32 = 100.0;
89 fn movement(
90 time: Res<Time>,
91 input: Res<ButtonInput<KeyCode>>,
92 mut query: Query<&mut Transform, With<Sprite>>,
93 ) {
94 for mut transform in &mut query {
95 let mut direction = Vec3::ZERO;
96 if input.pressed(KeyCode::ArrowLeft) {
97 direction.x -= 1.0;
98 }
99 if input.pressed(KeyCode::ArrowRight) {
100 direction.x += 1.0;
101 }
102 if input.pressed(KeyCode::ArrowUp) {
103 direction.y += 1.0;
104 }
105 if input.pressed(KeyCode::ArrowDown) {
106 direction.y -= 1.0;
107 }
108
109 if direction != Vec3::ZERO {
110 transform.translation += direction.normalize() * SPEED * time.delta_secs();
111 }
112 }
113 }
114
115 fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
116 for mut sprite in &mut query {
117 let new_color = LinearRgba {
118 blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
119 ..LinearRgba::from(sprite.color)
120 };
121
122 sprite.color = new_color.into();
123 }
124 }
125
126 fn toggle_pause(
127 input: Res<ButtonInput<KeyCode>>,
128 current_state: Res<State<IsPaused>>,
129 mut next_state: ResMut<NextState<IsPaused>>,
130 ) {
131 if input.just_pressed(KeyCode::Space) {
132 next_state.set(match current_state.get() {
133 IsPaused::Running => IsPaused::Paused,
134 IsPaused::Paused => IsPaused::Running,
135 });
136 }
137 }
138
139 mod ui {
140 use crate::*;
141
142 #[derive(Resource)]
143 pub struct MenuData {
144 pub button_entity: Entity,
145 }
146
147 pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
148 pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
149
150 pub fn setup(mut commands: Commands) {
151 commands.spawn(Camera2d);
152 }
153
154 pub fn setup_menu(mut commands: Commands) {
155 let button_entity = commands
156 .spawn((
157 Node {
158 // center button
159 width: percent(100),
160 height: percent(100),
161 justify_content: JustifyContent::Center,
162 align_items: AlignItems::Center,
163 ..default()
164 },
165 children![(
166 Button,
167 ActivateOnPress,
168 Hovered::default(),
169 Node {
170 width: px(150),
171 height: px(65),
172 // horizontally center child text
173 justify_content: JustifyContent::Center,
174 // vertically center child text
175 align_items: AlignItems::Center,
176 ..default()
177 },
178 BackgroundColor(NORMAL_BUTTON),
179 children![(
180 Text::new("Play"),
181 TextFont {
182 font_size: FontSize::Px(33.0),
183 ..default()
184 },
185 TextColor(Color::srgb(0.9, 0.9, 0.9)),
186 )]
187 )],
188 ))
189 .id();
190 commands.insert_resource(MenuData { button_entity });
191 }
192
193 pub fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
194 commands.spawn(Sprite::from_image(asset_server.load("branding/icon.png")));
195
196 commands.spawn_scene(bsn! {
197 Node {
198 margin: px(10),
199 }
200 Text("Move with arrow keys.\nPress SPACEBAR to pause.")
201 });
202 }
203
204 pub fn setup_paused_screen(mut commands: Commands) {
205 commands.spawn((
206 DespawnOnExit(IsPaused::Paused),
207 Node {
208 // center button
209 width: percent(100),
210 height: percent(100),
211 justify_content: JustifyContent::Center,
212 align_items: AlignItems::Center,
213 flex_direction: FlexDirection::Column,
214 row_gap: px(10),
215 ..default()
216 },
217 children![(
218 Node {
219 width: px(400),
220 height: px(400),
221 // horizontally center child text
222 justify_content: JustifyContent::Center,
223 // vertically center child text
224 align_items: AlignItems::Center,
225 ..default()
226 },
227 BackgroundColor(NORMAL_BUTTON),
228 children![(
229 Text::new("Paused"),
230 TextFont {
231 font_size: FontSize::Px(33.0),
232 ..default()
233 },
234 TextColor(Color::srgb(0.9, 0.9, 0.9)),
235 )]
236 )],
237 ));
238 }
239 }