Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/state/custom_transitions.rs
6595 views
1
//! This example illustrates how to register custom state transition behavior.
2
//!
3
//! In this case we are trying to add `OnReenter` and `OnReexit`
4
//! which will work much like `OnEnter` and `OnExit`,
5
//! but additionally trigger if the state changed into itself.
6
//!
7
//! While identity transitions exist internally in [`StateTransitionEvent`]s,
8
//! the default schedules intentionally ignore them, as this behavior is not commonly needed or expected.
9
//!
10
//! While this example displays identity transitions for a single state,
11
//! identity transitions are propagated through the entire state graph,
12
//! meaning any change to parent state will be propagated to [`ComputedStates`] and [`SubStates`].
13
14
use std::marker::PhantomData;
15
16
use bevy::{dev_tools::states::*, ecs::schedule::ScheduleLabel, prelude::*};
17
18
use custom_transitions::*;
19
20
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
21
enum AppState {
22
#[default]
23
Menu,
24
InGame,
25
}
26
27
fn main() {
28
App::new()
29
// We insert the custom transitions plugin for `AppState`.
30
.add_plugins((
31
DefaultPlugins,
32
IdentityTransitionsPlugin::<AppState>::default(),
33
))
34
.init_state::<AppState>()
35
.add_systems(Startup, setup)
36
.add_systems(OnEnter(AppState::Menu), setup_menu)
37
.add_systems(Update, menu.run_if(in_state(AppState::Menu)))
38
.add_systems(OnExit(AppState::Menu), cleanup_menu)
39
// We will restart the game progress every time we re-enter into it.
40
.add_systems(OnReenter(AppState::InGame), setup_game)
41
.add_systems(OnReexit(AppState::InGame), teardown_game)
42
// Doing it this way allows us to restart the game without any additional in-between states.
43
.add_systems(
44
Update,
45
((movement, change_color, trigger_game_restart).run_if(in_state(AppState::InGame)),),
46
)
47
.add_systems(Update, log_transitions::<AppState>)
48
.run();
49
}
50
51
/// This module provides the custom `OnReenter` and `OnReexit` transitions for easy installation.
52
mod custom_transitions {
53
use crate::*;
54
55
/// The plugin registers the transitions for one specific state.
56
/// If you use this for multiple states consider:
57
/// - installing the plugin multiple times,
58
/// - create an [`App`] extension method that inserts
59
/// those transitions during state installation.
60
#[derive(Default)]
61
pub struct IdentityTransitionsPlugin<S: States>(PhantomData<S>);
62
63
impl<S: States> Plugin for IdentityTransitionsPlugin<S> {
64
fn build(&self, app: &mut App) {
65
app.add_systems(
66
StateTransition,
67
// The internals can generate at most one transition event of specific type per frame.
68
// We take the latest one and clear the queue.
69
last_transition::<S>
70
// We insert the optional event into our schedule runner.
71
.pipe(run_reenter::<S>)
72
// State transitions are handled in three ordered steps, exposed as system sets.
73
// We can add our systems to them, which will run the corresponding schedules when they're evaluated.
74
// These are:
75
// - [`ExitSchedules`] - Ran from leaf-states to root-states,
76
// - [`TransitionSchedules`] - Ran in arbitrary order,
77
// - [`EnterSchedules`] - Ran from root-states to leaf-states.
78
.in_set(EnterSchedules::<S>::default()),
79
)
80
.add_systems(
81
StateTransition,
82
last_transition::<S>
83
.pipe(run_reexit::<S>)
84
.in_set(ExitSchedules::<S>::default()),
85
);
86
}
87
}
88
89
/// Custom schedule that will behave like [`OnEnter`], but run on identity transitions.
90
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
91
pub struct OnReenter<S: States>(pub S);
92
93
/// Schedule runner which checks conditions and if they're right
94
/// runs out custom schedule.
95
fn run_reenter<S: States>(transition: In<Option<StateTransitionEvent<S>>>, world: &mut World) {
96
// We return early if no transition event happened.
97
let Some(transition) = transition.0 else {
98
return;
99
};
100
101
// If we wanted to ignore identity transitions,
102
// we'd compare `exited` and `entered` here,
103
// and return if they were the same.
104
105
// We check if we actually entered a state.
106
// A [`None`] would indicate that the state was removed from the world.
107
// This only happens in the case of [`SubStates`] and [`ComputedStates`].
108
let Some(entered) = transition.entered else {
109
return;
110
};
111
112
// If all conditions are valid, we run our custom schedule.
113
let _ = world.try_run_schedule(OnReenter(entered));
114
115
// If you want to overwrite the default `OnEnter` behavior to act like re-enter,
116
// you can do so by running the `OnEnter` schedule here. Note that you don't want
117
// to run `OnEnter` when the default behavior does so.
118
// ```
119
// if transition.entered != transition.exited {
120
// return;
121
// }
122
// let _ = world.try_run_schedule(OnReenter(entered));
123
// ```
124
}
125
126
/// Custom schedule that will behave like [`OnExit`], but run on identity transitions.
127
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
128
pub struct OnReexit<S: States>(pub S);
129
130
fn run_reexit<S: States>(transition: In<Option<StateTransitionEvent<S>>>, world: &mut World) {
131
let Some(transition) = transition.0 else {
132
return;
133
};
134
let Some(exited) = transition.exited else {
135
return;
136
};
137
138
let _ = world.try_run_schedule(OnReexit(exited));
139
}
140
}
141
142
fn menu(
143
mut next_state: ResMut<NextState<AppState>>,
144
mut interaction_query: Query<
145
(&Interaction, &mut BackgroundColor),
146
(Changed<Interaction>, With<Button>),
147
>,
148
) {
149
for (interaction, mut color) in &mut interaction_query {
150
match *interaction {
151
Interaction::Pressed => {
152
*color = PRESSED_BUTTON.into();
153
next_state.set(AppState::InGame);
154
}
155
Interaction::Hovered => {
156
*color = HOVERED_BUTTON.into();
157
}
158
Interaction::None => {
159
*color = NORMAL_BUTTON.into();
160
}
161
}
162
}
163
}
164
165
fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
166
commands.entity(menu_data.button_entity).despawn();
167
}
168
169
const SPEED: f32 = 100.0;
170
fn movement(
171
time: Res<Time>,
172
input: Res<ButtonInput<KeyCode>>,
173
mut query: Query<&mut Transform, With<Sprite>>,
174
) {
175
for mut transform in &mut query {
176
let mut direction = Vec3::ZERO;
177
if input.pressed(KeyCode::ArrowLeft) {
178
direction.x -= 1.0;
179
}
180
if input.pressed(KeyCode::ArrowRight) {
181
direction.x += 1.0;
182
}
183
if input.pressed(KeyCode::ArrowUp) {
184
direction.y += 1.0;
185
}
186
if input.pressed(KeyCode::ArrowDown) {
187
direction.y -= 1.0;
188
}
189
190
if direction != Vec3::ZERO {
191
transform.translation += direction.normalize() * SPEED * time.delta_secs();
192
}
193
}
194
}
195
196
fn change_color(time: Res<Time>, mut query: Query<&mut Sprite>) {
197
for mut sprite in &mut query {
198
let new_color = LinearRgba {
199
blue: ops::sin(time.elapsed_secs() * 0.5) + 2.0,
200
..LinearRgba::from(sprite.color)
201
};
202
203
sprite.color = new_color.into();
204
}
205
}
206
207
// We can restart the game by pressing "R".
208
// This will trigger an [`AppState::InGame`] -> [`AppState::InGame`]
209
// transition, which will run our custom schedules.
210
fn trigger_game_restart(
211
input: Res<ButtonInput<KeyCode>>,
212
mut next_state: ResMut<NextState<AppState>>,
213
) {
214
if input.just_pressed(KeyCode::KeyR) {
215
// Although we are already in this state setting it again will generate an identity transition.
216
// While default schedules ignore those kinds of transitions, our custom schedules will react to them.
217
next_state.set(AppState::InGame);
218
}
219
}
220
221
fn setup(mut commands: Commands) {
222
commands.spawn(Camera2d);
223
}
224
225
fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
226
commands.spawn(Sprite::from_image(asset_server.load("branding/icon.png")));
227
info!("Setup game");
228
}
229
230
fn teardown_game(mut commands: Commands, player: Single<Entity, With<Sprite>>) {
231
commands.entity(*player).despawn();
232
info!("Teardown game");
233
}
234
235
#[derive(Resource)]
236
struct MenuData {
237
pub button_entity: Entity,
238
}
239
240
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
241
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
242
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
243
244
fn setup_menu(mut commands: Commands) {
245
let button_entity = commands
246
.spawn((
247
Node {
248
// center button
249
width: percent(100),
250
height: percent(100),
251
justify_content: JustifyContent::Center,
252
align_items: AlignItems::Center,
253
..default()
254
},
255
children![(
256
Button,
257
Node {
258
width: px(150),
259
height: px(65),
260
// horizontally center child text
261
justify_content: JustifyContent::Center,
262
// vertically center child text
263
align_items: AlignItems::Center,
264
..default()
265
},
266
BackgroundColor(NORMAL_BUTTON),
267
children![(
268
Text::new("Play"),
269
TextFont {
270
font_size: 33.0,
271
..default()
272
},
273
TextColor(Color::srgb(0.9, 0.9, 0.9)),
274
)]
275
)],
276
))
277
.id();
278
commands.insert_resource(MenuData { button_entity });
279
}
280
281