diff --git a/Cargo.toml b/Cargo.toml index 715d1e683aa03..eafad33e58694 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2007,6 +2007,17 @@ description = "Demonstrates how to send and receive events of the same type in a category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "entity_disabling" +path = "examples/ecs/entity_disabling.rs" +doc-scrape-examples = true + +[package.metadata.example.entity_disabling] +name = "Entity disabling" +description = "Demonstrates how to hide entities from the ECS without deleting them" +category = "ECS (Entity Component System)" +wasm = true + [[example]] name = "fixed_timestep" path = "examples/ecs/fixed_timestep.rs" diff --git a/examples/README.md b/examples/README.md index dc562b2bf9fe4..fef76c1a19fb3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -307,6 +307,7 @@ Example | Description [Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components [ECS Guide](../examples/ecs/ecs_guide.rs) | Full guide to Bevy's ECS +[Entity disabling](../examples/ecs/entity_disabling.rs) | Demonstrates how to hide entities from the ECS without deleting them [Event](../examples/ecs/event.rs) | Illustrates event creation, activation, and reception [Fallible System Parameters](../examples/ecs/fallible_params.rs) | Systems are skipped if their parameters cannot be acquired [Fallible Systems](../examples/ecs/fallible_systems.rs) | Systems that return results to handle errors diff --git a/examples/ecs/entity_disabling.rs b/examples/ecs/entity_disabling.rs new file mode 100644 index 0000000000000..d8c1301324805 --- /dev/null +++ b/examples/ecs/entity_disabling.rs @@ -0,0 +1,153 @@ +//! Disabling entities is a powerful feature that allows you to hide entities from the ECS without deleting them. +//! +//! This can be useful for implementing features like "sleeping" objects that are offscreen +//! or managing networked entities. +//! +//! While disabling entities *will* make them invisible, +//! that's not its primary purpose! +//! [`Visibility`](bevy::prelude::Visibility) should be used to hide entities; +//! disabled entities are skipped entirely, which can lead to subtle bugs. +//! +//! # Default query filters +//! +//! Under the hood, Bevy uses a "default query filter" that skips entities with the +//! the [`Disabled`] component. +//! These filters act as a by-default exclusion list for all queries, +//! and can be bypassed by explicitly including these components in your queries. +//! For example, `Query<&A, With`, `Query<(Entity, Has>)` or +//! `Query<&A, Or<(With, With)>>` will include disabled entities. + +use bevy::ecs::entity_disabling::Disabled; +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, MeshPickingPlugin)) + .add_observer(disable_entities_on_click) + .add_systems( + Update, + (list_all_named_entities, reenable_entities_on_space), + ) + .add_systems(Startup, (setup_scene, display_instructions)) + .run(); +} + +#[derive(Component)] +struct DisableOnClick; + +fn disable_entities_on_click( + trigger: Trigger>, + valid_query: Query<&DisableOnClick>, + mut commands: Commands, +) { + let clicked_entity = trigger.target(); + // Windows and text are entities and can be clicked! + // We definitely don't want to disable the window itself, + // because that would cause the app to close! + if valid_query.contains(clicked_entity) { + // Just add the `Disabled` component to the entity to disable it. + // Note that the `Disabled` component is *only* added to the entity, + // its children are not affected. + commands.entity(clicked_entity).insert(Disabled); + } +} + +#[derive(Component)] +struct EntityNameText; + +// The query here will not find entities with the `Disabled` component, +// because it does not explicitly include it. +fn list_all_named_entities( + query: Query<&Name>, + mut name_text_query: Query<&mut Text, With>, + mut commands: Commands, +) { + let mut text_string = String::from("Named entities found:\n"); + // Query iteration order is not guaranteed, so we sort the names + // to ensure the output is consistent. + for name in query.iter().sort::<&Name>() { + text_string.push_str(&format!("{:?}\n", name)); + } + + if let Ok(mut text) = name_text_query.get_single_mut() { + *text = Text::new(text_string); + } else { + commands.spawn(( + EntityNameText, + Text::default(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + right: Val::Px(12.0), + ..default() + }, + )); + } +} + +fn reenable_entities_on_space( + mut commands: Commands, + // This query can find disabled entities, + // because it explicitly includes the `Disabled` component. + disabled_entities: Query>, + input: Res>, +) { + if input.just_pressed(KeyCode::Space) { + for entity in disabled_entities.iter() { + // To re-enable an entity, just remove the `Disabled` component. + commands.entity(entity).remove::(); + } + } +} + +const X_EXTENT: f32 = 900.; + +fn setup_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(Camera2d); + + let named_shapes = [ + (Name::new("Annulus"), meshes.add(Annulus::new(25.0, 50.0))), + ( + Name::new("Bestagon"), + meshes.add(RegularPolygon::new(50.0, 6)), + ), + (Name::new("Rhombus"), meshes.add(Rhombus::new(75.0, 100.0))), + ]; + let num_shapes = named_shapes.len(); + + for (i, (name, shape)) in named_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(( + name, + DisableOnClick, + 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, + ), + )); + } +} + +fn display_instructions(mut commands: Commands) { + commands.spawn(( + Text::new( + "Click an entity to disable it.\n\nPress Space to re-enable all disabled entities.", + ), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }, + )); +}