diff --git a/Cargo.toml b/Cargo.toml index 715d1e683aa03..ae4b37def80d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2084,6 +2084,17 @@ description = "Illustrates parallel queries with `ParallelIterator`" category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "prefabs" +path = "examples/ecs/prefabs.rs" +doc-scrape-examples = true + +[package.metadata.example.prefabs] +name = "Prefabs" +description = "Demonstrates how to load, store and clone template entity collections" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "relationships" path = "examples/ecs/relationships.rs" diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index f96c78c1632c9..b526476cbb0f7 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -34,7 +34,7 @@ use {crate::reflect::ReflectComponent, bevy_reflect::Reflect}; /// A marker component for disabled entities. See [the module docs] for more info. /// /// [the module docs]: crate::entity_disabling -#[derive(Component)] +#[derive(Component, Debug, Clone, Copy)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct Disabled; diff --git a/examples/README.md b/examples/README.md index dc562b2bf9fe4..b17a158059267 100644 --- a/examples/README.md +++ b/examples/README.md @@ -320,6 +320,7 @@ Example | Description [Observers](../examples/ecs/observers.rs) | Demonstrates observers that react to events (both built-in life-cycle events and custom events) [One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them [Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator` +[Prefabs](../examples/ecs/prefabs.rs) | Demonstrates how to load, store and clone template entity collections [Relationships](../examples/ecs/relationships.rs) | Define and work with custom relationships between entities [Removal Detection](../examples/ecs/removal_detection.rs) | Query for entities that had a specific component removed earlier in the current frame [Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met diff --git a/examples/ecs/prefabs.rs b/examples/ecs/prefabs.rs new file mode 100644 index 0000000000000..a7e22036ee27d --- /dev/null +++ b/examples/ecs/prefabs.rs @@ -0,0 +1,126 @@ +//! Generating a collection of "prefab" entities can be faster and cleaner than +//! loading them from assets each time or working entirely in code. +//! +//! Rather than providing an opinonated prefab system, Bevy provides a flexible +//! set of tools that can be used to create and modify your solution. +//! +//! The core workflow is pretty straightforward: +//! +//! 1. Load asssets from disk. +//! 2. Create prefab entities from those assets. +//! 3. Make sure that these prefab entities aren't accidentally modified by adding a component that cause them to be ignored by default. +//! 4. Clone these entities (and their children) out from the prefab when you need to spawn an instance of them. +//! +//! This solution can be easily adapted to meet the needs of your own asset loading workflows, +//! and variants of prefabs (e.g. enemy variants) can readily be constructed ahead of time and stored for easy access. +//! +//! Be mindful of memory usage when defining prefabs; while they won't be seen by game logic, +//! the components and assets that they use will still be loaded into memory (although asset data is shared between instances). +//! Loading and unloading assets dynamically (e.g. per level) is an important strategy to manage memory usage. + +use std::f32::consts::PI; + +use bevy::platform_support::collections::HashMap; +use bevy::{ecs::entity_disabling::Disabled, prelude::*, scene::SceneInstanceReady}; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, MeshPickingPlugin)) + .init_resource::() + .add_systems(Startup, setup_scene) + .add_observer(spawn_prefab_on_mouse_click) + .run(); +} + +// An example asset that contains a mesh composed of multiple entities. +const GLTF_PATH: &str = "models/animated/Fox.glb"; + +/// We're keeping track of our disabled prefab entities in a resource, +/// allowing us to quickly look up and clone them when we need to spawn them. +#[derive(Resource, Default)] +struct Prefabs(HashMap); + +fn setup_scene( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Large floor plane to display our models on + commands.spawn(( + Mesh3d(meshes.add(Rectangle::new(500.0, 500.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + // Light + commands.spawn(( + DirectionalLight { + shadows_enabled: true, + illuminance: 32000.0, + ..default() + }, + Transform::from_xyz(100.0, 200.0, 200.0), + )); + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(100.0, 400.0, 100.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Load in our test scene that we're storing as a prefab + let mesh_scene = SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH))); + commands.spawn(mesh_scene).observe(respond_to_scene_loaded); +} + +// This observer will be triggered when the scene is loaded, +// allowing us to modify the scene as we please. +fn respond_to_scene_loaded( + trigger: Trigger, + mut prefabs: ResMut, + mut commands: Commands, +) { + let scene_root_entity = trigger.target(); + // Scenes are typically composed of multiple entities, so we need to + // modify all entities in the scene to disable the scene. + commands + .entity(scene_root_entity) + // TODO: use a custom DQF component to convey semantics + .insert_recursive::(Disabled); + + // Store the scene root entity in our prefab resource, + // along with a key that we can use to look it up later. + prefabs.0.insert("fox".to_string(), scene_root_entity); +} + +fn spawn_prefab_on_mouse_click( + mut trigger: Trigger>, + prefabs: Res, + mut commands: Commands, +) { + // Check that the prefab we're looking for is ready + let Some(prefab_entity) = prefabs.0.get("fox") else { + return; + }; + + let maybe_click_position = &trigger.event().event.hit.position; + let Some(click_position) = maybe_click_position else { + return; + }; + + let fresh_clone = commands.entity(*prefab_entity).clone_and_spawn().id(); + commands + .entity(fresh_clone) + .remove_recursive::(); + // Overwrite the position of the prefab entity with the new value we want to spawn it at + // and give it a random rotation. + let random_angle = PI * 2.0 * rand::random::(); + + commands.entity(fresh_clone).insert( + Transform::from_translation(*click_position) + .with_rotation(Quat::from_rotation_y(random_angle)), + ); + + // We've already handled this event; + // so we don't want it to propagate any further. + trigger.propagate(false); +}