
# Objective - Alternative to and builds on top of #16284. - Fixes #15849. ## Solution - Rename component `StateScoped` to `DespawnOnExitState`. - Rename system `clear_state_scoped_entities` to `despawn_entities_on_exit_state`. - Add `DespawnOnEnterState` and `despawn_entities_on_enter_state` which is the `OnEnter` equivalent. > [!NOTE] > Compared to #16284, the main change is that I did the rename in such a way as to keep the terms `OnExit` and `OnEnter` together. In my own game, I was adding `VisibleOnEnterState` and `HiddenOnExitState` and when naming those, I kept the `OnExit` and `OnEnter` together. When I checked #16284 it stood out to me that the naming was a bit awkward. Putting the `State` in the middle and breaking up `OnEnter` and `OnExit` also breaks searching for those terms. ## Open questions 1. Should we split `enable_state_scoped_entities` into two functions, one for the `OnEnter` and one for the `OnExit`? I personally have zero need thus far for the `OnEnter` version, so I'd be interested in not having this enabled unless I ask for it. 2. If yes to 1., should we follow my lead in my `Visibility` state components (see below) and name these `app.enable_despawn_entities_on_enter_state()` and `app.enable_despawn_entities_on_exit_state()`, which IMO says what it does on the tin? ## Testing Ran all changed examples. ## Side note: `VisibleOnEnterState` and `HiddenOnExitState` For reference to anyone else and to help with the open questions, I'm including the code I wrote for controlling entity visibility when a state is entered/exited. <details> <summary>visibility.rs</summary> ```rust use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_state::{prelude::*, state::StateTransitionSteps}; use tracing::*; pub trait AppExtStates { fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self; fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self; } impl AppExtStates for App { fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self { self.main_mut() .enable_visible_entities_on_enter_state::<S>(); self } fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self { self.main_mut().enable_hidden_entities_on_exit_state::<S>(); self } } impl AppExtStates for SubApp { fn enable_visible_entities_on_enter_state<S: States>(&mut self) -> &mut Self { if !self .world() .contains_resource::<Events<StateTransitionEvent<S>>>() { let name = core::any::type_name::<S>(); warn!("Visible entities on enter state are enabled for state `{}`, but the state isn't installed in the app!", name); } // We work with [`StateTransition`] in set // [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`], // because [`OnExit`] only runs for one specific variant of the state. self.add_systems( StateTransition, update_to_visible_on_enter_state::<S>.in_set(StateTransitionSteps::ExitSchedules), ) } fn enable_hidden_entities_on_exit_state<S: States>(&mut self) -> &mut Self { if !self .world() .contains_resource::<Events<StateTransitionEvent<S>>>() { let name = core::any::type_name::<S>(); warn!("Hidden entities on exit state are enabled for state `{}`, but the state isn't installed in the app!", name); } // We work with [`StateTransition`] in set // [`StateTransitionSteps::ExitSchedules`] as opposed to [`OnExit`], // because [`OnExit`] only runs for one specific variant of the state. self.add_systems( StateTransition, update_to_hidden_on_exit_state::<S>.in_set(StateTransitionSteps::ExitSchedules), ) } } #[derive(Clone, Component, Debug, Reflect)] #[reflect(Component, Debug)] pub struct VisibleOnEnterState<S: States>(pub S); #[derive(Clone, Component, Debug, Reflect)] #[reflect(Component, Debug)] pub struct HiddenOnExitState<S: States>(pub S); /// Makes entities marked with [`VisibleOnEnterState<S>`] visible when the state /// `S` is entered. pub fn update_to_visible_on_enter_state<S: States>( mut transitions: EventReader<StateTransitionEvent<S>>, mut query: Query<(&VisibleOnEnterState<S>, &mut Visibility)>, ) { // We use the latest event, because state machine internals generate at most // 1 transition event (per type) each frame. No event means no change // happened and we skip iterating all entities. let Some(transition) = transitions.read().last() else { return; }; if transition.entered == transition.exited { return; } let Some(entered) = &transition.entered else { return; }; for (binding, mut visibility) in query.iter_mut() { if binding.0 == *entered { visibility.set_if_neq(Visibility::Visible); } } } /// Makes entities marked with [`HiddenOnExitState<S>`] invisible when the state /// `S` is exited. pub fn update_to_hidden_on_exit_state<S: States>( mut transitions: EventReader<StateTransitionEvent<S>>, mut query: Query<(&HiddenOnExitState<S>, &mut Visibility)>, ) { // We use the latest event, because state machine internals generate at most // 1 transition event (per type) each frame. No event means no change // happened and we skip iterating all entities. let Some(transition) = transitions.read().last() else { return; }; if transition.entered == transition.exited { return; } let Some(exited) = &transition.exited else { return; }; for (binding, mut visibility) in query.iter_mut() { if binding.0 == *exited { visibility.set_if_neq(Visibility::Hidden); } } } ``` </details> --------- Co-authored-by: Benjamin Brienen <Benjamin.Brienen@outlook.com> Co-authored-by: Ben Frankel <ben.frankel7@gmail.com>
302 lines
9.3 KiB
Rust
302 lines
9.3 KiB
Rust
//! 2d testbed
|
|
//!
|
|
//! You can switch scene by pressing the spacebar
|
|
|
|
mod helpers;
|
|
|
|
use bevy::prelude::*;
|
|
use helpers::Next;
|
|
|
|
fn main() {
|
|
let mut app = App::new();
|
|
app.add_plugins((DefaultPlugins,))
|
|
.init_state::<Scene>()
|
|
.add_systems(OnEnter(Scene::Shapes), shapes::setup)
|
|
.add_systems(OnEnter(Scene::Bloom), bloom::setup)
|
|
.add_systems(OnEnter(Scene::Text), text::setup)
|
|
.add_systems(OnEnter(Scene::Sprite), sprite::setup)
|
|
.add_systems(OnEnter(Scene::Gizmos), gizmos::setup)
|
|
.add_systems(Update, switch_scene)
|
|
.add_systems(Update, gizmos::draw_gizmos.run_if(in_state(Scene::Gizmos)));
|
|
|
|
#[cfg(feature = "bevy_ci_testing")]
|
|
app.add_systems(Update, helpers::switch_scene_in_ci::<Scene>);
|
|
|
|
app.run();
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)]
|
|
#[states(scoped_entities)]
|
|
enum Scene {
|
|
#[default]
|
|
Shapes,
|
|
Bloom,
|
|
Text,
|
|
Sprite,
|
|
Gizmos,
|
|
}
|
|
|
|
impl Next for Scene {
|
|
fn next(&self) -> Self {
|
|
match self {
|
|
Scene::Shapes => Scene::Bloom,
|
|
Scene::Bloom => Scene::Text,
|
|
Scene::Text => Scene::Sprite,
|
|
Scene::Sprite => Scene::Gizmos,
|
|
Scene::Gizmos => Scene::Shapes,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn switch_scene(
|
|
keyboard: Res<ButtonInput<KeyCode>>,
|
|
scene: Res<State<Scene>>,
|
|
mut next_scene: ResMut<NextState<Scene>>,
|
|
) {
|
|
if keyboard.just_pressed(KeyCode::Space) {
|
|
info!("Switching scene");
|
|
next_scene.set(scene.get().next());
|
|
}
|
|
}
|
|
|
|
mod shapes {
|
|
use bevy::prelude::*;
|
|
|
|
const X_EXTENT: f32 = 900.;
|
|
|
|
pub fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
|
) {
|
|
commands.spawn((Camera2d, DespawnOnExitState(super::Scene::Shapes)));
|
|
|
|
let shapes = [
|
|
meshes.add(Circle::new(50.0)),
|
|
meshes.add(CircularSector::new(50.0, 1.0)),
|
|
meshes.add(CircularSegment::new(50.0, 1.25)),
|
|
meshes.add(Ellipse::new(25.0, 50.0)),
|
|
meshes.add(Annulus::new(25.0, 50.0)),
|
|
meshes.add(Capsule2d::new(25.0, 50.0)),
|
|
meshes.add(Rhombus::new(75.0, 100.0)),
|
|
meshes.add(Rectangle::new(50.0, 100.0)),
|
|
meshes.add(RegularPolygon::new(50.0, 6)),
|
|
meshes.add(Triangle2d::new(
|
|
Vec2::Y * 50.0,
|
|
Vec2::new(-50.0, -50.0),
|
|
Vec2::new(50.0, -50.0),
|
|
)),
|
|
];
|
|
let num_shapes = shapes.len();
|
|
|
|
for (i, shape) in shapes.into_iter().enumerate() {
|
|
// Distribute colors evenly across the rainbow.
|
|
let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);
|
|
|
|
commands.spawn((
|
|
Mesh2d(shape),
|
|
MeshMaterial2d(materials.add(color)),
|
|
Transform::from_xyz(
|
|
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
|
|
-X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
|
|
0.0,
|
|
0.0,
|
|
),
|
|
DespawnOnExitState(super::Scene::Shapes),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
mod bloom {
|
|
use bevy::{
|
|
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
|
|
prelude::*,
|
|
};
|
|
|
|
pub fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
|
) {
|
|
commands.spawn((
|
|
Camera2d,
|
|
Camera {
|
|
hdr: true,
|
|
..default()
|
|
},
|
|
Tonemapping::TonyMcMapface,
|
|
Bloom::default(),
|
|
DespawnOnExitState(super::Scene::Bloom),
|
|
));
|
|
|
|
commands.spawn((
|
|
Mesh2d(meshes.add(Circle::new(100.))),
|
|
MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
|
|
Transform::from_translation(Vec3::new(-200., 0., 0.)),
|
|
DespawnOnExitState(super::Scene::Bloom),
|
|
));
|
|
|
|
commands.spawn((
|
|
Mesh2d(meshes.add(RegularPolygon::new(100., 6))),
|
|
MeshMaterial2d(materials.add(Color::srgb(6.25, 9.4, 9.1))),
|
|
Transform::from_translation(Vec3::new(200., 0., 0.)),
|
|
DespawnOnExitState(super::Scene::Bloom),
|
|
));
|
|
}
|
|
}
|
|
|
|
mod text {
|
|
use bevy::color::palettes;
|
|
use bevy::prelude::*;
|
|
use bevy::sprite::Anchor;
|
|
use bevy::text::TextBounds;
|
|
|
|
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
commands.spawn((Camera2d, DespawnOnExitState(super::Scene::Text)));
|
|
|
|
for (i, justify) in [
|
|
JustifyText::Left,
|
|
JustifyText::Right,
|
|
JustifyText::Center,
|
|
JustifyText::Justified,
|
|
]
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
let y = 230. - 150. * i as f32;
|
|
spawn_anchored_text(&mut commands, -300. * Vec3::X + y * Vec3::Y, justify, None);
|
|
spawn_anchored_text(
|
|
&mut commands,
|
|
300. * Vec3::X + y * Vec3::Y,
|
|
justify,
|
|
Some(TextBounds::new(150., 55.)),
|
|
);
|
|
}
|
|
|
|
let sans_serif = TextFont::from_font(asset_server.load("fonts/FiraSans-Bold.ttf"));
|
|
|
|
const NUM_ITERATIONS: usize = 10;
|
|
for i in 0..NUM_ITERATIONS {
|
|
let fraction = i as f32 / (NUM_ITERATIONS - 1) as f32;
|
|
|
|
commands.spawn((
|
|
Text2d::new("Bevy"),
|
|
sans_serif.clone(),
|
|
Transform::from_xyz(0.0, fraction * 200.0, i as f32)
|
|
.with_scale(1.0 + Vec2::splat(fraction).extend(1.))
|
|
.with_rotation(Quat::from_rotation_z(fraction * core::f32::consts::PI)),
|
|
TextColor(Color::hsla(fraction * 360.0, 0.8, 0.8, 0.8)),
|
|
DespawnOnExitState(super::Scene::Text),
|
|
));
|
|
}
|
|
|
|
commands.spawn((
|
|
Text2d::new("This text is invisible."),
|
|
Visibility::Hidden,
|
|
DespawnOnExitState(super::Scene::Text),
|
|
));
|
|
}
|
|
|
|
fn spawn_anchored_text(
|
|
commands: &mut Commands,
|
|
dest: Vec3,
|
|
justify: JustifyText,
|
|
bounds: Option<TextBounds>,
|
|
) {
|
|
commands.spawn((
|
|
Sprite {
|
|
color: palettes::css::YELLOW.into(),
|
|
custom_size: Some(5. * Vec2::ONE),
|
|
..Default::default()
|
|
},
|
|
Transform::from_translation(dest),
|
|
DespawnOnExitState(super::Scene::Text),
|
|
));
|
|
|
|
for anchor in [
|
|
Anchor::TOP_LEFT,
|
|
Anchor::TOP_RIGHT,
|
|
Anchor::BOTTOM_RIGHT,
|
|
Anchor::BOTTOM_LEFT,
|
|
] {
|
|
let mut text = commands.spawn((
|
|
Text2d::new("L R\n"),
|
|
TextLayout::new_with_justify(justify),
|
|
Transform::from_translation(dest + Vec3::Z),
|
|
anchor,
|
|
DespawnOnExitState(super::Scene::Text),
|
|
children![
|
|
(
|
|
TextSpan::new(format!("{}, {}\n", anchor.x, anchor.y)),
|
|
TextFont::from_font_size(14.0),
|
|
TextColor(palettes::tailwind::BLUE_400.into()),
|
|
),
|
|
(
|
|
TextSpan::new(format!("{justify:?}")),
|
|
TextFont::from_font_size(14.0),
|
|
TextColor(palettes::tailwind::GREEN_400.into()),
|
|
),
|
|
],
|
|
));
|
|
if let Some(bounds) = bounds {
|
|
text.insert(bounds);
|
|
|
|
commands.spawn((
|
|
Sprite {
|
|
color: palettes::tailwind::GRAY_900.into(),
|
|
custom_size: Some(Vec2::new(bounds.width.unwrap(), bounds.height.unwrap())),
|
|
anchor,
|
|
..Default::default()
|
|
},
|
|
Transform::from_translation(dest - Vec3::Z),
|
|
DespawnOnExitState(super::Scene::Text),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mod sprite {
|
|
use bevy::color::palettes::css::{BLUE, LIME, RED};
|
|
use bevy::prelude::*;
|
|
use bevy::sprite::Anchor;
|
|
|
|
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
commands.spawn((Camera2d, DespawnOnExitState(super::Scene::Sprite)));
|
|
for (anchor, flip_x, flip_y, color) in [
|
|
(Anchor::BOTTOM_LEFT, false, false, Color::WHITE),
|
|
(Anchor::BOTTOM_RIGHT, true, false, RED.into()),
|
|
(Anchor::TOP_LEFT, false, true, LIME.into()),
|
|
(Anchor::TOP_RIGHT, true, true, BLUE.into()),
|
|
] {
|
|
commands.spawn((
|
|
Sprite {
|
|
image: asset_server.load("branding/bevy_logo_dark.png"),
|
|
anchor,
|
|
flip_x,
|
|
flip_y,
|
|
color,
|
|
..default()
|
|
},
|
|
DespawnOnExitState(super::Scene::Sprite),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
mod gizmos {
|
|
use bevy::{color::palettes::css::*, prelude::*};
|
|
|
|
pub fn setup(mut commands: Commands) {
|
|
commands.spawn((Camera2d, DespawnOnExitState(super::Scene::Gizmos)));
|
|
}
|
|
|
|
pub fn draw_gizmos(mut gizmos: Gizmos) {
|
|
gizmos.rect_2d(Isometry2d::IDENTITY, Vec2::new(200.0, 200.0), RED);
|
|
gizmos
|
|
.circle_2d(Isometry2d::IDENTITY, 200.0, GREEN)
|
|
.resolution(64);
|
|
}
|
|
}
|