bevy/examples/testbed/3d.rs
mgi388 7a1fcb7fe7
Rename StateScoped to DespawnOnExitState and add DespawnOnEnterState (#18818)
# 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>
2025-05-06 00:37:04 +00:00

327 lines
9.5 KiB
Rust

//! 3d 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::Light), light::setup)
.add_systems(OnEnter(Scene::Bloom), bloom::setup)
.add_systems(OnEnter(Scene::Gltf), gltf::setup)
.add_systems(OnEnter(Scene::Animation), animation::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]
Light,
Bloom,
Gltf,
Animation,
Gizmos,
}
impl Next for Scene {
fn next(&self) -> Self {
match self {
Scene::Light => Scene::Bloom,
Scene::Bloom => Scene::Gltf,
Scene::Gltf => Scene::Animation,
Scene::Animation => Scene::Gizmos,
Scene::Gizmos => Scene::Light,
}
}
}
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 light {
use std::f32::consts::PI;
use bevy::{
color::palettes::css::{DEEP_PINK, LIME, RED},
prelude::*,
};
const CURRENT_SCENE: super::Scene = super::Scene::Light;
pub fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::WHITE,
perceptual_roughness: 1.0,
..default()
})),
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: DEEP_PINK.into(),
..default()
})),
Transform::from_xyz(0.0, 1.0, 0.0),
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
PointLight {
intensity: 100_000.0,
color: RED.into(),
shadows_enabled: true,
..default()
},
Transform::from_xyz(1.0, 2.0, 0.0),
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
SpotLight {
intensity: 100_000.0,
color: LIME.into(),
shadows_enabled: true,
inner_angle: 0.6,
outer_angle: 0.8,
..default()
},
Transform::from_xyz(-1.0, 2.0, 0.0).looking_at(Vec3::new(-1.0, 0.0, 0.0), Vec3::Z),
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
DirectionalLight {
illuminance: light_consts::lux::OVERCAST_DAY,
shadows_enabled: true,
..default()
},
Transform {
translation: Vec3::new(0.0, 2.0, 0.0),
rotation: Quat::from_rotation_x(-PI / 4.),
..default()
},
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
DespawnOnExitState(CURRENT_SCENE),
));
}
}
mod bloom {
use bevy::{
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
prelude::*,
};
const CURRENT_SCENE: super::Scene = super::Scene::Bloom;
pub fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Camera3d::default(),
Camera {
hdr: true,
..default()
},
Tonemapping::TonyMcMapface,
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
Bloom::NATURAL,
DespawnOnExitState(CURRENT_SCENE),
));
let material_emissive1 = materials.add(StandardMaterial {
emissive: LinearRgba::rgb(13.99, 5.32, 2.0),
..default()
});
let material_emissive2 = materials.add(StandardMaterial {
emissive: LinearRgba::rgb(2.0, 13.99, 5.32),
..default()
});
let mesh = meshes.add(Sphere::new(0.5).mesh().ico(5).unwrap());
for z in -2..3_i32 {
let material = match (z % 2).abs() {
0 => material_emissive1.clone(),
1 => material_emissive2.clone(),
_ => unreachable!(),
};
commands.spawn((
Mesh3d(mesh.clone()),
MeshMaterial3d(material),
Transform::from_xyz(z as f32 * 2.0, 0.0, 0.0),
DespawnOnExitState(CURRENT_SCENE),
));
}
}
}
mod gltf {
use bevy::prelude::*;
const CURRENT_SCENE: super::Scene = super::Scene::Gltf;
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
EnvironmentMapLight {
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
intensity: 250.0,
..default()
},
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
DirectionalLight {
shadows_enabled: true,
..default()
},
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
SceneRoot(asset_server.load(
GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"),
)),
DespawnOnExitState(CURRENT_SCENE),
));
}
}
mod animation {
use std::{f32::consts::PI, time::Duration};
use bevy::{prelude::*, scene::SceneInstanceReady};
const CURRENT_SCENE: super::Scene = super::Scene::Animation;
const FOX_PATH: &str = "models/animated/Fox.glb";
#[derive(Resource)]
struct Animation {
animation: AnimationNodeIndex,
graph: Handle<AnimationGraph>,
}
pub fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
let (graph, node) = AnimationGraph::from_clip(
asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),
);
let graph_handle = graphs.add(graph);
commands.insert_resource(Animation {
animation: node,
graph: graph_handle,
});
commands.spawn((
Camera3d::default(),
Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
DespawnOnExitState(CURRENT_SCENE),
));
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadows_enabled: true,
..default()
},
DespawnOnExitState(CURRENT_SCENE),
));
commands
.spawn((
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH))),
DespawnOnExitState(CURRENT_SCENE),
))
.observe(pause_animation_frame);
}
fn pause_animation_frame(
trigger: Trigger<SceneInstanceReady>,
children: Query<&Children>,
mut commands: Commands,
animation: Res<Animation>,
mut players: Query<(Entity, &mut AnimationPlayer)>,
) {
for child in children.iter_descendants(trigger.target()) {
if let Ok((entity, mut player)) = players.get_mut(child) {
let mut transitions = AnimationTransitions::new();
transitions
.play(&mut player, animation.animation, Duration::ZERO)
.seek_to(0.5)
.pause();
commands
.entity(entity)
.insert(AnimationGraphHandle(animation.graph.clone()))
.insert(transitions);
}
}
}
}
mod gizmos {
use bevy::{color::palettes::css::*, prelude::*};
pub fn setup(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
DespawnOnExitState(super::Scene::Gizmos),
));
}
pub fn draw_gizmos(mut gizmos: Gizmos) {
gizmos.cuboid(
Transform::from_translation(Vec3::X * 2.0).with_scale(Vec3::splat(2.0)),
RED,
);
gizmos
.sphere(Isometry3d::from_translation(Vec3::X * -2.0), 1.0, GREEN)
.resolution(30_000 / 3);
}
}