From 896f09c27b5736d9eb1d7cee1ef123ed28a10076 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 6 Feb 2025 13:00:13 -0800 Subject: [PATCH 1/8] Example stub --- Cargo.toml | 11 +++++++ examples/README.md | 1 + examples/ecs/prefabs.rs | 63 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 examples/ecs/prefabs.rs 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/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..399c3990615bc --- /dev/null +++ b/examples/ecs/prefabs.rs @@ -0,0 +1,63 @@ +//! 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 using default query filters. +//! 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 bevy::prelude::*; + +#[derive(States, Debug, PartialEq, Eq, Clone, Copy, Hash)] +enum AssetLoadingState { + Loading, + Loaded, +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup_scene) + .add_systems(OnEnter(AssetLoadingState::Loading), load_models) + .run(); +} + +fn setup_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Circular floor to display our models on + commands.spawn(( + Mesh3d(meshes.add(Circle::new(4.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + // Light + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn load_models() {} From a94fced25eea3eafcb2977a3df96701e35bd7b44 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 9 Feb 2025 14:22:46 -0800 Subject: [PATCH 2/8] Make `Disabled` Clone --- crates/bevy_ecs/src/entity_disabling.rs | 2 +- examples/ecs/prefabs.rs | 28 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) 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/ecs/prefabs.rs b/examples/ecs/prefabs.rs index 399c3990615bc..680542a111d3a 100644 --- a/examples/ecs/prefabs.rs +++ b/examples/ecs/prefabs.rs @@ -18,24 +18,21 @@ //! 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 bevy::prelude::*; - -#[derive(States, Debug, PartialEq, Eq, Clone, Copy, Hash)] -enum AssetLoadingState { - Loading, - Loaded, -} +use bevy::{ecs::entity_disabling::Disabled, prelude::*, scene::SceneInstanceReady}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup_scene) - .add_systems(OnEnter(AssetLoadingState::Loading), load_models) .run(); } +// An example asset that contains a mesh composed of multiple entities. +const GLTF_PATH: &str = "models/animated/Fox.glb"; + fn setup_scene( mut commands: Commands, + asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, ) { @@ -58,6 +55,19 @@ fn setup_scene( Camera3d::default(), Transform::from_xyz(-2.5, 4.5, 9.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); } -fn load_models() {} +// 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 commands: Commands) { + let scene_root_entity = trigger.target(); + commands + .entity(scene_root_entity) + // Scenes are generally composed of multiple entities, + // so we need to make any changes to the scene as a whole. + .insert_recursive::(Disabled); +} From 5ef6d8911bf09fec2de41040aef671278de8bc62 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 9 Feb 2025 14:43:58 -0800 Subject: [PATCH 3/8] Fix camera --- examples/ecs/prefabs.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/ecs/prefabs.rs b/examples/ecs/prefabs.rs index 680542a111d3a..c2b27ab2ed8f1 100644 --- a/examples/ecs/prefabs.rs +++ b/examples/ecs/prefabs.rs @@ -38,7 +38,7 @@ fn setup_scene( ) { // Circular floor to display our models on commands.spawn(( - Mesh3d(meshes.add(Circle::new(4.0))), + Mesh3d(meshes.add(Circle::new(100.0))), MeshMaterial3d(materials.add(Color::WHITE)), Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), )); @@ -48,12 +48,12 @@ fn setup_scene( shadows_enabled: true, ..default() }, - Transform::from_xyz(4.0, 8.0, 4.0), + Transform::from_xyz(100.0, 200.0, 200.0), )); // Camera commands.spawn(( Camera3d::default(), - Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y), )); // Load in our test scene that we're storing as a prefab @@ -65,9 +65,9 @@ fn setup_scene( // allowing us to modify the scene as we please. fn respond_to_scene_loaded(trigger: Trigger, 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) - // Scenes are generally composed of multiple entities, - // so we need to make any changes to the scene as a whole. .insert_recursive::(Disabled); } From a20adfd0aa349060ebce1d1f601b44af337bdfee Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 9 Feb 2025 14:51:45 -0800 Subject: [PATCH 4/8] Bigger ground plane --- examples/ecs/prefabs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ecs/prefabs.rs b/examples/ecs/prefabs.rs index c2b27ab2ed8f1..f48801419a4c8 100644 --- a/examples/ecs/prefabs.rs +++ b/examples/ecs/prefabs.rs @@ -36,9 +36,9 @@ fn setup_scene( mut meshes: ResMut>, mut materials: ResMut>, ) { - // Circular floor to display our models on + // Large floor plane to display our models on commands.spawn(( - Mesh3d(meshes.add(Circle::new(100.0))), + 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)), )); @@ -53,7 +53,7 @@ fn setup_scene( // Camera 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), + 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 From 5939196c6e87bdfb273f08feba0c1450c80e6909 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 9 Feb 2025 15:03:13 -0800 Subject: [PATCH 5/8] Demo prefab concept --- examples/ecs/prefabs.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/examples/ecs/prefabs.rs b/examples/ecs/prefabs.rs index f48801419a4c8..9a9914f62ce1c 100644 --- a/examples/ecs/prefabs.rs +++ b/examples/ecs/prefabs.rs @@ -18,18 +18,26 @@ //! 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 bevy::platform_support::collections::HashMap; use bevy::{ecs::entity_disabling::Disabled, prelude::*, scene::SceneInstanceReady}; fn main() { App::new() .add_plugins(DefaultPlugins) + .init_resource::() .add_systems(Startup, setup_scene) + .add_systems(Update, spawn_prefab) .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, @@ -63,11 +71,32 @@ fn setup_scene( // 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 commands: Commands) { +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 + prefabs.0.insert("fox".to_string(), scene_root_entity); +} + +// TODO: make this more interactive and controllable +fn spawn_prefab(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 fresh_clone = commands.entity(*prefab_entity).clone_and_spawn().id(); + commands + .entity(fresh_clone) + .remove_recursive::(); } From 38005282355243ac7425699cc1d5225422ade07d Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 9 Feb 2025 15:18:36 -0800 Subject: [PATCH 6/8] Spawn entities on mouse position --- examples/ecs/prefabs.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/examples/ecs/prefabs.rs b/examples/ecs/prefabs.rs index 9a9914f62ce1c..8c84dea845834 100644 --- a/examples/ecs/prefabs.rs +++ b/examples/ecs/prefabs.rs @@ -8,7 +8,7 @@ //! //! 1. Load asssets from disk. //! 2. Create prefab entities from those assets. -//! 3. Make sure that these prefab entities aren't accidentally modified using default query filters. +//! 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, @@ -23,10 +23,10 @@ use bevy::{ecs::entity_disabling::Disabled, prelude::*, scene::SceneInstanceRead fn main() { App::new() - .add_plugins(DefaultPlugins) + .add_plugins((DefaultPlugins, MeshPickingPlugin)) .init_resource::() .add_systems(Startup, setup_scene) - .add_systems(Update, spawn_prefab) + .add_observer(spawn_prefab_on_mouse_click) .run(); } @@ -52,8 +52,9 @@ fn setup_scene( )); // Light commands.spawn(( - PointLight { + DirectionalLight { shadows_enabled: true, + illuminance: 32000.0, ..default() }, Transform::from_xyz(100.0, 200.0, 200.0), @@ -84,19 +85,32 @@ fn respond_to_scene_loaded( // TODO: use a custom DQF component to convey semantics .insert_recursive::(Disabled); - // Store the scene root entity in our prefab resource + // 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); } -// TODO: make this more interactive and controllable -fn spawn_prefab(prefabs: Res, mut commands: Commands) { +fn spawn_prefab_on_mouse_click( + 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 + commands + .entity(fresh_clone) + .insert(Transform::from_translation(*click_position)); } From a257facf072c27d8db99cb29d603f3e9d80afafd Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 9 Feb 2025 15:24:58 -0800 Subject: [PATCH 7/8] Randomize rotations --- examples/ecs/prefabs.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/ecs/prefabs.rs b/examples/ecs/prefabs.rs index 8c84dea845834..f82972613f469 100644 --- a/examples/ecs/prefabs.rs +++ b/examples/ecs/prefabs.rs @@ -18,6 +18,8 @@ //! 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}; @@ -110,7 +112,13 @@ fn spawn_prefab_on_mouse_click( .entity(fresh_clone) .remove_recursive::(); // Overwrite the position of the prefab entity with the new value we want to spawn it at - commands - .entity(fresh_clone) - .insert(Transform::from_translation(*click_position)); + // 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)), + ); + + println!("Spawned a prefab at {:?}", click_position); } From 6634fb063c27fe91a89af3d31ef84657bb2c5d28 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 9 Feb 2025 15:28:10 -0800 Subject: [PATCH 8/8] Avoid double events by stopping propagation --- examples/ecs/prefabs.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/ecs/prefabs.rs b/examples/ecs/prefabs.rs index f82972613f469..a7e22036ee27d 100644 --- a/examples/ecs/prefabs.rs +++ b/examples/ecs/prefabs.rs @@ -93,7 +93,7 @@ fn respond_to_scene_loaded( } fn spawn_prefab_on_mouse_click( - trigger: Trigger>, + mut trigger: Trigger>, prefabs: Res, mut commands: Commands, ) { @@ -120,5 +120,7 @@ fn spawn_prefab_on_mouse_click( .with_rotation(Quat::from_rotation_y(random_angle)), ); - println!("Spawned a prefab at {:?}", click_position); + // We've already handled this event; + // so we don't want it to propagate any further. + trigger.propagate(false); }