Skip to main content

state_scoped/
state_scoped.rs

1//! Shows how to spawn entities that are automatically despawned either when
2//! entering or exiting specific game states.
3//!
4//! This pattern is useful for managing menus, levels, or other state-specific
5//! content that should only exist during certain states.
6//!
7//! If the entity was already despawned then no error will be logged. This means
8//! that you don't have to worry about duplicate [`DespawnOnExit`] and
9//! [`DespawnOnEnter`] components deep in your hierarchy.
10
11use bevy::prelude::*;
12
13fn main() {
14    App::new()
15        .add_plugins(DefaultPlugins)
16        .init_state::<GameState>()
17        .add_systems(Startup, setup_camera)
18        .add_systems(OnEnter(GameState::A), on_a_enter)
19        .add_systems(OnEnter(GameState::B), on_b_enter)
20        .add_systems(OnExit(GameState::A), on_a_exit)
21        .add_systems(OnExit(GameState::B), on_b_exit)
22        .add_systems(OnEnter(GameState::C(1)), on_c_1_enter)
23        .add_systems(OnExit(GameState::C(1)), on_c_1_exit)
24        .add_systems(Update, toggle)
25        .insert_resource(TickTock(Timer::from_seconds(1.0, TimerMode::Repeating)))
26        .run();
27}
28
29#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
30enum GameState {
31    #[default]
32    A,
33    B,
34    C(u8),
35}
36
37#[derive(Resource)]
38struct TickTock(Timer);
39
40fn on_a_enter(mut commands: Commands) {
41    info!("on_a_enter");
42    commands.spawn((
43        DespawnOnExit(GameState::A),
44        Text::new("Game is in state 'A'"),
45        TextFont {
46            font_size: FontSize::Px(33.0),
47            ..default()
48        },
49        TextColor(Color::srgb(0.5, 0.5, 1.0)),
50        Node {
51            position_type: PositionType::Absolute,
52            top: px(0),
53            left: px(0),
54            ..default()
55        },
56        (children![DespawnOnExit(GameState::A)]),
57    ));
58}
59
60fn on_a_exit(mut commands: Commands) {
61    info!("on_a_exit");
62    commands.spawn((
63        DespawnOnEnter(GameState::A),
64        Text::new("Game state 'A' will be back in 1 second"),
65        TextFont {
66            font_size: FontSize::Px(33.0),
67            ..default()
68        },
69        TextColor(Color::srgb(0.5, 0.5, 1.0)),
70        Node {
71            position_type: PositionType::Absolute,
72            top: px(0),
73            left: px(500),
74            ..default()
75        },
76        // You can apply this even when the parent has a state scoped component.
77        // It is unnecessary but in complex hierarchies it saves you from having to
78        // mentally track which components are found at the top level.
79        (children![DespawnOnEnter(GameState::A)]),
80    ));
81}
82
83fn on_b_enter(mut commands: Commands) {
84    info!("on_b_enter");
85    commands.spawn((
86        DespawnOnExit(GameState::B),
87        Text::new("Game is in state 'B'"),
88        TextFont {
89            font_size: FontSize::Px(33.0),
90            ..default()
91        },
92        TextColor(Color::srgb(0.5, 0.5, 1.0)),
93        Node {
94            position_type: PositionType::Absolute,
95            top: px(50),
96            left: px(0),
97            ..default()
98        },
99        (children![DespawnOnExit(GameState::B)]),
100    ));
101}
102
103fn on_b_exit(mut commands: Commands) {
104    info!("on_b_exit");
105    commands.spawn((
106        DespawnOnEnter(GameState::B),
107        Text::new("Game state 'B' will be back in 1 second"),
108        TextFont {
109            font_size: FontSize::Px(33.0),
110            ..default()
111        },
112        TextColor(Color::srgb(0.5, 0.5, 1.0)),
113        Node {
114            position_type: PositionType::Absolute,
115            top: px(50),
116            left: px(500),
117            ..default()
118        },
119        (children![DespawnOnEnter(GameState::B)]),
120    ));
121}
122
123fn on_c_1_enter(mut commands: Commands) {
124    info!("on_c_1_enter");
125    commands.spawn((
126        DespawnWhen::new(|transition| matches!(transition.exited, Some(GameState::C(_)))),
127        Text::new("Game is in state 'C(1)'"),
128        TextFont {
129            font_size: FontSize::Px(33.0),
130            ..default()
131        },
132        TextColor(Color::srgb(0.5, 0.5, 1.0)),
133        Node {
134            position_type: PositionType::Absolute,
135            top: px(100),
136            left: px(0),
137            ..default()
138        },
139        (children![DespawnWhen::new(|transition| matches!(
140            transition.exited,
141            Some(GameState::C(_))
142        ))]),
143    ));
144}
145
146fn on_c_1_exit(mut commands: Commands) {
147    info!("on_c_1_exit");
148    commands.spawn((
149        DespawnWhen::new(|transition| matches!(transition.entered, Some(GameState::C(1)))),
150        Text::new("Game state 'C(1)' will be back in 1 second"),
151        TextFont {
152            font_size: FontSize::Px(33.0),
153            ..default()
154        },
155        TextColor(Color::srgb(0.5, 0.5, 1.0)),
156        Node {
157            position_type: PositionType::Absolute,
158            top: px(100),
159            left: px(500),
160            ..default()
161        },
162        (children![DespawnWhen::new(|transition| matches!(
163            transition.entered,
164            Some(GameState::C(_))
165        ))]),
166    ));
167}
168
169fn setup_camera(mut commands: Commands) {
170    commands.spawn(Camera3d::default());
171}
172
173fn toggle(
174    time: Res<Time>,
175    mut timer: ResMut<TickTock>,
176    state: Res<State<GameState>>,
177    mut next_state: ResMut<NextState<GameState>>,
178) {
179    if !timer.0.tick(time.delta()).is_finished() {
180        return;
181    }
182    *next_state = match state.get() {
183        GameState::A => NextState::Pending(GameState::B),
184        GameState::B => NextState::Pending(GameState::C(1)),
185        GameState::C(_) => NextState::Pending(GameState::A),
186    }
187}