diff --git a/Cargo.toml b/Cargo.toml index 6a5e5a94a5..dee4cebcbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,7 +164,7 @@ audio = ["bevy_audio", "vorbis"] audio-all-formats = ["bevy_audio", "aac", "flac", "mp3", "mp4", "vorbis", "wav"] # COLLECTION: Features used to compose Bevy scenes. -scene = ["bevy_scene"] +scene = ["bevy_scene", "bevy_scene2"] # COLLECTION: Enables picking with all backends. picking = ["bevy_picking", "mesh_picking", "sprite_picking", "ui_picking"] @@ -320,6 +320,9 @@ bevy_render = ["bevy_internal/bevy_render"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene"] +# Provides scene functionality +bevy_scene2 = ["bevy_internal/bevy_scene2"] + # Provides raytraced lighting (experimental) bevy_solari = ["bevy_internal/bevy_solari"] @@ -731,6 +734,7 @@ bevy_image = { path = "crates/bevy_image", version = "0.19.0-dev", default-featu bevy_reflect = { path = "crates/bevy_reflect", version = "0.19.0-dev", default-features = false } bevy_render = { path = "crates/bevy_render", version = "0.19.0-dev", default-features = false } bevy_state = { path = "crates/bevy_state", version = "0.19.0-dev", default-features = false } +bevy_scene2 = { path = "crates/bevy_scene2", version = "0.19.0-dev", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" futures-timer = { version = "3", features = ["wasm-bindgen", "gloo-timers"] } @@ -3006,6 +3010,17 @@ description = "Demonstrates loading from and saving scenes to files" category = "Scene" wasm = false +[[example]] +name = "bsn" +path = "examples/scene/bsn.rs" +doc-scrape-examples = true + +[package.metadata.example.bsn] +name = "BSN example" +description = "Demonstrates how to use BSN to compose scenes" +category = "Scene" +wasm = true + # Shaders [[package.metadata.example_category]] name = "Shaders" diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 49d2fcacfa..51fde2893a 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -24,7 +24,9 @@ bevy_camera = { path = "../crates/bevy_camera" } bevy_mesh = { path = "../crates/bevy_mesh" } bevy_asset = { path = "../crates/bevy_asset" } bevy_render = { path = "../crates/bevy_render" } +bevy_scene2 = { path = "../crates/bevy_scene2" } bevy_tasks = { path = "../crates/bevy_tasks" } +bevy_ui = { path = "../crates/bevy_ui" } bevy_platform = { path = "../crates/bevy_platform", default-features = false, features = [ "std", ] } @@ -99,6 +101,11 @@ name = "render" path = "benches/bevy_render/main.rs" harness = false +[[bench]] +name = "scene" +path = "benches/bevy_scene/main.rs" +harness = false + [[bench]] name = "tasks" path = "benches/bevy_tasks/main.rs" diff --git a/benches/benches/bevy_scene/main.rs b/benches/benches/bevy_scene/main.rs new file mode 100644 index 0000000000..82e59792ba --- /dev/null +++ b/benches/benches/bevy_scene/main.rs @@ -0,0 +1,5 @@ +use criterion::criterion_main; + +mod spawn; + +criterion_main!(spawn::benches); diff --git a/benches/benches/bevy_scene/spawn.rs b/benches/benches/bevy_scene/spawn.rs new file mode 100644 index 0000000000..ced366c304 --- /dev/null +++ b/benches/benches/bevy_scene/spawn.rs @@ -0,0 +1,225 @@ +use bevy_reflect::TypePath; +use criterion::{criterion_group, Criterion}; +use std::{path::Path, time::Duration}; + +use bevy_app::App; +use bevy_asset::{ + io::{ + memory::{Dir, MemoryAssetReader}, + AssetSourceBuilder, AssetSourceId, + }, + AssetApp, AssetLoader, AssetServer, Assets, +}; +use bevy_ecs::prelude::*; +use bevy_scene2::{prelude::*, ScenePatch}; +use bevy_ui::prelude::*; + +criterion_group!(benches, spawn); + +fn ui() -> impl Scene { + bsn! { + Node + Children [ + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + (:button Node { width: Val::Px(200.) }), + ] + } +} + +fn ui_loaded_asset() -> impl Scene { + bsn! { + Node + Children [ + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + (:"button.bsn" Node { width: Val::Px(200.) }), + ] + } +} + +// A non-Node component that we add to force archetype moves, inflating their cost if/when they happen +#[derive(Component, Default, Clone)] +struct Marker; + +fn button() -> impl Scene { + bsn! { + Button + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + } + Children [ + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + (Text("Text") Marker), + ] + } +} + +fn raw_button() -> impl Bundle { + ( + Button, + Node { + width: Val::Px(200.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..Default::default() + }, + children![ + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + (Text("Text".into()), Marker), + ], + ) +} + +fn raw_ui() -> impl Bundle { + ( + Node::default(), + children![ + raw_button(), + raw_button(), + raw_button(), + raw_button(), + raw_button(), + raw_button(), + raw_button(), + raw_button(), + raw_button(), + raw_button(), + ], + ) +} + +/// Fork of `bevy_asset::tests::run_app_until`. +fn run_app_until(app: &mut App, mut predicate: impl FnMut() -> bool) { + const LARGE_ITERATION_COUNT: usize = 10000; + for _ in 0..LARGE_ITERATION_COUNT { + app.update(); + if predicate() { + return; + } + } + + panic!("Ran out of loops to return `Some` from `predicate`"); +} + +fn spawn(c: &mut Criterion) { + let mut group = c.benchmark_group("spawn"); + group.warm_up_time(Duration::from_millis(500)); + group.measurement_time(Duration::from_secs(4)); + group.bench_function("ui_immediate_function_scene", |b| { + let mut app = App::new(); + app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene2::ScenePlugin)); + + b.iter(move || { + app.world_mut().spawn_scene(ui()).unwrap(); + }); + }); + group.bench_function("ui_immediate_loaded_scene", |b| { + let mut app = App::new(); + let dir = Dir::default(); + let dir_clone = dir.clone(); + app.register_asset_source( + AssetSourceId::Default, + AssetSourceBuilder::new(move || { + Box::new(MemoryAssetReader { + root: dir_clone.clone(), + }) + }), + ); + app.add_plugins(( + bevy_app::TaskPoolPlugin::default(), + bevy_asset::AssetPlugin::default(), + bevy_scene2::ScenePlugin, + )); + app.finish(); + app.cleanup(); + + // Create a fake loader to act as a ScenePatch loaded from a file. + app.register_asset_loader(FakeSceneLoader); + + #[derive(TypePath)] + struct FakeSceneLoader; + + impl AssetLoader for FakeSceneLoader { + type Asset = ScenePatch; + type Error = std::io::Error; + type Settings = (); + + async fn load( + &self, + _reader: &mut dyn bevy_asset::io::Reader, + _settings: &Self::Settings, + load_context: &mut bevy_asset::LoadContext<'_>, + ) -> Result { + Ok(ScenePatch::load_with(load_context, button())) + } + } + + // Insert an asset that the fake loader can fake read. + dir.insert_asset_text(Path::new("button.bsn"), ""); + + let asset_server = app.world().resource::().clone(); + let handle = asset_server.load("button.bsn"); + assert!(app.world().get_resource::>().is_some()); + + run_app_until(&mut app, || asset_server.is_loaded(&handle)); + + let patch = app + .world() + .resource::>() + .get(&handle) + .unwrap(); + assert!(patch.resolved.is_some()); + + b.iter(move || { + app.world_mut().spawn_scene(ui_loaded_asset()).unwrap(); + }); + + drop(handle); + }); + group.bench_function("ui_raw_bundle_no_scene", |b| { + let mut app = App::new(); + app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene2::ScenePlugin)); + + b.iter(move || { + app.world_mut().spawn(raw_ui()); + }); + }); + group.finish(); +} diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 941f871f23..7dff597252 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -199,6 +199,15 @@ pub struct Last; #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] pub struct AnimationSystems; +/// Set enum for the systems relating to scene spawning. +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub enum SceneSpawnerSystems { + /// Bevy's original scene system. + SceneSpawn, + /// Bevy's next-generation scene system + Scene2Spawn, +} + /// Defines the schedules to be run for the [`Main`] schedule, including /// their order. #[derive(Resource, Debug)] diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index be85903790..044b33c892 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -1,9 +1,11 @@ use crate::{ - meta::MetaTransform, Asset, AssetId, AssetIndex, AssetIndexAllocator, AssetPath, + meta::MetaTransform, Asset, AssetId, AssetIndex, AssetIndexAllocator, AssetPath, AssetServer, ErasedAssetIndex, ReflectHandle, UntypedAssetId, }; use alloc::sync::Arc; -use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; +use bevy_ecs::template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext}; +use bevy_platform::collections::Equivalent; +use bevy_reflect::{Reflect, TypePath}; use core::{ any::TypeId, hash::{Hash, Hasher}, @@ -128,7 +130,7 @@ impl core::fmt::Debug for StrongHandle { /// /// [`Handle::Strong`], via [`StrongHandle`] also provides access to useful [`Asset`] metadata, such as the [`AssetPath`] (if it exists). #[derive(Reflect)] -#[reflect(Default, Debug, Hash, PartialEq, Clone, Handle)] +#[reflect(Debug, Hash, PartialEq, Clone, Handle)] pub enum Handle { /// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. @@ -196,6 +198,55 @@ impl Default for Handle { } } +// This enables FromTemplate specialization for `Handle` using the +// ["auto trait specialization" trick](https://github.com/coolcatcoder/rust_techniques/issues/1) +// This enables Handle to implement Default _and_ implement FromTemplate, without conflicting with the +// blanket impl of FromTemplate for T: Default + Clone. +impl Unpin for Handle where for<'a> [()]: SpecializeFromTemplate {} + +impl FromTemplate for Handle { + type Template = HandleTemplate; +} + +pub enum HandleTemplate { + Path(AssetPath<'static>), + Handle(Handle), +} + +impl Default for HandleTemplate { + fn default() -> Self { + Self::Path(Default::default()) + } +} + +impl>, T: Asset> From for HandleTemplate { + fn from(value: I) -> Self { + Self::Path(value.into()) + } +} + +impl From> for HandleTemplate { + fn from(value: Handle) -> Self { + Self::Handle(value) + } +} + +impl Template for HandleTemplate { + type Output = Handle; + fn build_template(&self, context: &mut TemplateContext) -> bevy_ecs::error::Result> { + Ok(match self { + HandleTemplate::Path(asset_path) => context.resource::().load(asset_path), + HandleTemplate::Handle(handle) => handle.clone(), + }) + } + + fn clone_template(&self) -> Self { + match self { + HandleTemplate::Path(asset_path) => HandleTemplate::Path(asset_path.clone()), + HandleTemplate::Handle(handle) => HandleTemplate::Handle(handle.clone()), + } + } +} impl core::fmt::Debug for Handle { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let name = ShortName::of::(); @@ -219,6 +270,13 @@ impl Hash for Handle { } } +// Handle uses AssetId when hashing. This enables using AssetId instead of handle with hashsets and hashmaps. +impl Equivalent> for AssetId { + fn equivalent(&self, key: &Handle) -> bool { + *self == key.id() + } +} + impl PartialOrd for Handle { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 6626bdfc12..98f905af37 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -498,6 +498,12 @@ impl VisitAssetDependencies for Option { } } +impl VisitAssetDependencies for UntypedAssetId { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + visit(*self); + } +} + impl VisitAssetDependencies for [Handle; N] { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { @@ -514,34 +520,18 @@ impl VisitAssetDependencies for [UntypedHandle; N] { } } -impl VisitAssetDependencies for Vec> { +impl VisitAssetDependencies for Vec { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { - visit(dependency.id().untyped()); + dependency.visit_dependencies(visit); } } } -impl VisitAssetDependencies for Vec { +impl VisitAssetDependencies for HashSet { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { - visit(dependency.id()); - } - } -} - -impl VisitAssetDependencies for HashSet> { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { - for dependency in self { - visit(dependency.id().untyped()); - } - } -} - -impl VisitAssetDependencies for HashSet { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { - for dependency in self { - visit(dependency.id()); + dependency.visit_dependencies(visit); } } } diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index cdb39ddb0e..b7c569b153 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -423,6 +423,16 @@ impl LoadFromPath for AssetServer { } } +impl LoadFromPath for &AssetServer { + fn load_from_path_erased( + &mut self, + type_id: TypeId, + path: AssetPath<'static>, + ) -> UntypedHandle { + self.load_erased(type_id, path) + } +} + /// A [`ReflectDeserializerProcessor`] that manually deserializes [`Handle`] and [`UntypedHandle`], /// and passes through for all other types. /// diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index cc4d7dadc7..bdbeb8fef6 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -76,6 +76,7 @@ std = [ "arrayvec/std", "log/std", "bevy_platform/std", + "downcast-rs/std", ] ## `critical-section` provides the building blocks for synchronization primitives @@ -126,6 +127,7 @@ log = { version = "0.4", default-features = false } bumpalo = "3" subsecond = { version = "0.7.0-rc.0", optional = true } slotmap = { version = "1.0.7", default-features = false } +downcast-rs = { version = "2", default-features = false } concurrent-queue = { version = "2.5.0", default-features = false } [target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies] diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 19ecef332d..cfcb35883b 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -9,6 +9,8 @@ mod event; mod message; mod query_data; mod query_filter; +mod template; +mod variant_defaults; mod world_query; use crate::{ @@ -775,3 +777,15 @@ pub fn derive_from_world(input: TokenStream) -> TokenStream { } }) } + +/// Derives `FromTemplate`. +#[proc_macro_derive(FromTemplate, attributes(template, default))] +pub fn derive_from_template(input: TokenStream) -> TokenStream { + template::derive_from_template(input) +} + +/// Derives `VariantDefaults`. +#[proc_macro_derive(VariantDefaults)] +pub fn derive_variant_defaults(input: TokenStream) -> TokenStream { + variant_defaults::derive_variant_defaults(input) +} diff --git a/crates/bevy_ecs/macros/src/template.rs b/crates/bevy_ecs/macros/src/template.rs new file mode 100644 index 0000000000..91b3b82d72 --- /dev/null +++ b/crates/bevy_ecs/macros/src/template.rs @@ -0,0 +1,415 @@ +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, Data, DeriveInput, + Fields, FieldsUnnamed, Index, Path, Result, Token, WhereClause, +}; + +const TEMPLATE_DEFAULT_ATTRIBUTE: &str = "default"; +const TEMPLATE_ATTRIBUTE: &str = "template"; + +pub(crate) fn derive_from_template(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs = BevyManifest::shared(|manifest| manifest.get_path("bevy_ecs")); + + let type_ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + let template_ident = format_ident!("{type_ident}Template"); + + let is_pub = matches!(ast.vis, syn::Visibility::Public(_)); + let maybe_pub = if is_pub { quote!(pub) } else { quote!() }; + + let template = match &ast.data { + Data::Struct(data_struct) => { + let result = match struct_impl(&data_struct.fields, &bevy_ecs, false) { + Ok(result) => result, + Err(err) => return err.into_compile_error().into(), + }; + let StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + template_field_clones, + .. + } = result; + match &data_struct.fields { + Fields::Named(_) => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident #impl_generics #where_clause { + #(#template_fields,)* + } + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build_template(&self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(#type_ident { + #(#template_field_builds,)* + }) + } + + fn clone_template(&self) -> Self { + Self { + #(#template_field_clones,)* + } + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self { + #(#template_field_defaults,)* + } + } + } + } + } + Fields::Unnamed(_) => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident #impl_generics ( + #(#template_fields,)* + ) #where_clause; + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build_template(&self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(#type_ident ( + #(#template_field_builds,)* + )) + } + + fn clone_template(&self) -> Self { + Self( + #(#template_field_clones,)* + ) + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self ( + #(#template_field_defaults,)* + ) + } + } + } + } + Fields::Unit => { + quote! { + #[allow(missing_docs)] + #maybe_pub struct #template_ident; + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident; + fn build_template(&self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(#type_ident) + } + + fn clone_template(&self) -> Self { + Self + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + Self + } + } + } + } + } + } + Data::Enum(data_enum) => { + let mut variant_definitions = Vec::new(); + let mut variant_builds = Vec::new(); + let mut variant_clones = Vec::new(); + let mut variant_default_ident = None; + let mut variant_defaults = Vec::new(); + for variant in &data_enum.variants { + let result = match struct_impl(&variant.fields, &bevy_ecs, true) { + Ok(result) => result, + Err(err) => return err.into_compile_error().into(), + }; + let StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + template_field_clones, + .. + } = result; + + let is_default = variant + .attrs + .iter() + .any(|a| a.path().is_ident(TEMPLATE_DEFAULT_ATTRIBUTE)); + if is_default && variant_default_ident.is_some() { + panic!("Cannot have multiple default variants"); + } + let variant_ident = &variant.ident; + let variant_name_lower = variant_ident.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &variant.fields { + Fields::Named(fields) => { + variant_definitions.push(quote! { + #variant_ident { + #(#template_fields,)* + } + }); + let field_idents = fields.named.iter().map(|f| &f.ident); + variant_builds.push(quote! { + // TODO: proper assignments here + #template_ident::#variant_ident { + #(#field_idents,)* + } => { + #type_ident::#variant_ident { + #(#template_field_builds,)* + } + } + }); + + let field_idents = fields.named.iter().map(|f| &f.ident); + variant_clones.push(quote! { + // TODO: proper assignments here + #template_ident::#variant_ident { + #(#field_idents,)* + } => { + #template_ident::#variant_ident { + #(#template_field_clones,)* + } + } + }); + + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident { + #(#template_field_defaults,)* + } + }); + } + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident { + #(#template_field_defaults,)* + } + } + }); + } + Fields::Unnamed(FieldsUnnamed { unnamed: f, .. }) => { + let field_idents = f + .iter() + .enumerate() + .map(|(i, _)| format_ident!("t{}", i)) + .collect::>(); + variant_definitions.push(quote! { + #variant_ident(#(#template_fields,)*) + }); + variant_builds.push(quote! { + // TODO: proper assignments here + #template_ident::#variant_ident( + #(#field_idents,)* + ) => { + #type_ident::#variant_ident( + #(#template_field_builds,)* + ) + } + }); + variant_clones.push(quote! { + #template_ident::#variant_ident( + #(#field_idents,)* + ) => { + #template_ident::#variant_ident( + #(#template_field_clones,)* + ) + } + }); + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident( + #(#template_field_defaults,)* + ) + }); + } + + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident( + #(#template_field_defaults,)* + ) + } + }); + } + Fields::Unit => { + variant_definitions.push(quote! {#variant_ident}); + variant_builds.push( + quote! {#template_ident::#variant_ident => #type_ident::#variant_ident}, + ); + variant_clones.push( + quote! {#template_ident::#variant_ident => #template_ident::#variant_ident}, + ); + if is_default { + variant_default_ident = Some(quote! { + Self::#variant_ident + }); + } + variant_defaults.push(quote! { + #maybe_pub fn #variant_default_name() -> Self { + Self::#variant_ident + } + }); + } + } + } + + if variant_default_ident.is_none() { + panic!("Deriving Template for enums requires picking a default variant using #[default]"); + } + + quote! { + #[allow(missing_docs)] + #maybe_pub enum #template_ident #type_generics #where_clause { + #(#variant_definitions,)* + } + + impl #impl_generics #template_ident #type_generics #where_clause { + #(#variant_defaults)* + } + + impl #impl_generics #bevy_ecs::template::Template for #template_ident #type_generics #where_clause { + type Output = #type_ident #type_generics; + fn build_template(&self, context: &mut #bevy_ecs::template::TemplateContext) -> #bevy_ecs::error::Result { + Ok(match self { + #(#variant_builds,)* + }) + } + + fn clone_template(&self) -> Self { + match self { + #(#variant_clones,)* + } + } + } + + impl #impl_generics Default for #template_ident #type_generics #where_clause { + fn default() -> Self { + #variant_default_ident + } + } + } + } + Data::Union(_) => panic!("Union types are not supported yet."), + }; + + let mut unpin_where_clause = where_clause.cloned().unwrap_or_else(|| WhereClause { + where_token: ::default(), + predicates: Punctuated::new(), + }); + + unpin_where_clause + .predicates + .push(parse_quote! { for<'a> [()]: #bevy_ecs::template::SpecializeFromTemplate }); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs::template::FromTemplate for #type_ident #type_generics #where_clause { + type Template = #template_ident #type_generics; + } + + impl #impl_generics Unpin for #type_ident #type_generics #unpin_where_clause {} + + #template + }) +} + +struct StructImpl { + template_fields: Vec, + template_field_builds: Vec, + template_field_defaults: Vec, + template_field_clones: Vec, +} + +fn struct_impl(fields: &Fields, bevy_ecs: &Path, is_enum: bool) -> Result { + let mut template_fields = Vec::with_capacity(fields.len()); + let mut template_field_builds = Vec::with_capacity(fields.len()); + let mut template_field_defaults = Vec::with_capacity(fields.len()); + let mut template_field_clones = Vec::with_capacity(fields.len()); + let is_named = matches!(fields, Fields::Named(_)); + for (index, field) in fields.iter().enumerate() { + let is_pub = matches!(field.vis, syn::Visibility::Public(_)); + let field_maybe_pub = if is_pub { quote!(pub) } else { quote!() }; + let ident = &field.ident; + let ty = &field.ty; + let index = Index::from(index); + let mut template_type = None; + for attr in &field.attrs { + if attr.path().is_ident(TEMPLATE_ATTRIBUTE) { + let Ok(path) = attr.parse_args::() else { + return Err(syn::Error::new( + attr.span(), + "Expected a Template type path", + )); + }; + template_type = Some(quote!(#path)); + } + } + + if template_type.is_none() { + template_type = Some(quote!(<#ty as #bevy_ecs::template::FromTemplate>::Template)); + } + + if is_named { + template_fields.push(quote! { + #field_maybe_pub #ident: #template_type + }); + if is_enum { + template_field_builds.push(quote! { + #ident: #ident.build_template(context)? + }); + template_field_clones.push(quote! { + #ident: #bevy_ecs::template::Template::clone_template(#ident) + }); + } else { + template_field_builds.push(quote! { + #ident: self.#ident.build_template(context)? + }); + template_field_clones.push(quote! { + #ident: #bevy_ecs::template::Template::clone_template(&self.#ident) + }); + } + + template_field_defaults.push(quote! { + #ident: Default::default() + }); + } else { + template_fields.push(quote! { + #field_maybe_pub #template_type + }); + if is_enum { + let enum_tuple_ident = format_ident!("t{}", index); + template_field_builds.push(quote! { + #enum_tuple_ident.build_template(context)? + }); + template_field_clones.push(quote! { + #bevy_ecs::template::Template::clone_template(#enum_tuple_ident) + }); + } else { + template_field_builds.push(quote! { + self.#index.build_template(context)? + }); + template_field_clones.push(quote! { + #bevy_ecs::template::Template::clone_template(&self.#index) + }); + } + template_field_defaults.push(quote! { + Default::default() + }); + } + } + Ok(StructImpl { + template_fields, + template_field_builds, + template_field_defaults, + template_field_clones, + }) +} diff --git a/crates/bevy_ecs/macros/src/variant_defaults.rs b/crates/bevy_ecs/macros/src/variant_defaults.rs new file mode 100644 index 0000000000..59639fbc4c --- /dev/null +++ b/crates/bevy_ecs/macros/src/variant_defaults.rs @@ -0,0 +1,60 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput}; + +pub(crate) fn derive_variant_defaults(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + let Data::Enum(data_enum) = &ast.data else { + panic!("Can only derive VariantDefaults for enums"); + }; + + let mut variant_defaults = Vec::new(); + for variant in &data_enum.variants { + let variant_ident = &variant.ident; + let variant_name_lower = variant_ident.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &variant.fields { + syn::Fields::Named(fields_named) => { + let fields = fields_named.named.iter().map(|f| &f.ident); + variant_defaults.push(quote! { + #[allow(missing_docs)] + pub fn #variant_default_name() -> Self { + Self::#variant_ident { + #(#fields: Default::default(),)* + } + } + }); + } + syn::Fields::Unnamed(fields_unnamed) => { + let fields = fields_unnamed + .unnamed + .iter() + .map(|_| quote! {Default::default()}); + variant_defaults.push(quote! { + #[allow(missing_docs)] + pub fn #variant_default_name() -> Self { + Self::#variant_ident( + #(#fields,)* + ) + } + }); + } + syn::Fields::Unit => { + variant_defaults.push(quote! { + #[allow(missing_docs)] + pub fn #variant_default_name() -> Self { + Self::#variant_ident + } + }); + } + } + } + + TokenStream::from(quote! { + impl #impl_generics #type_ident #type_generics #where_clause { + #(#variant_defaults)* + } + }) +} diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 15f22500f3..a0f9aea277 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -84,13 +84,8 @@ mod clone_entities; mod entity_set; mod map_entities; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; -#[cfg(all(feature = "bevy_reflect", feature = "serialize"))] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; pub use clone_entities::*; -use derive_more::derive::Display; pub use entity_set::*; pub use map_entities::*; @@ -116,7 +111,6 @@ pub mod unique_array; pub mod unique_slice; pub mod unique_vec; -use nonmax::NonMaxU32; pub use unique_array::{UniqueEntityArray, UniqueEntityEquivalentArray}; pub use unique_slice::{UniqueEntityEquivalentSlice, UniqueEntitySlice}; pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; @@ -128,8 +122,14 @@ use crate::{ }; use alloc::vec::Vec; use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; +use derive_more::derive::Display; use log::warn; +use nonmax::NonMaxU32; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +#[cfg(all(feature = "bevy_reflect", feature = "serialize"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index b84074a102..f26f9aa253 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -52,6 +52,7 @@ pub mod schedule; pub mod spawn; pub mod storage; pub mod system; +pub mod template; pub mod traversal; pub mod world; @@ -97,6 +98,7 @@ pub mod prelude { Res, ResMut, Single, System, SystemIn, SystemInput, SystemParamBuilder, SystemParamFunction, }, + template::{FromTemplate, Template}, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, FromWorld, World, @@ -119,6 +121,8 @@ pub mod prelude { pub use crate::reflect::AppFunctionRegistry; } +pub use bevy_ecs_macros::VariantDefaults; + /// Exports used by macros. /// /// These are not meant to be used directly and are subject to breaking changes. diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 2887475cc6..7218815428 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -6,9 +6,9 @@ use alloc::{ borrow::{Cow, ToOwned}, string::String, }; -use bevy_platform::hash::FixedHasher; +use bevy_platform::hash::Hashed; use core::{ - hash::{BuildHasher, Hash, Hasher}, + hash::{Hash, Hasher}, ops::Deref, }; @@ -47,10 +47,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; all(feature = "serialize", feature = "bevy_reflect"), reflect(Deserialize, Serialize) )] -pub struct Name { - hash: u64, // Won't be serialized - name: Cow<'static, str>, -} +pub struct Name(pub HashedStr); impl Default for Name { fn default() -> Self { @@ -58,15 +55,29 @@ impl Default for Name { } } +/// A wrapper over Hashed. This exists to make Name("value".into()) possible, which plays nicely with contexts like the `bsn!` macro. +#[derive(Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct HashedStr(Hashed>); + +impl From<&'static str> for HashedStr { + fn from(value: &'static str) -> Self { + Self(Hashed::new(Cow::Borrowed(value))) + } +} + +impl From for HashedStr { + fn from(value: String) -> Self { + Self(Hashed::new(Cow::Owned(value))) + } +} + impl Name { /// Creates a new [`Name`] from any string-like type. /// /// The internal hash will be computed immediately. pub fn new(name: impl Into>) -> Self { - let name = name.into(); - let mut name = Name { name, hash: 0 }; - name.update_hash(); - name + Self(HashedStr(Hashed::new(name.into()))) } /// Sets the entity's name. @@ -82,33 +93,37 @@ impl Name { /// This will allocate a new string if the name was previously /// created from a borrow. #[inline(always)] - pub fn mutate(&mut self, f: F) { - f(self.name.to_mut()); - self.update_hash(); + pub fn mutate(&mut self, func: impl FnOnce(&mut String)) { + self.0 .0.mutate(|cow_str| match cow_str { + Cow::Borrowed(borrowed) => { + let mut owned = borrowed.to_owned(); + func(&mut owned); + *cow_str = Cow::Owned(owned); + } + Cow::Owned(owned) => { + func(owned); + } + }); } /// Gets the name of the entity as a `&str`. #[inline(always)] pub fn as_str(&self) -> &str { - &self.name - } - - fn update_hash(&mut self) { - self.hash = FixedHasher.hash_one(&self.name); + &self.0 .0 } } impl core::fmt::Display for Name { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt(&self.name, f) + core::fmt::Display::fmt(&*self.0 .0, f) } } impl core::fmt::Debug for Name { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Debug::fmt(&self.name, f) + core::fmt::Debug::fmt(&self.0 .0, f) } } @@ -172,7 +187,7 @@ impl From for Name { impl AsRef for Name { #[inline(always)] fn as_ref(&self) -> &str { - &self.name + &self.0 .0 } } @@ -186,24 +201,24 @@ impl From<&Name> for String { impl From for String { #[inline(always)] fn from(val: Name) -> String { - val.name.into_owned() + val.as_str().to_owned() } } impl Hash for Name { fn hash(&self, state: &mut H) { - self.name.hash(state); + Hash::hash(&self.0 .0, state); } } impl PartialEq for Name { fn eq(&self, other: &Self) -> bool { - if self.hash != other.hash { + if self.0 .0.hash() != other.0 .0.hash() { // Makes the common case of two strings not been equal very fast return false; } - self.name.eq(&other.name) + self.0 .0.eq(&other.0 .0) } } @@ -217,7 +232,7 @@ impl PartialOrd for Name { impl Ord for Name { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.name.cmp(&other.name) + self.0 .0.cmp(&other.0 .0) } } @@ -225,7 +240,7 @@ impl Deref for Name { type Target = str; fn deref(&self) -> &Self::Target { - self.name.as_ref() + self.as_str() } } diff --git a/crates/bevy_ecs/src/template.rs b/crates/bevy_ecs/src/template.rs new file mode 100644 index 0000000000..31a6d77472 --- /dev/null +++ b/crates/bevy_ecs/src/template.rs @@ -0,0 +1,456 @@ +//! Functionality that relates to the [`Template`] trait. + +pub use bevy_ecs_macros::FromTemplate; + +use crate::{ + bundle::Bundle, + entity::Entity, + error::{BevyError, Result}, + resource::Resource, + world::{EntityWorldMut, Mut, World}, +}; +use alloc::{boxed::Box, vec, vec::Vec}; +use downcast_rs::{impl_downcast, Downcast}; +use variadics_please::all_tuples; + +/// A [`Template`] is something that, given a spawn context (target [`Entity`], [`World`], etc), can produce a [`Template::Output`]. +/// +/// [`Template`] is the cornerstone of scene systems. It enables define types (and hierarchies) that require no [`World`] or [`Entity`] context to define, +/// but can _use_ that context to produce the final runtime state. A [`Template`] is notably: +/// * **Repeatable**: Building a [`Template`] does not consume it. This enables reusing "baked" scenes / avoids rebuilding scenes each time we want to spawn one. +/// * **Clone-able**: Templates can be duplicated via [`Template::clone_template`], enabling scenes to be duplicated, supporting copy-on-write behaviors, etc. +/// * **(Often) Serializable**: Templates are intended to be easily serialized and deserialized, as they are typically composed of raw data. +/// +/// Asset handles and [`Entity`] are two commonly [`Template`]-ed types. Asset handles are often "loaded" from an "asset path". The "asset path" would be the [`Template`]. +/// Likewise [`Entity`] on its own has no reasonable default. A type with an [`Entity`] reference could use an "entity path" template to point to a specific entity, relative +/// to the current spawn context. +/// +/// See [`FromTemplate`], which defines the canonical [`Template`] for a type. This can be derived, which will generate a [`Template`] for the deriving type. +pub trait Template { + /// The type of value produced by this [`Template`]. + type Output; + + /// Uses this template and the given `entity` context to produce a [`Template::Output`]. + fn build_template(&self, context: &mut TemplateContext) -> Result; + + /// Clones this template. See [`Clone`]. + fn clone_template(&self) -> Self; +} + +/// The context used to apply the current [`Template`]. This contains a reference to the entity that the template is being +/// applied to (via an [`EntityWorldMut`]). +pub struct TemplateContext<'a, 'w> { + /// The current entity the template is being applied to + pub entity: &'a mut EntityWorldMut<'w>, + /// The scoped entities mapping for the current template context + pub scoped_entities: &'a mut ScopedEntities, + /// The entity scopes for the current template context. This matches + /// the `scoped_entities`. + pub entity_scopes: &'a EntityScopes, +} + +impl<'a, 'w> TemplateContext<'a, 'w> { + /// Creates a new [`TemplateContext`]. + pub fn new( + entity: &'a mut EntityWorldMut<'w>, + scoped_entities: &'a mut ScopedEntities, + entity_scopes: &'a EntityScopes, + ) -> Self { + Self { + entity, + scoped_entities, + entity_scopes, + } + } + + /// Retrieves the scoped entity if it has already been spawned, and spawns a new entity if it has not + /// yet been spawned. + pub fn get_scoped_entity(&mut self, scoped_entity_index: ScopedEntityIndex) -> Entity { + self.scoped_entities.get( + // SAFETY: this only uses the world to spawn an empty entity + unsafe { self.entity.world_mut() }, + self.entity_scopes, + scoped_entity_index, + ) + } + + /// Retrieves a reference to the given resource `R`. + #[inline] + pub fn resource(&self) -> &R { + self.entity.resource() + } + + /// Retrieves a mutable reference to the given resource `R`. + #[inline] + pub fn resource_mut(&mut self) -> Mut<'_, R> { + self.entity.resource_mut() + } +} + +/// A mapping from from an entity reference's (scope, index) to a contiguous flat index that uniquely +/// identifies the entity within a scene. +#[derive(Default, Debug)] +pub struct EntityScopes { + scopes: Vec>>, + next_index: usize, +} + +impl EntityScopes { + /// The number of entities defined across all scopes. + #[inline] + pub fn entity_len(&self) -> usize { + self.next_index + } + + /// Allocate a new contiguous entity index for the given (scope, index) pair. + pub fn alloc(&mut self, scoped_entity_index: ScopedEntityIndex) { + *self.get_mut(scoped_entity_index) = Some(self.next_index); + self.next_index += 1; + } + + /// Assign an existing contiguous entity index for the given (scope, index) pair. + /// This is generally used when there are multiple (scope, index) pairs that point + /// to the same entity (ex: scene inheritance). + pub fn assign(&mut self, scoped_entity_index: ScopedEntityIndex, value: usize) { + let option = self.get_mut(scoped_entity_index); + *option = Some(value); + } + + #[expect(unsafe_code, reason = "Easily verifiable performance optimization")] + fn get_mut(&mut self, scoped_entity_index: ScopedEntityIndex) -> &mut Option { + // NOTE: this is ok because PatchContext::new_scope adds scopes as they are created. + // this shouldn't panic unless internals are broken. + let indices = &mut self.scopes[scoped_entity_index.scope]; + if scoped_entity_index.index >= indices.len() { + indices.resize_with(scoped_entity_index.index + 1, || None); + } + // SAFETY: just allocated above + unsafe { indices.get_unchecked_mut(scoped_entity_index.index) } + } + + /// Gets the assigned contiguous entity index for the given (scope, index) pair + pub fn get(&self, scoped_entity_index: ScopedEntityIndex) -> Option { + *self + .scopes + .get(scoped_entity_index.scope)? + .get(scoped_entity_index.index)? + } + + /// Creates a new scope and returns it. + pub fn add_scope(&mut self) -> usize { + let scope_index = self.scopes.len(); + self.scopes.push(Vec::default()); + scope_index + } +} + +/// A contiguous list of entities identified by their index in the list. +#[derive(Debug)] +pub struct ScopedEntities(Vec>); + +impl ScopedEntities { + /// Creates a new [`ScopedEntities`] with the given `size`, initialized to [`None`] (no [`Entity`] assigned). + pub fn new(size: usize) -> Self { + Self(vec![None; size]) + } +} + +impl ScopedEntities { + /// Gets the [`Entity`] assigned to the given (scope, index) pair, if it exists, and spawns a new entity if + /// it does not. + pub fn get( + &mut self, + world: &mut World, + entity_scopes: &EntityScopes, + scoped_entity_index: ScopedEntityIndex, + ) -> Entity { + let index = entity_scopes.get(scoped_entity_index).unwrap(); + *self.0[index].get_or_insert_with(|| world.spawn_empty().id()) + } + + /// Assigns the given `entity` to the (scope, index) pair. + pub fn set( + &mut self, + entity_scopes: &EntityScopes, + scoped_entity_index: ScopedEntityIndex, + entity: Entity, + ) { + let index = entity_scopes.get(scoped_entity_index).unwrap(); + self.0[index] = Some(entity); + } +} + +/// [`FromTemplate`] is implemented for types that can be produced by a specific, canonical [`Template`]. This creates a way to correlate to the [`Template`] using the +/// desired template output type. This is used by Bevy's scene system. +/// +/// Both [`FromTemplate`] and [`Template`] are blanket implemented for types that implement [`Default`] and [`Clone`], meaning most types you would want to use +/// _already have templates_. +/// +/// It is best to think of [`FromTemplate`] as an alternative to [`Default`] for types that require world/spawn context to instantiate. Note that because of the blanket +/// impl, you cannot implement [`FromTemplate`], [`Default`], and [`Clone`] together on the same type, as it would result in two conflicting [`FromTemplate`] impls. +/// This is also why [`Template`] has its own [`Template::clone_template`] method (to avoid using the [`Clone`] impl, which would pull in the auto-impl). +/// +/// You can _and should_ prefer deriving [`Default`] and [`Clone`] instead of an explicit [`FromTemplate`] impl, unless your type uses something that requires (or uses) +/// a [`Template`]. Handles in an asset system or [`Entity`] are examples of "templated" types. If you want your type to support templates of them, you probably want +/// to derive [`FromTemplate`]. +/// +/// [`FromTemplate`] can be derived for types whose fields _also_ implement [`FromTemplate`]: +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Default, Clone)] +/// # struct Handle(core::marker::PhantomData); +/// # #[derive(Default, Clone)] +/// # struct Image; +/// #[derive(FromTemplate)] +/// struct Player { +/// image: Handle +/// } +/// ``` +/// +/// Deriving [`FromTemplate`] will generate a [`Template`] type for the deriving type. The example above would generate a `PlayerTemplate` like this: +/// ``` +/// # use bevy_ecs::{prelude::*, template::TemplateContext}; +/// # #[derive(FromTemplate)] +/// # struct Handle(core::marker::PhantomData); +/// # #[derive(Default, Clone)] +/// # struct Image; +/// struct Player { +/// image: Handle +/// } +/// +/// impl FromTemplate for Player { +/// type Template = PlayerTemplate; +/// } +/// +/// struct PlayerTemplate { +/// image: HandleTemplate, +/// } +/// +/// impl Template for PlayerTemplate { +/// type Output = Player; +/// fn build_template(&self, context: &mut TemplateContext) -> Result { +/// Ok(Player { +/// image: self.image.build_template(context)?, +/// }) +/// } +/// +/// fn clone_template(&self) -> Self { +/// PlayerTemplate { +/// image: self.image.clone_template(), +/// } +/// } +/// } +/// ``` +/// +/// [`FromTemplate`] derives can specify custom templates to use instead of a canonical [`FromTemplate`]: +/// ``` +/// # use bevy_ecs::{prelude::*, template::TemplateContext}; +/// # struct Image; +/// #[derive(FromTemplate)] +/// struct Counter { +/// #[template(Always10)] +/// count: usize +/// } +/// +/// #[derive(Default)] +/// struct Always10; +/// +/// impl Template for Always10 { +/// type Output = usize; +/// +/// fn build_template(&self, context: &mut TemplateContext) -> Result { +/// Ok(10) +/// } +/// +/// fn clone_template(&self) -> Self { +/// Always10 +/// } +/// } +/// ``` +pub trait FromTemplate: Sized { + /// The [`Template`] for this type. + type Template: Template; +} + +macro_rules! template_impl { + ($($template: ident),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + impl<$($template: Template),*> Template for TemplateTuple<($($template,)*)> { + type Output = ($($template::Output,)*); + fn build_template(&self, _context: &mut TemplateContext) -> Result { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($template,)*) = &self.0; + Ok(($($template.build_template(_context)?,)*)) + } + + fn clone_template(&self) -> Self { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($template,)*) = &self.0; + TemplateTuple(($($template.clone_template(),)*)) + } + } + } +} + +/// A wrapper over a tuple of [`Template`] implementations, which also implements [`Template`]. This exists because [`Template`] cannot +/// be directly implemented for tuples of [`Template`] implementations. +pub struct TemplateTuple(pub T); + +all_tuples!(template_impl, 0, 12, T); + +// This includes `Unpin` to enable specialization for Templates that also implement Default, by using the +// ["auto trait specialization" trick](https://github.com/coolcatcoder/rust_techniques/issues/1) +impl Template for T { + type Output = T; + + fn build_template(&self, _context: &mut TemplateContext) -> Result { + Ok(self.clone()) + } + + fn clone_template(&self) -> Self { + self.clone() + } +} + +// This includes `Unpin` to enable specialization for Templates that also implement Default, by using the +// ["auto trait specialization" trick](https://github.com/coolcatcoder/rust_techniques/issues/1) +impl FromTemplate for T { + type Template = T; +} + +/// This is used to help improve error messages related to [`FromTemplate`] specialization. Developers should generally just ignore +/// this trait and read the error message when they encounter it. +#[diagnostic::on_unimplemented( + message = "This type does not manually implement FromTemplate, and it must. If you are deriving FromTemplate and you see this, it is likely because \ + a field does not have a FromTemplate impl. This can usually be fixed by using a custom template for that field. \ + Ex: for an Option> field, annotate the field with `#[template(OptionTemplate>)]`", + note = "FromTemplate currently uses pseudo-specialization to enable FromTemplate to override Default. This error message is a consequence of t." +)] +pub trait SpecializeFromTemplate: Sized {} + +/// A [`Template`] reference to an [`Entity`]. +pub enum EntityReference { + /// A reference to an entity via a [`ScopedEntityIndex`] + ScopedEntityIndex(ScopedEntityIndex), +} + +/// An entity index within the current [`TemplateContext`], which is defined by a scope +/// and an index. This references a specific (and sometimes yet-to-be-spawned) entity defined +/// within a given scope. +/// +/// In most cases this is initialized by the scene system and should not be initialized manually. +/// Scopes must be defined ahead of time on the [`TemplateContext`]. +#[derive(Copy, Clone, Debug)] +pub struct ScopedEntityIndex { + /// The scope of the entity index. This must be defined ahead of time. + pub scope: usize, + /// The index that uniquely identifies the entity within the current scope. + pub index: usize, +} + +impl Default for EntityReference { + fn default() -> Self { + Self::ScopedEntityIndex(ScopedEntityIndex { scope: 0, index: 0 }) + } +} + +impl Template for EntityReference { + type Output = Entity; + + fn build_template(&self, context: &mut TemplateContext) -> Result { + Ok(match self { + EntityReference::ScopedEntityIndex(scoped_entity_index) => { + context.get_scoped_entity(*scoped_entity_index) + } + }) + } + + fn clone_template(&self) -> Self { + match self { + Self::ScopedEntityIndex(scoped_entity_index) => { + Self::ScopedEntityIndex(*scoped_entity_index) + } + } + } +} + +impl FromTemplate for Entity { + type Template = EntityReference; +} + +/// A type-erased, object-safe, downcastable version of [`Template`]. +pub trait ErasedTemplate: Downcast + Send + Sync { + /// Applies this template to the given `entity`. + fn apply(&self, context: &mut TemplateContext) -> Result<(), BevyError>; + + /// Clones this template. See [`Clone`]. + fn clone_template(&self) -> Box; +} + +impl_downcast!(ErasedTemplate); + +impl + Send + Sync + 'static> ErasedTemplate for T { + fn apply(&self, context: &mut TemplateContext) -> Result<(), BevyError> { + let bundle = self.build_template(context)?; + context.entity.insert(bundle); + Ok(()) + } + + fn clone_template(&self) -> Box { + Box::new(Template::clone_template(self)) + } +} + +/// A [`Template`] driven by a function that returns an output. This is used to create "free floating" templates without +/// defining a new type. See [`template`] for usage. +pub struct FnTemplate Result, O>(pub F); + +impl Result + Clone, O> Template for FnTemplate { + type Output = O; + + fn build_template(&self, context: &mut TemplateContext) -> Result { + (self.0)(context) + } + + fn clone_template(&self) -> Self { + Self(self.0.clone()) + } +} + +/// Returns a "free floating" template for a given `func`. This prevents the need to define a custom type for one-off templates. +pub fn template Result, O>(func: F) -> FnTemplate { + FnTemplate(func) +} + +/// A [`Template`] for Option. +pub struct OptionTemplate(Option); + +impl Default for OptionTemplate { + fn default() -> Self { + Self(None) + } +} + +impl Template for OptionTemplate { + type Output = Option; + + fn build_template(&self, context: &mut TemplateContext) -> Result { + Ok(match &self.0 { + Some(template) => Some(template.build_template(context)?), + None => None, + }) + } + + fn clone_template(&self) -> Self { + OptionTemplate(self.0.as_ref().map(Template::clone_template)) + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 2be61126e7..a4f5c3d13d 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -528,6 +528,7 @@ bevy_settings = { path = "../bevy_settings", optional = true, version = "0.19.0- bevy_remote = { path = "../bevy_remote", optional = true, version = "0.19.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.19.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.19.0-dev" } +bevy_scene2 = { path = "../bevy_scene2", optional = true, version = "0.19.0-dev" } bevy_solari = { path = "../bevy_solari", optional = true, version = "0.19.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.19.0-dev" } bevy_sprite_render = { path = "../bevy_sprite_render", optional = true, version = "0.19.0-dev" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 0f75594b7b..817eb9d674 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -29,6 +29,8 @@ plugin_group! { bevy_asset:::AssetPlugin, #[cfg(feature = "bevy_scene")] bevy_scene:::ScenePlugin, + #[cfg(feature = "bevy_scene2")] + bevy_scene2:::ScenePlugin, // NOTE: WinitPlugin needs to be after AssetPlugin because of custom cursors. #[cfg(feature = "bevy_winit")] bevy_winit:::WinitPlugin, diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 859ebec553..f955a5ebf2 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -78,6 +78,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_scene2")] +pub use bevy_scene2 as scene2; #[cfg(feature = "bevy_settings")] pub use bevy_settings as settings; #[cfg(feature = "bevy_shader")] diff --git a/crates/bevy_mesh/src/components.rs b/crates/bevy_mesh/src/components.rs index feb1f522a4..437f4686ae 100644 --- a/crates/bevy_mesh/src/components.rs +++ b/crates/bevy_mesh/src/components.rs @@ -3,7 +3,7 @@ use bevy_asset::{AsAssetId, AssetEvent, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChangesMut, component::Component, message::MessageReader, - reflect::ReflectComponent, system::Query, + reflect::ReflectComponent, system::Query, template::FromTemplate, }; use bevy_platform::{collections::HashSet, hash::FixedHasher}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -92,7 +92,9 @@ impl AsAssetId for Mesh2d { /// )); /// } /// ``` -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive( + Component, FromTemplate, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From, +)] #[reflect(Component, Default, Clone, PartialEq)] #[require(Transform)] pub struct Mesh3d(pub Handle); diff --git a/crates/bevy_pbr/src/mesh_material.rs b/crates/bevy_pbr/src/mesh_material.rs index 46520ccdb5..934964dda5 100644 --- a/crates/bevy_pbr/src/mesh_material.rs +++ b/crates/bevy_pbr/src/mesh_material.rs @@ -1,7 +1,7 @@ use crate::Material; use bevy_asset::{AsAssetId, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, template::FromTemplate}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use derive_more::derive::From; @@ -36,7 +36,7 @@ use derive_more::derive::From; /// )); /// } /// ``` -#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, From)] +#[derive(Component, FromTemplate, Clone, Debug, Deref, DerefMut, Reflect, From)] #[reflect(Component, Default, Clone, PartialEq)] pub struct MeshMaterial3d(pub Handle); diff --git a/crates/bevy_platform/src/hash.rs b/crates/bevy_platform/src/hash.rs index 812b88cd10..358283aa4e 100644 --- a/crates/bevy_platform/src/hash.rs +++ b/crates/bevy_platform/src/hash.rs @@ -52,6 +52,12 @@ impl Hashed { } } + /// Mutates the current value and re-computes the hash. + pub fn mutate(&mut self, func: impl FnOnce(&mut V)) { + func(&mut self.value); + self.hash = H::default().hash_one(&self.value); + } + /// The pre-computed hash. #[inline] pub fn hash(&self) -> u64 { diff --git a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs index c2a38dd5f3..3fe85255a0 100644 --- a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs +++ b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs @@ -1,5 +1,7 @@ -use bevy_reflect_derive::impl_type_path; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; impl_type_path!(::bevy_platform::hash::NoOpHash); impl_type_path!(::bevy_platform::hash::FixedHasher); impl_type_path!(::bevy_platform::hash::PassHash); + +impl_reflect_opaque!(::bevy_platform::hash::Hashed()); diff --git a/crates/bevy_scene/src/components.rs b/crates/bevy_scene/src/components.rs index b4902751f8..e6c556e6af 100644 --- a/crates/bevy_scene/src/components.rs +++ b/crates/bevy_scene/src/components.rs @@ -1,6 +1,6 @@ use bevy_asset::{AsAssetId, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{component::Component, prelude::ReflectComponent}; +use bevy_ecs::{component::Component, prelude::ReflectComponent, template::FromTemplate}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_transform::components::Transform; use derive_more::derive::From; @@ -11,7 +11,9 @@ use crate::{DynamicScene, Scene}; /// Adding this component will spawn the scene as a child of that entity. /// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. -#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[derive( + Component, FromTemplate, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From, +)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[require(Transform)] #[require(Visibility)] diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 36ef00d6eb..16c5ba3f8e 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -46,7 +46,9 @@ pub mod prelude { use bevy_app::prelude::*; #[cfg(feature = "serialize")] -use {bevy_asset::AssetApp, bevy_ecs::schedule::IntoScheduleConfigs}; +use { + bevy_app::SceneSpawnerSystems, bevy_asset::AssetApp, bevy_ecs::schedule::IntoScheduleConfigs, +}; /// Plugin that provides scene functionality to an [`App`]. #[derive(Default)] @@ -63,7 +65,7 @@ impl Plugin for ScenePlugin { SpawnScene, (scene_spawner, scene_spawner_system) .chain() - .in_set(SceneSpawnerSystems::Spawn), + .in_set(SceneSpawnerSystems::SceneSpawn), ); // Register component hooks for DynamicSceneRoot diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 41d560ddcd..2c214b42cf 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -17,7 +17,6 @@ use uuid::Uuid; use crate::{DynamicSceneRoot, SceneRoot}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::SystemSet; use bevy_ecs::{ change_detection::ResMut, prelude::{Changed, Component, Without}, @@ -58,13 +57,6 @@ impl InstanceId { } } -/// Set enum for the systems relating to scene spawning. -#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub enum SceneSpawnerSystems { - /// Systems that spawn scenes. - Spawn, -} - /// Handles spawning and despawning scenes in the world, either synchronously or batched through the [`scene_spawner_system`]. /// /// Synchronous methods: (Scene operations will take effect immediately) diff --git a/crates/bevy_scene2/Cargo.toml b/crates/bevy_scene2/Cargo.toml new file mode 100644 index 0000000000..1e78de1e62 --- /dev/null +++ b/crates/bevy_scene2/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bevy_scene2" +version = "0.19.0-dev" +edition = "2024" +description = "Provides scene functionality for Bevy Engine" +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +bevy_scene2_macros = { path = "macros", version = "0.19.0-dev" } + +bevy_app = { path = "../bevy_app", version = "0.19.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.19.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.19.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.19.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.19.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.19.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.19.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.19.0-dev" } + +thiserror = { version = "2", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std"] } +variadics_please = "1.0" +[lints] +workspace = true diff --git a/crates/bevy_scene2/LICENSE-APACHE b/crates/bevy_scene2/LICENSE-APACHE new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/crates/bevy_scene2/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_scene2/LICENSE-MIT b/crates/bevy_scene2/LICENSE-MIT new file mode 100644 index 0000000000..9cf106272a --- /dev/null +++ b/crates/bevy_scene2/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_scene2/macros/Cargo.toml b/crates/bevy_scene2/macros/Cargo.toml new file mode 100644 index 0000000000..b62e54e853 --- /dev/null +++ b/crates/bevy_scene2/macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bevy_scene2_macros" +version = "0.19.0-dev" +edition = "2024" +description = "Derive implementations for bevy_scene" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.19.0-dev" } + +syn = { version = "2.0", features = ["full", "extra-traits"] } +proc-macro2 = "1.0" +quote = "1.0" diff --git a/crates/bevy_scene2/macros/src/bsn/codegen.rs b/crates/bevy_scene2/macros/src/bsn/codegen.rs new file mode 100644 index 0000000000..eaad2fdc80 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/codegen.rs @@ -0,0 +1,483 @@ +use crate::bsn::types::{ + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnListRoot, BsnRelatedSceneList, + BsnRoot, BsnSceneListItem, BsnSceneListItems, BsnType, BsnValue, +}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use std::collections::{hash_map::Entry, HashMap}; +use syn::{Ident, Index, Lit, Member, Path}; + +#[derive(Default)] +pub(crate) struct EntityRefs { + refs: HashMap, + next_index: usize, +} + +impl EntityRefs { + fn get(&mut self, name: String) -> usize { + match self.refs.entry(name) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let index = self.next_index; + entry.insert(index); + self.next_index += 1; + index + } + } + } +} + +impl BsnRoot { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + let tokens = self + .0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); + quote! {#bevy_scene::SceneScope(#tokens)} + } +} + +impl Bsn { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + let mut entries = Vec::with_capacity(self.entries.len()); + for bsn_entry in &self.entries { + entries.push(match bsn_entry { + BsnEntry::TemplatePatch(bsn_type) => { + let mut assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut assignments, + true, + &[Member::Named(Ident::new( + "value", + proc_macro2::Span::call_site(), + ))], + true, + entity_refs, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchTemplate>::patch_template(move |value, _context| { + #(#assignments)* + }) + } + } + BsnEntry::FromTemplatePatch(bsn_type) => { + let mut assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut assignments, + true, + &[Member::Named(Ident::new( + "value", + proc_macro2::Span::call_site(), + ))], + true, + entity_refs, + ); + let path = &bsn_type.path; + quote! { + <#path as #bevy_scene::PatchFromTemplate>::patch(move |value, _context| { + #(#assignments)* + }) + } + } + BsnEntry::TemplateConst { + type_path, + const_ident, + } => { + quote! { + <#type_path as #bevy_scene::PatchTemplate>::patch_template( + move |value, _context| { + *value = #type_path::#const_ident; + }, + ) + } + } + BsnEntry::SceneExpression(block) => { + quote! {{#block}} + } + BsnEntry::TemplateConstructor(BsnConstructor { + type_path, + function, + args, + }) => { + quote! { + <#type_path as #bevy_scene::PatchTemplate>::patch_template( + move |value, _context| { + *value = #type_path::#function(#args); + }, + ) + } + } + BsnEntry::FromTemplateConstructor(BsnConstructor { + type_path, + function, + args, + }) => { + // NOTE: The odd turbofish line break below avoids breaking rustfmt + quote! { + <#type_path as #bevy_scene::PatchFromTemplate>::patch( + move |value, _context| { + *value = <#type_path as #bevy_ecs::template::FromTemplate> + ::Template::#function(#args); + } + ) + } + } + BsnEntry::RelatedSceneList(BsnRelatedSceneList { + scene_list, + relationship_path, + }) => { + let scenes = + scene_list + .0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); + // NOTE: The odd turbofish line breaks below avoid breaking rustfmt + quote! { + #bevy_scene::RelatedScenes::< + <#relationship_path as #bevy_ecs::relationship::RelationshipTarget> + ::Relationship, + _ + >::new( + #scenes + ) + } + } + BsnEntry::InheritedScene(inherited_scene) => match inherited_scene { + BsnInheritedScene::Asset(lit_str) => { + quote! {#bevy_scene::InheritSceneAsset::from(#lit_str)} + } + BsnInheritedScene::Fn { function, args } => { + quote! {#bevy_scene::SceneScope(#function(#args))} + } + }, + BsnEntry::Name(ident) => { + let name = ident.to_string(); + let index = entity_refs.get(name.clone()); + quote! { + #bevy_scene::NameEntityReference { + name: #bevy_ecs::name::Name(#name.into()), + index: #index, + } + } + } + BsnEntry::NameExpression(expr_tokens) => { + quote! { + <#bevy_ecs::name::Name as #bevy_scene::PatchFromTemplate>::patch( + move |value, _context| { + *value = #bevy_ecs::Name({#expr_tokens}.into()); + } + ) + } + } + }); + } + + quote! {(#(#entries,)*)} + } +} + +macro_rules! field_value_type { + () => { + BsnValue::Expr(_) + | BsnValue::Closure(_) + | BsnValue::Ident(_) + | BsnValue::Lit(_) + | BsnValue::Tuple(_) + }; +} + +impl BsnType { + #[allow(clippy::too_many_arguments)] + fn to_patch_tokens( + &self, + bevy_ecs: &Path, + bevy_scene: &Path, + assignments: &mut Vec, + is_root_template: bool, + field_path: &[Member], + is_path_ref: bool, + entity_refs: &mut EntityRefs, + ) { + let path = &self.path; + if !is_root_template { + assignments.push(quote! {#bevy_scene::macro_utils::touch_type::<#path>();}); + } + let maybe_deref = is_path_ref.then(|| quote! {*}); + let maybe_borrow_mut = (!is_path_ref).then(|| quote! {&mut}); + if let Some(variant) = &self.enum_variant { + let variant_name_lower = variant.to_string().to_lowercase(); + let variant_default_name = format_ident!("default_{}", variant_name_lower); + match &self.fields { + BsnFields::Named(fields) => { + let field_assignments = fields.iter().map(|f| { + let name = &f.name; + let value = &f.value; + if let Some(BsnValue::Type(bsn_type)) = &value { + if bsn_type.enum_variant.is_some() { + quote! {*#name = #bsn_type;} + } else { + let mut type_assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut type_assignments, + false, + &[Member::Named(name.clone())], + true, + entity_refs, + ); + quote! {#(#type_assignments)*} + } + } else { + quote! {*#name = #value;} + } + }); + let field_names = fields.iter().map(|f| &f.name); + assignments.push(quote! { + if !matches!(#(#field_path).*, #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template>::#variant { .. }) { + #maybe_deref #(#field_path).* = #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template>::#variant_default_name(); + } + if let #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template>::#variant { #(#field_names, )*.. } = #maybe_borrow_mut #(#field_path).* { + #(#field_assignments)* + } + }) + } + BsnFields::Tuple(fields) => { + // root template enums produce per-field "patches", at the cost of requiring the EnumDefaults pattern + let field_assignments = fields.iter().enumerate().map(|(index, f)| { + let name = format_ident!("t{}", index); + let value = &f.value; + if let BsnValue::Type(bsn_type) = &value { + if bsn_type.enum_variant.is_some() { + quote! {*#name = #bsn_type;} + } else { + let mut type_assignments = Vec::new(); + bsn_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + &mut type_assignments, + false, + &[Member::Named(name.clone())], + true, + entity_refs, + ); + quote! {#(#type_assignments)*} + } + } else { + quote! {*#name = #value;} + } + }); + let field_names = fields + .iter() + .enumerate() + .map(|(index, _)| format_ident!("t{}", index)); + assignments.push(quote! { + if !matches!(#(#field_path).*, #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template>::#variant(..)) { + #maybe_deref #(#field_path).* = #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template>::#variant_default_name(); + } + if let #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template>::#variant (#(#field_names, )*.. ) = #maybe_borrow_mut #(#field_path).* { + #(#field_assignments)* + } + }) + } + } + } else { + match &self.fields { + BsnFields::Named(fields) => { + for field in fields { + let field_name = &field.name; + let field_value = &field.value; + match field_value { + // NOTE: It is very important to still produce outputs for None field values. This is what + // enables field autocomplete in Rust Analyzer + Some(field_value_type!()) | None => { + assignments + .push(quote! {#(#field_path.)*#field_name = #field_value;}); + } + Some(BsnValue::Name(ident)) => { + let name = ident.to_string(); + let index = entity_refs.get(name); + assignments.push(quote!{ + #(#field_path.)*#field_name = #bevy_ecs::template::EntityReference::ScopedEntityIndex( + #bevy_ecs::template::ScopedEntityIndex { scope: _context.current_entity_scope(), index: #index } + ); + }) + } + Some(BsnValue::Type(field_type)) => { + if field_type.enum_variant.is_some() { + assignments + .push(quote! {#(#field_path.)*#field_name = #field_type;}); + } else { + let mut new_field_path = field_path.to_vec(); + new_field_path.push(Member::Named(field_name.clone())); + field_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + assignments, + false, + &new_field_path, + false, + entity_refs, + ); + } + } + } + } + } + BsnFields::Tuple(fields) => { + for (index, field) in fields.iter().enumerate() { + let field_index = Index::from(index); + let field_value = &field.value; + match field_value { + field_value_type!() => { + assignments + .push(quote! {#(#field_path.)*#field_index = #field_value;}); + } + BsnValue::Name(ident) => { + let name = ident.to_string(); + let index = entity_refs.get(name); + assignments.push(quote!{ + #(#field_path.)*#field_index = #bevy_ecs::template::EntityReference::ScopedEntityIndex( + #bevy_ecs::template::ScopedEntityIndex { scope: _context.current_entity_scope(), index: #index } + ); + }) + } + BsnValue::Type(field_type) => { + if field_type.enum_variant.is_some() { + assignments + .push(quote! {#(#field_path.)*#field_index = #field_type;}); + } else { + let mut new_field_path = field_path.to_vec(); + new_field_path.push(Member::Unnamed(field_index)); + field_type.to_patch_tokens( + bevy_ecs, + bevy_scene, + assignments, + false, + &new_field_path, + false, + entity_refs, + ); + } + } + } + } + } + } + } + } +} + +impl BsnSceneListItems { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + let scenes = self.0.iter().map(|scene| match scene { + BsnSceneListItem::Scene(bsn) => { + let tokens = bsn.to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); + quote! {#bevy_scene::EntityScene(#tokens)} + } + BsnSceneListItem::Expression(statements) => quote! {#(#statements)*}, + }); + quote! { + #bevy_scene::auto_nest_tuple!(#(#scenes),*) + } + } +} + +impl ToTokens for BsnType { + fn to_tokens(&self, tokens: &mut TokenStream) { + let path = &self.path; + let maybe_variant = self.enum_variant.as_ref().map(|variant| { + quote! {::#variant} + }); + let result = match &self.fields { + BsnFields::Named(fields) => { + let assignments = fields.iter().map(|f| { + let name = &f.name; + let value = &f.value; + quote! {#name: #value} + }); + quote! { + #path #maybe_variant { + #(#assignments,)* + } + } + } + BsnFields::Tuple(fields) => { + let assignments = fields.iter().map(|f| &f.value); + quote! { + #path #maybe_variant ( + #(#assignments,)* + ) + } + } + }; + result.to_tokens(tokens); + } +} + +impl ToTokens for BsnValue { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + BsnValue::Expr(expr_tokens) => { + quote! {{#expr_tokens}.into()}.to_tokens(tokens); + } + BsnValue::Closure(closure_tokens) => { + quote! {(#closure_tokens).into()}.to_tokens(tokens); + } + BsnValue::Ident(ident) => { + quote! {(#ident).into()}.to_tokens(tokens); + } + BsnValue::Lit(lit) => match lit { + Lit::Str(str) => quote! {#str.into()}.to_tokens(tokens), + _ => lit.to_tokens(tokens), + }, + BsnValue::Tuple(tuple) => { + let tuple_tokens = tuple.0.iter(); + quote! {(#(#tuple_tokens),*)}.to_tokens(tokens); + } + BsnValue::Type(ty) => { + ty.to_tokens(tokens); + } + BsnValue::Name(_ident) => { + // Name requires additional context to convert to tokens + unreachable!() + } + }; + } +} + +impl BsnListRoot { + pub fn to_tokens( + &self, + bevy_scene: &Path, + bevy_ecs: &Path, + bevy_asset: &Path, + entity_refs: &mut EntityRefs, + ) -> TokenStream { + let tokens = self + .0 + .to_tokens(bevy_scene, bevy_ecs, bevy_asset, entity_refs); + quote! {#bevy_scene::SceneListScope(#tokens)} + } +} diff --git a/crates/bevy_scene2/macros/src/bsn/mod.rs b/crates/bevy_scene2/macros/src/bsn/mod.rs new file mode 100644 index 0000000000..1fa84e8af6 --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/mod.rs @@ -0,0 +1,37 @@ +use crate::bsn::{ + codegen::EntityRefs, + types::{BsnListRoot, BsnRoot}, +}; +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use syn::parse_macro_input; + +pub mod codegen; +pub mod parse; +pub mod types; + +pub fn bsn(input: TokenStream) -> TokenStream { + let scene = parse_macro_input!(input as BsnRoot); + let (bevy_scene, bevy_ecs, bevy_asset) = BevyManifest::shared(|manifest| { + ( + manifest.get_path("bevy_scene2"), + manifest.get_path("bevy_ecs"), + manifest.get_path("bevy_asset"), + ) + }); + let mut entity_refs = EntityRefs::default(); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset, &mut entity_refs)) +} + +pub fn bsn_list(input: TokenStream) -> TokenStream { + let scene = parse_macro_input!(input as BsnListRoot); + let (bevy_scene, bevy_ecs, bevy_asset) = BevyManifest::shared(|manifest| { + ( + manifest.get_path("bevy_scene2"), + manifest.get_path("bevy_ecs"), + manifest.get_path("bevy_asset"), + ) + }); + let mut entity_refs = EntityRefs::default(); + TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset, &mut entity_refs)) +} diff --git a/crates/bevy_scene2/macros/src/bsn/parse.rs b/crates/bevy_scene2/macros/src/bsn/parse.rs new file mode 100644 index 0000000000..9603f0591b --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/parse.rs @@ -0,0 +1,555 @@ +use crate::bsn::types::{ + Bsn, BsnConstructor, BsnEntry, BsnFields, BsnInheritedScene, BsnListRoot, BsnNamedField, + BsnRelatedSceneList, BsnRoot, BsnSceneList, BsnSceneListItem, BsnSceneListItems, BsnTuple, + BsnType, BsnUnnamedField, BsnValue, +}; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; +use quote::quote; +use syn::{ + braced, bracketed, + buffer::Cursor, + parenthesized, + parse::{Parse, ParseBuffer, ParseStream}, + spanned::Spanned, + token::{At, Brace, Bracket, Colon, Comma, Paren}, + Block, Expr, Ident, Lit, LitStr, Path, Result, Token, +}; + +/// Functionally identical to [`Punctuated`](syn::punctuated::Punctuated), but fills the given `$list` Vec instead +/// of allocating a new one inside [`Punctuated`](syn::punctuated::Punctuated). This exists to avoid allocating an intermediate Vec. +/// +/// This also attempts to parse $parse a second time _before_ parsing $separator, as this enables autocomplete to work in cases where +/// it is being typed in the middle of a list +macro_rules! parse_punctuated_vec_autocomplete_friendly { + ($list:ident, $input:ident, $parse:ident, $separator:ident) => { + loop { + if $input.is_empty() { + break; + } + let value = $input.parse::<$parse>()?; + $list.push(value); + if $input.is_empty() { + break; + } + + // Try parsing without a comma separator first. This makes autocomplete + // work in more places + if !$input.is_empty() && !$input.peek($separator) { + let value = $input.parse::<$parse>()?; + $list.push(value); + } + $input.parse::<$separator>()?; + } + }; +} + +impl Parse for BsnRoot { + fn parse(input: ParseStream) -> Result { + Ok(BsnRoot(input.parse::>()?)) + } +} + +impl Parse for BsnListRoot { + fn parse(input: ParseStream) -> Result { + Ok(BsnListRoot(input.parse::()?)) + } +} + +impl Parse for Bsn { + fn parse(input: ParseStream) -> Result { + let mut entries = Vec::new(); + let mut found_inherited_scene = false; + if input.peek(Paren) { + let content; + parenthesized![content in input]; + while !content.is_empty() { + let entry = BsnEntry::parse(&content, found_inherited_scene)?; + if matches!(entry, BsnEntry::InheritedScene(_)) { + found_inherited_scene = true; + } + entries.push(entry); + } + } else if ALLOW_FLAT { + while !input.is_empty() { + let entry = BsnEntry::parse(input, found_inherited_scene)?; + if matches!(entry, BsnEntry::InheritedScene(_)) { + found_inherited_scene = true; + } + entries.push(entry); + if input.peek(Comma) { + // Not ideal, but this anticipatory break allows us to parse non-parenthesized + // flat Bsn entries in SceneLists + break; + } + } + } else { + entries.push(BsnEntry::parse(input, found_inherited_scene)?); + } + + Ok(Self { entries }) + } +} + +impl BsnEntry { + fn parse(input: ParseStream, found_inherited_scene: bool) -> Result { + Ok(if input.peek(Token![:]) { + BsnEntry::InheritedScene(BsnInheritedScene::parse(input, found_inherited_scene)?) + } else if input.peek(Token![#]) { + input.parse::()?; + if input.peek(Brace) { + BsnEntry::NameExpression(braced_tokens(input)?) + } else { + BsnEntry::Name(input.parse::()?) + } + } else if input.peek(Brace) { + BsnEntry::SceneExpression(braced_tokens(input)?) + } else { + let is_template = input.peek(At); + if is_template { + input.parse::()?; + } + let mut path = input.parse::()?; + let path_type = PathType::new(&path); + match path_type { + PathType::Type | PathType::Enum => { + let enum_variant = if matches!(path_type, PathType::Enum) { + take_last_path_ident(&mut path) + } else { + None + }; + if input.peek(Bracket) { + // TODO: fail if this is an enum variant + BsnEntry::RelatedSceneList(BsnRelatedSceneList { + relationship_path: path, + scene_list: input.parse::()?, + }) + } else { + let fields = input.parse::()?; + let bsn_type = BsnType { + path, + enum_variant, + fields, + }; + if is_template { + BsnEntry::TemplatePatch(bsn_type) + } else { + BsnEntry::FromTemplatePatch(bsn_type) + } + } + } + PathType::TypeConst => { + let const_ident = take_last_path_ident(&mut path).unwrap(); + BsnEntry::TemplateConst { + type_path: path, + const_ident, + } + } + PathType::Const => { + return Err(syn::Error::new( + path.span(), + "Consts are not currently supported in this position", + )) + } + PathType::TypeFunction => { + let function = take_last_path_ident(&mut path).unwrap(); + let args = if input.peek(Paren) { + let content; + parenthesized!(content in input); + Some(content.parse_terminated(Expr::parse, Token![,])?) + } else { + None + }; + + let bsn_constructor = BsnConstructor { + type_path: path, + function, + args, + }; + if is_template { + BsnEntry::TemplateConstructor(bsn_constructor) + } else { + BsnEntry::FromTemplateConstructor(bsn_constructor) + } + } + PathType::Function => { + if input.peek(Paren) { + let tokens = parenthesized_tokens(input)?; + BsnEntry::SceneExpression(quote! {#path(#tokens)}) + } else { + BsnEntry::SceneExpression(quote! {#path}) + } + } + } + }) + } +} + +impl Parse for BsnSceneList { + fn parse(input: ParseStream) -> Result { + let content; + bracketed!(content in input); + Ok(BsnSceneList(content.parse::()?)) + } +} + +impl Parse for BsnSceneListItems { + fn parse(input: ParseStream) -> Result { + let mut scenes = Vec::new(); + parse_punctuated_vec_autocomplete_friendly!(scenes, input, BsnSceneListItem, Comma); + Ok(BsnSceneListItems(scenes)) + } +} + +impl Parse for BsnSceneListItem { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + let block = input.parse::()?; + BsnSceneListItem::Expression(block.stmts) + } else { + BsnSceneListItem::Scene(input.parse::>()?) + }) + } +} + +impl BsnInheritedScene { + fn parse(input: ParseStream, found_inherited_scene: bool) -> Result { + let colon = input.parse::()?; + if found_inherited_scene { + return Err(syn::Error::new( + colon.span(), + "Cannot inherit scenes more than once", + )); + } + Ok(if input.peek(LitStr) { + let path = input.parse::()?; + BsnInheritedScene::Asset(path) + } else { + let function = input.parse::()?; + let args = if input.peek(Paren) { + let content; + parenthesized!(content in input); + Some(content.parse_terminated(Expr::parse, Token![,])?) + } else { + None + }; + BsnInheritedScene::Fn { function, args } + }) + } +} + +impl Parse for BsnType { + fn parse(input: ParseStream) -> Result { + let mut path = input.parse::()?; + let enum_variant = match PathType::new(&path) { + PathType::Type => None, + PathType::Enum => take_last_path_ident(&mut path), + PathType::Function | PathType::TypeFunction => { + return Err(syn::Error::new( + path.span(), + "Expected a path to a BSN type but encountered a path to a function.", + )) + } + PathType::Const | PathType::TypeConst => { + return Err(syn::Error::new( + path.span(), + "Expected a path to a BSN type but encountered a path to a const.", + )) + } + }; + let fields = input.parse::()?; + Ok(BsnType { + path, + enum_variant, + fields, + }) + } +} + +impl Parse for BsnTuple { + fn parse(input: ParseStream) -> Result { + let content; + parenthesized![content in input]; + let mut fields = Vec::new(); + while !content.is_empty() { + fields.push(content.parse::()?); + } + Ok(BsnTuple(fields)) + } +} + +impl Parse for BsnFields { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + let content; + braced![content in input]; + let mut fields = Vec::new(); + parse_punctuated_vec_autocomplete_friendly!(fields, content, BsnNamedField, Comma); + BsnFields::Named(fields) + } else if input.peek(Paren) { + let content; + parenthesized![content in input]; + let mut fields = Vec::new(); + parse_punctuated_vec_autocomplete_friendly!(fields, content, BsnUnnamedField, Comma); + BsnFields::Tuple(fields) + } else { + BsnFields::Named(Vec::new()) + }) + } +} + +impl Parse for BsnNamedField { + fn parse(input: ParseStream) -> Result { + let name = input.parse::()?; + let value = if input.peek(Colon) { + input.parse::()?; + Some(input.parse::()?) + } else { + None + }; + Ok(BsnNamedField { name, value }) + } +} + +impl Parse for BsnUnnamedField { + fn parse(input: ParseStream) -> Result { + let value = input.parse::()?; + Ok(BsnUnnamedField { value }) + } +} + +/// Parse a closure "loosely" without caring about the tokens between `|...|` and `{...}`. This ensures autocomplete works. +fn parse_closure_loose(input: &ParseBuffer) -> Result { + let start = input.cursor(); + input.parse::()?; + let tokens = input.step(|cursor| { + let mut rest = *cursor; + while let Some((tt, next)) = rest.token_tree() { + match &tt { + TokenTree::Punct(punct) if punct.as_char() == '|' => { + if let Some((TokenTree::Group(group), next)) = next.token_tree() + && group.delimiter() == Delimiter::Brace + { + return Ok((tokens_between(start, next), next)); + } else { + return Err(cursor.error("closures expect '{' to follow '|'")); + } + } + _ => rest = next, + } + } + Err(cursor.error("no matching `|` was found after this point")) + })?; + Ok(tokens) +} + +// Used to parse a block "loosely" without caring about the content in `{...}`. This ensures autocomplete works. +fn braced_tokens(input: &ParseBuffer) -> Result { + let content; + braced!(content in input); + content.parse::() +} + +// Used to parse parenthesized tokens "loosely" without caring about the content in `(...)`. This ensures autocomplete works. +fn parenthesized_tokens(input: &ParseBuffer) -> Result { + let content; + parenthesized!(content in input); + content.parse::() +} + +fn tokens_between(begin: Cursor, end: Cursor) -> TokenStream { + assert!(begin <= end); + let mut cursor = begin; + let mut tokens = TokenStream::new(); + while cursor < end { + let (token, next) = cursor.token_tree().unwrap(); + tokens.extend(std::iter::once(token)); + cursor = next; + } + tokens +} + +impl Parse for BsnValue { + fn parse(input: ParseStream) -> Result { + Ok(if input.peek(Brace) { + BsnValue::Expr(braced_tokens(input)?) + } else if input.peek(Token![|]) { + let tokens = parse_closure_loose(input)?; + BsnValue::Closure(tokens) + } else if input.peek(Ident) { + let forked = input.fork(); + let path = forked.parse::()?; + if path.segments.len() == 1 && (forked.is_empty() || forked.peek(Comma)) { + return Ok(BsnValue::Ident(input.parse::()?)); + } + match PathType::new(&path) { + PathType::TypeFunction | PathType::Function => { + input.parse::()?; + let token_stream = parenthesized_tokens(input)?; + BsnValue::Expr(quote! { #path(#token_stream) }) + } + PathType::Const | PathType::TypeConst => { + input.parse::()?; + BsnValue::Expr(quote! { #path }) + } + PathType::Type | PathType::Enum => BsnValue::Type(input.parse::()?), + } + } else if input.peek(Lit) { + BsnValue::Lit(input.parse::()?) + } else if input.peek(Paren) { + BsnValue::Tuple(input.parse::()?) + } else if input.peek(Token![#]) { + input.parse::()?; + BsnValue::Name(input.parse::()?) + } else { + return Err(input.error("Unexpected input: Invalid BsnValue. This does not match any expected BSN value type.")); + }) + } +} + +#[derive(PartialEq, Eq, Debug)] +enum PathType { + Type, + Enum, + Const, + TypeConst, + TypeFunction, + Function, +} + +impl PathType { + fn new(path: &Path) -> PathType { + let mut iter = path.segments.iter().rev(); + if let Some(last_segment) = iter.next() { + let last_string = last_segment.ident.to_string(); + let mut last_string_chars = last_string.chars(); + let last_ident_first_char = last_string_chars.next().unwrap(); + if last_ident_first_char.is_uppercase() { + let is_const = is_const(&last_string); + if let Some(second_to_last_segment) = iter.next() { + // PERF: is there some way to avoid this string allocation? + let second_to_last_string = second_to_last_segment.ident.to_string(); + let first_char = second_to_last_string.chars().next().unwrap(); + if first_char.is_uppercase() { + if is_const { + PathType::TypeConst + } else { + PathType::Enum + } + } else if is_const { + PathType::Const + } else { + PathType::Type + } + } else if is_const { + PathType::Const + } else { + PathType::Type + } + } else if let Some(second_to_last) = iter.next() { + // PERF: is there some way to avoid this string allocation? + let second_to_last_string = second_to_last.ident.to_string(); + let first_char = second_to_last_string.chars().next().unwrap(); + if first_char.is_uppercase() { + PathType::TypeFunction + } else { + PathType::Function + } + } else { + PathType::Function + } + } else { + // This won't be hit so just pick one to make it easy on consumers + PathType::Type + } + } +} + +fn is_const(path: &str) -> bool { + // Paths of length 1 are ambiguous, we give the tie to Types, as that is more useful + // for scenes + if path.len() == 1 { + return false; + } + + for char in path.chars() { + if char == '_' { + return true; + } + + if char.is_lowercase() { + return false; + } + } + + // All characters are uppercase ... this is a Const + true +} + +fn take_last_path_ident(path: &mut Path) -> Option { + let ident = path.segments.pop().map(|s| s.into_value().ident); + path.segments.pop_punct(); + ident +} + +#[cfg(test)] +mod tests { + use crate::bsn::parse::PathType; + use syn::{parse_str, Path}; + #[test] + fn path_type() { + let path = parse_str::("foo::X").unwrap(); + // This case is ambiguous. We parse it as a Type as that works better in the scene patching context + assert_eq!(PathType::new(&path), PathType::Type); + + let path = parse_str::("foo::X_AXIS").unwrap(); + assert_eq!(PathType::new(&path), PathType::Const); + + let path = parse_str::("foo::XAXIS").unwrap(); + assert_eq!(PathType::new(&path), PathType::Const); + + let path = parse_str::("X").unwrap(); + // This case is ambiguous. We parse it as a Type as that works better in the scene patching context + assert_eq!(PathType::new(&path), PathType::Type); + + let path = parse_str::("X_AXIS").unwrap(); + assert_eq!(PathType::new(&path), PathType::Const); + + let path = parse_str::("XAXIS").unwrap(); + assert_eq!(PathType::new(&path), PathType::Const); + + let path = parse_str::("XType").unwrap(); + assert_eq!(PathType::new(&path), PathType::Type); + + let path = parse_str::("foo::XType").unwrap(); + assert_eq!(PathType::new(&path), PathType::Type); + + let path = parse_str::("Foo::Bar").unwrap(); + assert_eq!(PathType::new(&path), PathType::Enum); + + let path = parse_str::("foo::Foo::Bar").unwrap(); + assert_eq!(PathType::new(&path), PathType::Enum); + + let path = parse_str::("Foo::bar").unwrap(); + assert_eq!(PathType::new(&path), PathType::TypeFunction); + + let path = parse_str::("foo::Foo::bar").unwrap(); + assert_eq!(PathType::new(&path), PathType::TypeFunction); + + let path = parse_str::("Foo::B").unwrap(); + // This is ambiguous with TypeConst ... we give the tie to Enum as that works better + // in a scene context + assert_eq!(PathType::new(&path), PathType::Enum); + + let path = parse_str::("Foo::BAR").unwrap(); + assert_eq!(PathType::new(&path), PathType::TypeConst); + + let path = parse_str::("foo").unwrap(); + assert_eq!(PathType::new(&path), PathType::Function); + + let path = parse_str::("foo::foo").unwrap(); + assert_eq!(PathType::new(&path), PathType::Function); + + let path = parse_str::("f").unwrap(); + assert_eq!(PathType::new(&path), PathType::Function); + } +} diff --git a/crates/bevy_scene2/macros/src/bsn/types.rs b/crates/bevy_scene2/macros/src/bsn/types.rs new file mode 100644 index 0000000000..d5efb9419c --- /dev/null +++ b/crates/bevy_scene2/macros/src/bsn/types.rs @@ -0,0 +1,101 @@ +use proc_macro2::TokenStream; +use syn::{punctuated::Punctuated, Expr, Ident, Lit, LitStr, Path, Stmt, Token}; + +#[derive(Debug)] +pub struct BsnRoot(pub Bsn); + +#[derive(Debug)] +pub struct BsnListRoot(pub BsnSceneListItems); + +#[derive(Debug)] +pub struct Bsn { + pub entries: Vec, +} + +#[derive(Debug)] +pub enum BsnEntry { + Name(Ident), + NameExpression(TokenStream), + FromTemplatePatch(BsnType), + TemplatePatch(BsnType), + FromTemplateConstructor(BsnConstructor), + TemplateConstructor(BsnConstructor), + TemplateConst { type_path: Path, const_ident: Ident }, + SceneExpression(TokenStream), + InheritedScene(BsnInheritedScene), + RelatedSceneList(BsnRelatedSceneList), +} + +#[derive(Debug)] +pub struct BsnType { + pub path: Path, + pub enum_variant: Option, + pub fields: BsnFields, +} + +#[derive(Debug)] +pub struct BsnRelatedSceneList { + pub relationship_path: Path, + pub scene_list: BsnSceneList, +} + +#[derive(Debug)] +pub struct BsnSceneList(pub BsnSceneListItems); + +#[derive(Debug)] +pub struct BsnSceneListItems(pub Vec); + +#[derive(Debug)] +pub enum BsnSceneListItem { + Scene(Bsn), + Expression(Vec), +} + +#[derive(Debug)] +pub enum BsnInheritedScene { + Asset(LitStr), + Fn { + function: Ident, + args: Option>, + }, +} + +#[derive(Debug)] +pub struct BsnConstructor { + pub type_path: Path, + pub function: Ident, + pub args: Option>, +} + +#[derive(Debug)] +pub enum BsnFields { + Named(Vec), + Tuple(Vec), +} + +#[derive(Debug)] +pub struct BsnTuple(pub Vec); + +#[derive(Debug)] +pub struct BsnNamedField { + pub name: Ident, + /// This is an Option to enable autocomplete when the field name is being typed + /// To improve autocomplete further we'll need to forgo a lot of the syn parsing + pub value: Option, +} + +#[derive(Debug)] +pub struct BsnUnnamedField { + pub value: BsnValue, +} + +#[derive(Debug)] +pub enum BsnValue { + Expr(TokenStream), + Closure(TokenStream), + Ident(Ident), + Lit(Lit), + Type(BsnType), + Tuple(BsnTuple), + Name(Ident), +} diff --git a/crates/bevy_scene2/macros/src/lib.rs b/crates/bevy_scene2/macros/src/lib.rs new file mode 100644 index 0000000000..f93d495211 --- /dev/null +++ b/crates/bevy_scene2/macros/src/lib.rs @@ -0,0 +1,13 @@ +mod bsn; + +use proc_macro::TokenStream; + +#[proc_macro] +pub fn bsn(input: TokenStream) -> TokenStream { + crate::bsn::bsn(input) +} + +#[proc_macro] +pub fn bsn_list(input: TokenStream) -> TokenStream { + crate::bsn::bsn_list(input) +} diff --git a/crates/bevy_scene2/src/lib.rs b/crates/bevy_scene2/src/lib.rs new file mode 100644 index 0000000000..a2a66c7525 --- /dev/null +++ b/crates/bevy_scene2/src/lib.rs @@ -0,0 +1,639 @@ +//! Provides BSN functionality. See [`Scene`], [`SceneList`], [`ScenePatch`], and the [`bsn!`] / [`bsn_list!`] macros for more information. + +/// The Bevy Scene prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + pub use crate::{ + bsn, bsn_list, on, CommandsSceneExt, EntityCommandsSceneExt, EntityWorldMutSceneExt, + PatchFromTemplate, PatchTemplate, Scene, SceneList, ScenePatchInstance, WorldSceneExt, + }; +} + +/// Functionality used by the [`bsn!`] macro. +pub mod macro_utils; + +extern crate alloc; + +mod resolved_scene; +mod scene; +mod scene_list; +mod scene_patch; +mod spawn; + +pub use bevy_scene2_macros::*; +pub use resolved_scene::*; +pub use scene::*; +pub use scene_list::*; +pub use scene_patch::*; +pub use spawn::*; + +use bevy_app::{App, Plugin, SceneSpawnerSystems, SpawnScene}; +use bevy_asset::AssetApp; +use bevy_ecs::prelude::*; + +/// Adds support for spawning Bevy Scenes. See [`Scene`], [`SceneList`], [`ScenePatch`], and the [`bsn!`] macro for more information. +#[derive(Default)] +pub struct ScenePlugin; + +impl Plugin for ScenePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_asset::() + .init_asset::() + .add_systems( + SpawnScene, + (resolve_scene_patches, spawn_queued) + .chain() + .in_set(SceneSpawnerSystems::Scene2Spawn) + .after(SceneSpawnerSystems::SceneSpawn), + ) + .add_observer(on_add_scene_patch_instance); + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + use crate::{self as bevy_scene2, ScenePlugin}; + use bevy_app::{App, TaskPoolPlugin}; + use bevy_asset::{Asset, AssetApp, AssetPlugin, AssetServer, Handle}; + use bevy_ecs::prelude::*; + use bevy_reflect::TypePath; + + fn test_app() -> App { + let mut app = App::new(); + app.add_plugins(( + TaskPoolPlugin::default(), + AssetPlugin::default(), + ScenePlugin, + )); + app + } + + #[test] + fn inheritance_patching() { + let mut app = test_app(); + let world = app.world_mut(); + + #[derive(Component, FromTemplate)] + struct Position { + x: f32, + y: f32, + z: f32, + } + + fn b() -> impl Scene { + bsn! { + :a + Position { x: 1. } + Children [ #Y ] + } + } + + fn a() -> impl Scene { + bsn! { + Position { y: 2. } + Children [ #X ] + } + } + + let id = world.spawn_scene(b()).unwrap().id(); + let root = world.entity(id); + + let position = root.get::().unwrap(); + assert_eq!(position.x, 1.); + assert_eq!(position.y, 2.); + assert_eq!(position.z, 0.); + + let children = root.get::().unwrap(); + assert_eq!(children.len(), 2); + + let x = world.entity(children[0]); + let name = x.get::().unwrap(); + assert_eq!(name.as_str(), "X"); + + let y = world.entity(children[1]); + let name = y.get::().unwrap(); + assert_eq!(name.as_str(), "Y"); + } + + #[test] + fn inline_scene_patching() { + let mut app = test_app(); + let world = app.world_mut(); + + #[derive(Component, FromTemplate)] + struct Position { + x: f32, + y: f32, + z: f32, + } + + fn b() -> impl Scene { + bsn! { + a() + Position { x: 1. } + Children [ #Y ] + } + } + + fn a() -> impl Scene { + bsn! { + Position { y: 2. } + Children [ #X ] + } + } + + let id = world.spawn_scene(b()).unwrap().id(); + let root = world.entity(id); + + let position = root.get::().unwrap(); + assert_eq!(position.x, 1.); + assert_eq!(position.y, 2.); + assert_eq!(position.z, 0.); + + let children = root.get::().unwrap(); + assert_eq!(children.len(), 2); + + let x = world.entity(children[0]); + let name = x.get::().unwrap(); + assert_eq!(name.as_str(), "X"); + + let y = world.entity(children[1]); + let name = y.get::().unwrap(); + assert_eq!(name.as_str(), "Y"); + } + + #[test] + fn hierarchy() { + let mut app = test_app(); + let world = app.world_mut(); + + fn scene() -> impl Scene { + bsn! { + #A + Children [ + ( + #B + Children [ + #X + ] + ), + ( + #C + Children [ + #Y + ] + ) + ] + } + } + + let id = world.spawn_scene(scene()).unwrap().id(); + + let a = world.entity(id); + let name = a.get::().unwrap(); + assert_eq!(name.as_str(), "A"); + + let children = a.get::().unwrap(); + assert_eq!(children.len(), 2); + + let b = world.entity(children[0]); + let c = world.entity(children[1]); + + let name = b.get::().unwrap(); + assert_eq!(name.as_str(), "B"); + + let name = c.get::().unwrap(); + assert_eq!(name.as_str(), "C"); + + let children = b.get::().unwrap(); + assert_eq!(children.len(), 1); + let x = world.entity(children[0]); + let name = x.get::().unwrap(); + assert_eq!(name.as_str(), "X"); + + let children = c.get::().unwrap(); + assert_eq!(children.len(), 1); + let y = world.entity(children[0]); + let name = y.get::().unwrap(); + assert_eq!(name.as_str(), "Y"); + } + + #[test] + fn constant_values() { + let mut app = test_app(); + let world = app.world_mut(); + + const X_AXIS: usize = 1; + const XAXIS: usize = 2; + + #[derive(Component, FromTemplate)] + struct Value(usize); + + fn x_axis() -> impl Scene { + bsn! {Value(X_AXIS)} + } + + fn xaxis() -> impl Scene { + bsn! {Value(XAXIS)} + } + + let entity = world.spawn_scene(x_axis()).unwrap(); + assert_eq!(entity.get::().unwrap().0, 1); + + let entity = world.spawn_scene(xaxis()).unwrap(); + assert_eq!(entity.get::().unwrap().0, 2); + } + + #[derive(Component, FromTemplate)] + struct Reference(Entity); + + #[test] + fn bsn_name_references() { + let mut app = test_app(); + let world = app.world_mut(); + + fn a() -> impl Scene { + bsn! { + #X + Children [ + (:b Reference(#X)) + ] + } + } + + fn b() -> impl Scene { + let inline = bsn! {#Y Reference(#Y) Children [ Reference(#Y)] }; + bsn! { + #X + Children [ + Reference(#X), + (inline Reference(#X)), + ] + } + } + + let id = world.spawn_scene(a()).unwrap().id(); + + let a = world.entity(id); + let name = a.get::().unwrap(); + assert_eq!(name.as_str(), "X"); + + let children = a.get::().unwrap(); + assert_eq!(children.len(), 1); + + let b = world.entity(children[0]); + let reference = b.get::().unwrap(); + assert_eq!(reference.0, id); + + let b_name = b.get::().unwrap(); + assert_eq!(b_name.as_str(), "X"); + + let grandchildren = b.get::().unwrap(); + assert_eq!(grandchildren.len(), 2); + + let grandchild = world.entity(grandchildren[0]); + assert_eq!(grandchild.get::().unwrap().0, b.id()); + + let grandchild = world.entity(grandchildren[1]); + assert_eq!(grandchild.get::().unwrap().0, b.id()); + assert_eq!(grandchild.get::().unwrap().as_str(), "Y"); + + assert_eq!( + grandchild.id(), + world + .entity(grandchild.get::().unwrap()[0]) + .get::() + .unwrap() + .0 + ); + } + + #[test] + fn bsn_list_name_references() { + let mut app = test_app(); + let world = app.world_mut(); + + fn b() -> impl Scene { + bsn! { + #Z + Children [ + Reference(#Z) + ] + } + } + + fn a() -> impl SceneList { + bsn_list![ + ( + #X + Reference(#Y) + Children [ + (#Z Reference(#X)) + ] + + ), + ( + #Y + Reference(#X) + Children [ + Reference(#Y) + ] + ), + (:b #Z) + ] + } + + let ids = world.spawn_scene_list(a()).unwrap(); + assert_eq!(ids.len(), 3); + + let e0 = world.entity(ids[0]); + let name = e0.get::().unwrap(); + assert_eq!(name.as_str(), "X"); + let reference = e0.get::().unwrap(); + assert_eq!(reference.0, ids[1]); + + let child0 = e0.get::().unwrap()[0]; + let reference = world.entity(child0).get::().unwrap(); + assert_eq!(reference.0, ids[0]); + + let e1 = world.entity(ids[1]); + let name = e1.get::().unwrap(); + assert_eq!(name.as_str(), "Y"); + + let reference = e1.get::().unwrap(); + assert_eq!(reference.0, ids[0]); + + let child0 = e1.get::().unwrap()[0]; + let reference = world.entity(child0).get::().unwrap(); + assert_eq!(reference.0, ids[1]); + + let e2 = world.entity(ids[2]); + let name = e2.get::().unwrap(); + assert_eq!(name.as_str(), "Z"); + let child0 = e2.get::().unwrap()[0]; + let reference = world.entity(child0).get::().unwrap(); + assert_eq!(reference.0, ids[2]); + } + + #[test] + fn on_template() { + #[derive(Resource)] + struct Exploded(Option); + + #[derive(EntityEvent)] + struct Explode(Entity); + + let mut app = test_app(); + let world = app.world_mut(); + world.insert_resource(Exploded(None)); + + fn scene() -> impl Scene { + bsn! { + on(|explode: On, mut exploded: ResMut|{ + exploded.0 = Some(explode.0); + }) + } + } + + let id = world.spawn_scene(scene()).unwrap().id(); + world.trigger(Explode(id)); + let exploded = world.resource::(); + assert_eq!(exploded.0, Some(id)); + } + + #[test] + fn enum_patching() { + let mut app = test_app(); + let world = app.world_mut(); + + #[derive(Component, FromTemplate, PartialEq, Eq, Debug)] + enum Foo { + #[default] + Bar { + x: u32, + y: u32, + z: u32, + }, + Baz(usize), + Qux, + } + + fn a() -> impl Scene { + bsn! { + Foo::Baz(10) + } + } + + fn b() -> impl Scene { + bsn! { + a() + Foo::Bar { x: 1 } + } + } + + fn c() -> impl Scene { + bsn! { + b() + Foo::Bar { y: 2 } + } + } + + fn d() -> impl Scene { + bsn! { + c() + Foo::Qux + } + } + + let id = world.spawn_scene(c()).unwrap().id(); + let root = world.entity(id); + + let foo = root.get::().unwrap(); + assert_eq!(Foo::Bar { x: 1, y: 2, z: 0 }, *foo); + + let id = world.spawn_scene(a()).unwrap().id(); + let root = world.entity(id); + + let foo = root.get::().unwrap(); + assert_eq!(Foo::Baz(10), *foo); + + let id = world.spawn_scene(d()).unwrap().id(); + let root = world.entity(id); + let foo = root.get::().unwrap(); + assert_eq!(Foo::Qux, *foo); + } + + #[test] + fn struct_patching() { + let mut app = test_app(); + let world = app.world_mut(); + + #[derive(Component, FromTemplate, PartialEq, Eq, Debug)] + struct Foo { + x: u32, + y: u32, + z: u32, + nested: Bar, + } + + #[derive(Component, FromTemplate, PartialEq, Eq, Debug)] + struct Bar(usize, usize, usize); + + fn a() -> impl Scene { + bsn! { + Foo { + x: 1, + nested: Bar(1, 1), + } + } + } + + fn b() -> impl Scene { + bsn! { + a() + Foo { + y: 2, + nested: Bar(2), + } + } + } + + let id = world.spawn_scene(b()).unwrap().id(); + let root = world.entity(id); + + let foo = root.get::().unwrap(); + assert_eq!( + *foo, + Foo { + x: 1, + y: 2, + z: 0, + nested: Bar(2, 1, 0) + } + ); + } + + #[test] + fn handle_template() { + let mut app = test_app(); + app.init_asset::(); + + #[derive(Asset, TypePath)] + struct Image; + + let handle = app.world().resource::().load("image.png"); + let world = app.world_mut(); + + #[derive(Component, FromTemplate, PartialEq, Eq, Debug)] + struct Sprite(Handle); + + fn scene() -> impl Scene { + bsn! { + Sprite("image.png") + } + } + + let id = world.spawn_scene(scene()).unwrap().id(); + let root = world.entity(id); + + let sprite = root.get::().unwrap(); + assert_eq!(sprite.0, handle); + } + + #[test] + fn scene_list_children() { + let mut app = test_app(); + let world = app.world_mut(); + + fn root(children: impl SceneList) -> impl Scene { + bsn! { + Children [ + #A, + {children}, + #D + ] + } + } + + let children = bsn_list! [ + #B, + #C, + ]; + + let id = world.spawn_scene(root(children)).unwrap().id(); + let root = world.entity(id); + let children = root.get::().unwrap(); + let a = world.entity(children[0]).get::().unwrap(); + let b = world.entity(children[1]).get::().unwrap(); + let c = world.entity(children[2]).get::().unwrap(); + let d = world.entity(children[3]).get::().unwrap(); + assert_eq!(a.as_str(), "A"); + assert_eq!(b.as_str(), "B"); + assert_eq!(c.as_str(), "C"); + assert_eq!(d.as_str(), "D"); + } + + #[test] + fn generic_patching() { + let mut app = test_app(); + let world = app.world_mut(); + + #[derive(Component, FromTemplate, PartialEq, Eq, Debug)] + struct Foo>> { + value: T, + number: u32, + } + + #[derive(Component, FromTemplate, PartialEq, Eq, Debug)] + struct Position { + x: u32, + y: u32, + z: u32, + } + + fn a() -> impl Scene { + bsn! { + Foo:: { + value: Position { x: 1 } + } + } + } + + fn b() -> impl Scene { + bsn! { + a() + Foo:: { + value: Position { y: 2 }, + number: 10, + } + } + } + + let id = world.spawn_scene(b()).unwrap().id(); + let root = world.entity(id); + + let foo = root.get::>().unwrap(); + assert_eq!( + *foo, + Foo { + value: Position { x: 1, y: 2, z: 0 }, + number: 10 + } + ); + } + + #[test] + fn empty_scene_expressions() { + let mut app = test_app(); + let world = app.world_mut(); + fn a() -> impl Scene { + bsn! { + {} + } + } + world.spawn_scene(a()).unwrap(); + } +} diff --git a/crates/bevy_scene2/src/macro_utils.rs b/crates/bevy_scene2/src/macro_utils.rs new file mode 100644 index 0000000000..52e4502bbf --- /dev/null +++ b/crates/bevy_scene2/src/macro_utils.rs @@ -0,0 +1,146 @@ +/// This is used by the [`bsn!`](crate::bsn) macro to generate compile-time only references to symbols. Currently this is used +/// to add IDE support for nested type names, as it allows us to pass the input Ident from the input to the output code. +pub const fn touch_type() {} + +/// Creates a tuple that will be nested after it passes 11 items. +/// When there is a single item, it is _not_ wrapped in a tuple. +/// This is implemented in a way that creates the smallest number of trait impls possible. +#[macro_export] +#[doc(hidden)] +macro_rules! auto_nest_tuple { + // direct expansion + () => { () }; + ($a:expr) => { + $a + }; + ($a:expr, $b:expr) => { + ( + $a, + $b, + ) + }; + ($a:expr, $b:expr, $c:expr) => { + ( + $a, + $b, + $c, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr) => { + ( + $a, + $b, + $c, + $d, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + $j, + ) + }; + ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + $j, + $k, + ) + }; + + // recursive expansion + ( + $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, + $g:expr, $h:expr, $i:expr, $j:expr, $k:expr, $($rest:expr),* + ) => { + ( + $a, + $b, + $c, + $d, + $e, + $f, + $g, + $h, + $i, + $j, + $k, + $crate::auto_nest_tuple!($($rest),*) + ) + }; +} + +/// This is used by the [`bsn!`](crate::bsn) derive to work around [this Rust limitation](https://github.com/rust-lang/rust/issues/86935). +/// A fix is implemented and on track for stabilization. If it is ever implemented, we can remove this. +pub type PathResolveHelper = T; diff --git a/crates/bevy_scene2/src/resolved_scene.rs b/crates/bevy_scene2/src/resolved_scene.rs new file mode 100644 index 0000000000..a2b968d69e --- /dev/null +++ b/crates/bevy_scene2/src/resolved_scene.rs @@ -0,0 +1,430 @@ +use crate::{ResolveContext, ScenePatch}; +use bevy_asset::{AssetId, AssetPath, Assets, Handle, UntypedAssetId}; +use bevy_ecs::{ + bundle::Bundle, + entity::Entity, + error::{BevyError, Result}, + relationship::Relationship, + template::{ + EntityScopes, ErasedTemplate, ScopedEntities, ScopedEntityIndex, Template, TemplateContext, + }, + world::{EntityWorldMut, World}, +}; +use bevy_utils::TypeIdMap; +use core::any::TypeId; +use thiserror::Error; + +/// A final "spawnable" root [`ResolvedScene`]. This includes the [`EntityScopes`] for the whole tree. +pub struct ResolvedSceneRoot { + /// The root [`ResolvedScene`]. + pub scene: ResolvedScene, + /// The [`EntityScopes`] associated with the `root` [`ResolvedScene`]. + pub entity_scopes: EntityScopes, +} + +impl ResolvedSceneRoot { + /// This will spawn a new [`Entity`], then call [`ResolvedSceneRoot::apply`] on it. + pub fn spawn<'w>(&self, world: &'w mut World) -> Result, ApplySceneError> { + let mut entity = world.spawn_empty(); + self.apply(&mut entity)?; + Ok(entity) + } + + /// Applies this scene to the given [`EntityWorldMut`]. + /// + /// This will apply all of the [`Template`]s in this root [`ResolvedScene`] to the entity. It will also + /// spawn all of this [`ResolvedScene`]'s related entities. + /// + /// If this root [`ResolvedScene`] inherits from another scene, that scene will be applied _first_. + pub fn apply(&self, entity: &mut EntityWorldMut) -> Result<(), ApplySceneError> { + let mut scoped_entities = ScopedEntities::new(self.entity_scopes.entity_len()); + self.scene.apply(&mut TemplateContext::new( + entity, + &mut scoped_entities, + &self.entity_scopes, + )) + } +} + +/// A final "spawnable" root list of [`ResolvedScene`]s. This includes the [`EntityScopes`] for the whole graph of entities. +pub struct ResolvedSceneListRoot { + /// The root [`ResolvedScene`] list. + pub scenes: Vec, + /// The [`EntityScopes`] associated with the `root` [`ResolvedScene`]. + pub entity_scopes: EntityScopes, +} + +impl ResolvedSceneListRoot { + /// Spawns a new [`Entity`] for each [`ResolvedScene`] in the list, and calls [`ResolvedScene::apply`] on them. + pub fn spawn<'w>(&self, world: &'w mut World) -> Result, ApplySceneError> { + self.spawn_with(world, |_| {}) + } + + pub(crate) fn spawn_with( + &self, + world: &mut World, + func: impl Fn(&mut EntityWorldMut), + ) -> Result, ApplySceneError> { + let mut entities = Vec::new(); + let mut scoped_entities = ScopedEntities::new(self.entity_scopes.entity_len()); + for scene in self.scenes.iter() { + let mut entity = if let Some(scoped_entity_index) = + scene.entity_indices.first().copied() + { + let entity = scoped_entities.get(world, &self.entity_scopes, scoped_entity_index); + world.entity_mut(entity) + } else { + world.spawn_empty() + }; + + func(&mut entity); + entities.push(entity.id()); + scene.apply(&mut TemplateContext::new( + &mut entity, + &mut scoped_entities, + &self.entity_scopes, + ))?; + } + + Ok(entities) + } +} + +/// A final resolved scene (usually produced by calling [`Scene::resolve`]). This consists of: +/// 1. A collection of [`Template`]s to apply to a spawned [`Entity`], which are stored as [`ErasedTemplate`]s. +/// 2. A collection of [`RelatedResolvedScenes`], which will be spawned as "related" entities (ex: [`Children`] entities). +/// 3. The inherited [`ScenePatch`] if it exists. +/// +/// This uses "copy-on-write" behavior for inherited scenes. If a [`Template`] that the inherited scene has is requested, it will be +/// cloned (using [`Template::clone_template`]) and added to the current [`ResolvedScene`]. +/// +/// When applying this [`ResolvedScene`] to an [`Entity`], the inherited scene (including its children) is applied _first_. _Then_ this +/// [`ResolvedScene`] is applied. +/// +/// [`Scene::resolve`]: crate::Scene::resolve +/// [`Children`]: bevy_ecs::hierarchy::Children +#[derive(Default)] +pub struct ResolvedScene { + /// The collection of [`Template`]s to apply to a spawned [`Entity`]. This can have multiple copies of the same [`Template`]. + templates: Vec>, + /// The collection of [`RelatedResolvedScenes`], which will be spawned as "related" entities (ex: [`Children`] entities). + /// + /// [`Children`]: bevy_ecs::hierarchy::Children + // PERF: special casing Children might make sense here to avoid hashing + related: TypeIdMap, + /// The inherited [`ScenePatch`] to apply _first_ before applying this [`ResolvedScene`]. + inherited: Option>, + /// A [`TypeId`] to `templates` index mapping. If a [`Template`] is intended to be shared / patched across scenes, it should be registered + /// here. + template_indices: TypeIdMap, + /// A list of all [`ScopedEntityIndex`] values associated with this entity. There can be more than one if this scene uses + /// "flattened" inheritance. + pub entity_indices: Vec, +} + +impl core::fmt::Debug for ResolvedScene { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ResolvedScene") + .field("inherited", &self.inherited) + .field("template_types", &self.template_indices.keys()) + .field("related", &self.related) + .field("entity_indices", &self.entity_indices) + .finish() + } +} + +impl ResolvedScene { + /// Applies this scene to the given [`TemplateContext`] (which holds an already-spawned [`EntityWorldMut`]). + /// + /// This will apply all of the [`Template`]s in this [`ResolvedScene`] to the entity in the [`TemplateContext`]. It will also + /// spawn all of this [`ResolvedScene`]'s related entities. + /// + /// If this [`ResolvedScene`] inherits from another scene, that scene will be applied _first_. + pub fn apply(&self, context: &mut TemplateContext) -> Result<(), ApplySceneError> { + if let Some(inherited) = &self.inherited { + let scene_patches = context.resource::>(); + let Some(patch) = scene_patches.get(inherited) else { + return Err(ApplySceneError::MissingInheritedScene { + path: inherited.path().cloned(), + id: inherited.id(), + }); + }; + let Some(resolved_inherited) = &patch.resolved else { + return Err(ApplySceneError::UnresolvedInheritedScene { + path: inherited.path().cloned(), + id: inherited.id(), + }); + }; + let resolved_inherited = resolved_inherited.clone(); + resolved_inherited.apply(context.entity).map_err(|e| { + ApplySceneError::InheritedSceneApplyError { + inherited: inherited.path().cloned(), + error: Box::new(e), + } + })?; + } + + if let Some(scoped_entity_index) = self.entity_indices.first().copied() { + context.scoped_entities.set( + context.entity_scopes, + scoped_entity_index, + context.entity.id(), + ); + } + for template in &self.templates { + template + .apply(context) + .map_err(ApplySceneError::TemplateBuildError)?; + } + + for related_resolved_scenes in self.related.values() { + let target = context.entity.id(); + context + .entity + .world_scope(|world| -> Result<(), ApplySceneError> { + for (index, scene) in related_resolved_scenes.scenes.iter().enumerate() { + let mut entity = if let Some(scoped_entity_index) = + scene.entity_indices.first().copied() + { + let entity = context.scoped_entities.get( + world, + context.entity_scopes, + scoped_entity_index, + ); + world.entity_mut(entity) + } else { + world.spawn_empty() + }; + (related_resolved_scenes.insert)(&mut entity, target); + // PERF: this will result in an archetype move + scene + .apply(&mut TemplateContext::new( + &mut entity, + context.scoped_entities, + context.entity_scopes, + )) + .map_err(|e| ApplySceneError::RelatedSceneError { + relationship_type_name: related_resolved_scenes.relationship_name, + index, + error: Box::new(e), + })?; + } + Ok(()) + })?; + } + + Ok(()) + } + + /// This will get the [`Template`], if it already exists in this [`ResolvedScene`]. If it doesn't exist, + /// it will use [`Default`] to create a new [`Template`]. + /// + /// This uses "copy-on-write" behavior for inherited scenes. If a [`Template`] that the inherited scene has is requested, it will be + /// cloned (using [`Template::clone_template`]), added to the current [`ResolvedScene`], and returned. + /// + /// This will ignore [`Template`]s added to this scene using [`ResolvedScene::push_template`], as these are not registered as the "canonical" + /// [`Template`] for a given [`TypeId`]. + pub fn get_or_insert_template< + 'a, + T: Template + Default + Send + Sync + 'static, + >( + &'a mut self, + context: &mut ResolveContext, + ) -> &'a mut T { + self.get_or_insert_erased_template(context, TypeId::of::(), || Box::new(T::default())) + .downcast_mut() + .unwrap() + } + + /// This will get the [`ErasedTemplate`] for the given [`TypeId`], if it already exists in this [`ResolvedScene`]. If it doesn't exist, + /// it will use the `default` function to create a new [`ErasedTemplate`]. _For correctness, the [`TypeId`] of the [`Template`] returned + /// by `default` should match the passed in `type_id`_. + /// + /// This uses "copy-on-write" behavior for inherited scenes. If a [`Template`] that the inherited scene has is requested, it will be + /// cloned (using [`Template::clone_template`]), added to the current [`ResolvedScene`], and returned. + /// + /// This will ignore [`Template`]s added to this scene using [`ResolvedScene::push_template`], as these are not registered as the "canonical" + /// [`Template`] for a given [`TypeId`]. + pub fn get_or_insert_erased_template<'a>( + &'a mut self, + context: &mut ResolveContext, + type_id: TypeId, + default: fn() -> Box, + ) -> &'a mut dyn ErasedTemplate { + self.internal_get_or_insert_template_with(type_id, || { + if let Some(inherited_scene) = context.inherited + && let Some(resolved_inherited) = &inherited_scene.resolved + && let Some(inherited_template) = + resolved_inherited.scene.get_direct_erased_template(type_id) + { + inherited_template.clone_template() + } else { + default() + } + }) + } + + fn internal_get_or_insert_template_with( + &mut self, + type_id: TypeId, + get_value: impl FnOnce() -> Box, + ) -> &mut dyn ErasedTemplate { + let index = self.template_indices.entry(type_id).or_insert_with(|| { + let index = self.templates.len(); + self.templates.push(get_value()); + index + }); + self.templates + .get_mut(*index) + .map(|value| &mut **value) + .unwrap() + } + + /// Returns the [`ErasedTemplate`] for the given `type_id`, if it exists in this [`ResolvedScene`]. This ignores scene inheritance. + pub fn get_direct_erased_template(&self, type_id: TypeId) -> Option<&dyn ErasedTemplate> { + let index = self.template_indices.get(&type_id)?; + Some(&*self.templates[*index]) + } + + /// Adds the `template` to the "back" of the [`ResolvedScene`] (it will applied later than earlier [`Template`]s). + pub fn push_template + Send + Sync + 'static>( + &mut self, + template: T, + ) { + self.push_template_erased(Box::new(template)); + } + + /// Adds the `template` to the "back" of the [`ResolvedScene`] (it will applied later than earlier [`Template`]s). + pub fn push_template_erased(&mut self, template: Box) { + self.templates.push(template); + } + + /// This will return the existing [`RelatedResolvedScenes`], if it exists. If not, a new empty [`RelatedResolvedScenes`] will be inserted and returned. + /// + /// This is used to add new related scenes and read existing related scenes. + pub fn get_or_insert_related_resolved_scenes( + &mut self, + ) -> &mut RelatedResolvedScenes { + self.related + .entry(TypeId::of::()) + .or_insert_with(RelatedResolvedScenes::new::) + } + + /// Configures this [`ResolvedScene`] to inherit from the given [`ScenePatch`]. + /// + /// If this [`ResolvedScene`] already inherits from a scene, it will return [`InheritSceneError::MultipleInheritance`]. + /// If this [`ResolvedScene`] already has [`Template`]s or related scenes, it will return [`InheritSceneError::LateInheritance`]. + pub fn inherit(&mut self, handle: Handle) -> Result<(), InheritSceneError> { + if let Some(inherited) = &self.inherited { + return Err(InheritSceneError::MultipleInheritance { + id: inherited.id().untyped(), + path: inherited.path().cloned(), + }); + } + if !(self.templates.is_empty() && self.related.is_empty()) { + return Err(InheritSceneError::LateInheritance { + id: handle.id().untyped(), + path: handle.path().cloned(), + }); + } + self.inherited = Some(handle); + Ok(()) + } +} + +/// The error returned by [`ResolvedScene::inherit`]. +#[derive(Error, Debug)] +pub enum InheritSceneError { + /// Caused when attempting to inherit from a second scene. + #[error("Attempted to inherit from a second scene (id {id:?}, path: {path:?}), which is not allowed.")] + MultipleInheritance { + /// The asset id of the second inherited scene. + id: UntypedAssetId, + /// The path of the second inherited scene. + path: Option>, + }, + /// Caused when attempting to inherit when a [`ResolvedScene`] already has [`Template`]s or related scenes. + #[error("Attempted to inherit from (id {id:?}, path: {path:?}), but the resolved scene already has templates. For correctness, inheritance should always come first.")] + LateInheritance { + /// The asset id of the scene that was inherited late. + id: UntypedAssetId, + /// The path of the scene that was inherited late. + path: Option>, + }, +} + +/// An error produced when calling [`ResolvedScene::apply`]. +#[derive(Error, Debug)] +pub enum ApplySceneError { + /// Caused when a [`Template`] fails to build + #[error("Failed to build a Template in the current Scene: {0}")] + TemplateBuildError(BevyError), + /// Caused when the inherited [`ResolvedScene`] fails to [`ResolvedScene::apply`]. + #[error("Failed to apply the inherited Scene (asset path: \"{inherited:?}\"): {error}")] + InheritedSceneApplyError { + /// The asset path of the inherited scene that failed to apply. + inherited: Option>, + /// The error that occurred while applying the inherited scene. + error: Box, + }, + /// Caused when an inherited scene is not present. + #[error("The inherited scene (id: {id:?}, path: \"{path:?}\") does not exist.")] + MissingInheritedScene { + /// The path of the inherited scene. + path: Option>, + /// The asset id of the inherited scene. + id: AssetId, + }, + /// Caused when an inherited scene has not been resolved yet. + #[error("The inherited scene (id: {id:?}, path: \"{path:?}\") has not been resolved yet.")] + UnresolvedInheritedScene { + /// The path of the inherited scene. + path: Option>, + /// The asset id of the inherited scene. + id: AssetId, + }, + /// Caused when a related [`ResolvedScene`] fails to [`ResolvedScene::apply`]. + #[error( + "Failed to apply the related {relationship_type_name} Scene at index {index}: {error}" + )] + RelatedSceneError { + /// The type name of the relationship. + relationship_type_name: &'static str, + /// The index of the related scene that failed to apply. + index: usize, + /// The error that occurred when applying the related scene. + error: Box, + }, +} + +/// A collection of [`ResolvedScene`]s that are related to a given [`ResolvedScene`] by a [`Relationship`]. +/// Each [`ResolvedScene`] added here will be spawned as a new [`Entity`] when the "parent" [`ResolvedScene`] is spawned. +pub struct RelatedResolvedScenes { + /// The related resolved scenes. Each entry in the list corresponds to a new related entity that will be spawned with the given scene. + pub scenes: Vec, + /// The function that will be called to add the relationship to the spawned scene. + pub insert: fn(&mut EntityWorldMut, target: Entity), + /// The type name of the relationship. This is used for more helpful error message. + pub relationship_name: &'static str, +} + +impl core::fmt::Debug for RelatedResolvedScenes { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ResolvedRelatedScenes") + .field("scenes", &self.scenes) + .finish() + } +} + +impl RelatedResolvedScenes { + /// Creates a new empty [`RelatedResolvedScenes`] for the given relationship type. + pub fn new() -> Self { + Self { + scenes: Vec::new(), + insert: |entity, target| { + entity.insert(R::from(target)); + }, + relationship_name: core::any::type_name::(), + } + } +} diff --git a/crates/bevy_scene2/src/scene.rs b/crates/bevy_scene2/src/scene.rs new file mode 100644 index 0000000000..12dd7adaf4 --- /dev/null +++ b/crates/bevy_scene2/src/scene.rs @@ -0,0 +1,479 @@ +use crate::{InheritSceneError, ResolvedScene, SceneList, ScenePatch}; +use bevy_asset::{Asset, AssetPath, AssetServer, Assets}; +use bevy_ecs::{ + bundle::Bundle, + error::Result, + event::EntityEvent, + name::Name, + relationship::Relationship, + system::IntoObserverSystem, + template::{ + EntityScopes, FnTemplate, FromTemplate, ScopedEntityIndex, Template, TemplateContext, + }, +}; +use core::{any::TypeId, marker::PhantomData}; +use thiserror::Error; +use variadics_please::all_tuples; + +/// Conceptually, a [`Scene`] describes what a spawned [`Entity`] should look like. This often describes what [`Component`]s the entity should have. +/// +/// [`Scene`] is _always_ a single top level [`Entity`] / root entity. For "lists of scenes" / multiple "root" entities, see [`SceneList`]. These are +/// separate traits for logical reasons: [`World::spawn`] is a "single entity" action. Additionally, "scene inheritance" only makes sense when both scenes +/// are "single root entities". A good way to think of this is [`Entity`] vs [`Vec`]: these are different types with different APIs and semantics. +/// +/// ## Resolving Scenes +/// +/// Functionally, a [`Scene`] is something that can contribute to a [`ResolvedScene`] by calling [`Scene::resolve`]. [`Scene`] is inherently composable. +/// A collection of [`Scene`]s is essentially a description of what a final [`ResolvedScene`] should look like. This is typically done with +/// tuples of [`Scene`]s (which also implement [`Scene`]). +/// +/// A [`Scene`] generally does one or more of the following to a [`ResolvedScene`]: +/// - Adding a new [`Template`] +/// - Editing an existing [`Template`] (ex: "patching" [`Template`] fields) +/// - Adding one or more "related" [`ResolvedScene`]s, which will be spawned alongside the root [`ResolvedScene`] and "related" back to it with a [`Relationship`]. +/// - Editing an existing "related" [`ResolvedScene`]. +/// - Setting a [`ScenePatch`] to inherit from. +/// +/// See [`ResolvedScene`] for more information on how it can be composed. +/// +/// A [`Scene`] can have dependencies (defined with [`Scene::register_dependencies`]), which _must_ be loaded before calling [`Scene::resolve`], or it +/// might return a [`ResolveSceneError`]. +/// +/// You generally don't need to resolve [`Scene`]s yourself. Instead use APIs like [`World::spawn_scene`] or [`World::queue_spawn_scene`] +/// +/// [`World::spawn`]: crate::World::spawn +/// [`World::spawn_scene`]: crate::WorldSceneExt::spawn_scene +/// [`World::queue_spawn_scene`]: crate::WorldSceneExt::queue_spawn_scene +/// [`Entity`]: bevy_ecs::entity::Entity +/// [`Component`]: bevy_ecs::component::Component +pub trait Scene: Send + Sync + 'static { + /// This will apply the changes described in this [`Scene`] to the given [`ResolvedScene`]. This should not be called until all of the dependencies + /// in [`Scene::register_dependencies`] have been loaded. The scene system will generally call this method on behalf of developers. + /// + /// [`Scene`]s are free to modify [`ResolvedScene`] in arbitrary ways. In the context of related entities, in general they should just be pushing new + /// entities to the back of the list. + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError>; + + /// [`Scene`] can have [`Asset`] dependencies, which _must_ be loaded before calling [`Scene::resolve`] or it might return a [`ResolveSceneError`]! + /// + /// In most cases, the scene system will ensure [`Scene::resolve`] is called _after_ these dependencies have been loaded. + /// + /// [`Asset`]: bevy_asset::Asset + fn register_dependencies(&self, _dependencies: &mut SceneDependencies) {} +} + +/// A collection of asset dependencies required by a [`Scene`]. +#[derive(Default)] +pub struct SceneDependencies(Vec); + +impl SceneDependencies { + /// Registers a new asset dependency with the given `type_id` and `path`. The `type_id` should match + /// the type of the asset being loaded. + pub fn register_erased(&mut self, type_id: TypeId, path: AssetPath<'static>) { + self.0.push(SceneDependency { path, type_id }); + } + + /// Registers a new asset dependency with the given `A` type and `path`. `A` should match + /// the type of the asset being loaded. + pub fn register(&mut self, path: AssetPath<'static>) { + self.register_erased(TypeId::of::(), path); + } + + /// Iterates the current dependencies. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + +/// An asset dependency of a [`Scene`]. +pub struct SceneDependency { + /// The path of the asset. + pub path: AssetPath<'static>, + /// The type of the asset. + pub type_id: TypeId, +} + +/// An [`Error`] that occurs during [`Scene::resolve`]. +#[derive(Error, Debug)] +pub enum ResolveSceneError { + /// Caused when a dependency listed in [`Scene::register_dependencies`] is not available when calling [`Scene::resolve`] + #[error("Cannot resolve scene because the asset dependency {0} is not present. This could be because it isn't loaded yet, or because the asset does not exist. Consider using `queue_spawn_scene()` if you would like to wait for scene dependencies before spawning.")] + MissingSceneDependency(AssetPath<'static>), + /// Caused when inheriting a scene during [`Scene::resolve`] fails. + #[error(transparent)] + InheritSceneError(#[from] InheritSceneError), +} + +/// Context used by [`Scene`] implementations during [`Scene::resolve`]. +pub struct ResolveContext<'a> { + /// The current asset server + pub assets: &'a AssetServer, + /// The current [`ScenePatch`] asset collection + pub patches: &'a Assets, + /// The currently inherited [`ScenePatch`], if there is one. + pub inherited: Option<&'a ScenePatch>, + pub(crate) entity_scopes: &'a mut EntityScopes, + pub(crate) current_scope: usize, +} + +impl<'a> ResolveContext<'a> { + /// The current entity scope. + #[inline] + pub fn current_entity_scope(&self) -> usize { + self.current_scope + } + + /// Creates a new entity scope, which is active for the duration of `func`. When this function returns, + /// the original scope will be returned to. + pub fn new_entity_scope(&mut self, func: impl FnOnce(&mut ResolveContext) -> T) -> T { + let current_scope = self.entity_scopes.add_scope(); + let mut context = ResolveContext { + assets: self.assets, + patches: self.patches, + inherited: None, + entity_scopes: self.entity_scopes, + current_scope, + }; + (func)(&mut context) + } +} + +macro_rules! scene_impl { + ($($patch: ident),*) => { + impl<$($patch: Scene),*> Scene for ($($patch,)*) { + fn resolve(&self, _context: &mut ResolveContext, _scene: &mut ResolvedScene) -> Result<(), ResolveSceneError> { + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro, and as such, may not trigger in all cases." + )] + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($patch,)*) = self; + $($patch.resolve(_context, _scene)?;)* + Ok(()) + } + + fn register_dependencies(&self, _dependencies: &mut SceneDependencies) { + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro, and as such, may not trigger in all cases." + )] + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($patch,)*) = self; + $($patch.register_dependencies(_dependencies);)* + } + } + } +} + +all_tuples!(scene_impl, 0, 12, P); + +/// A [`Scene`] that patches a [`Template`] of type `T` with a given function `F`. +/// +/// Functionally, a [`TemplatePatch`] scene will initialize a [`Default`] value of the patched +/// template if it does not already exist in the [`ResolvedScene`], then it will apply the patch on top +/// of the current [`Template`] in the [`ResolvedScene`]. +/// +/// This is usually created by the [`PatchTemplate`] or [`PatchFromTemplate`] traits. +/// +/// This enables defining things like "field" patches, which set specific fields without overriding +/// any other fields: +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_scene2::PatchFromTemplate; +/// #[derive(FromTemplate)] +/// struct Position { +/// x: usize, +/// y: usize, +/// } +/// +/// let patch = Position::patch(|position_template, context| { +/// position_template.x = 10; +/// }); +/// +/// let position = Position { x: 0, y: 0}; +/// // applying patch to position would result in { x: 10, y: 0 } +/// ``` +pub struct TemplatePatch(pub F, pub PhantomData); + +/// Returns a [`Scene`] that completely overwrites the current value of a [`Template`] `T` with the given `value`. +/// The `value` is cloned each time the [`Template`] is built. +pub fn template_value( + value: T, +) -> TemplatePatch { + TemplatePatch( + move |input: &mut T, _context: &mut ResolveContext| { + *input = value.clone(); + }, + PhantomData, + ) +} + +/// A helper function that returns a [`TemplatePatch`] [`Scene`] for something that implements [`FromTemplate`]. +/// It will use [`FromTemplate::Template`] as the "patched template". +pub trait PatchFromTemplate { + /// The [`Template`] that will be patched. + type Template; + + /// Takes a "patch function" `func`, and turns it into a [`TemplatePatch`]. + fn patch( + func: F, + ) -> TemplatePatch; +} + +impl PatchFromTemplate for G { + type Template = G::Template; + fn patch( + func: F, + ) -> TemplatePatch { + TemplatePatch(func, PhantomData) + } +} + +/// A helper function that returns a [`TemplatePatch`] [`Scene`] for something that implements [`Template`]. +pub trait PatchTemplate: Sized { + /// Takes a "patch function" `func` that patches this [`Template`], and turns it into a [`TemplatePatch`]. + fn patch_template(func: F) -> TemplatePatch; +} + +impl PatchTemplate for T { + fn patch_template(func: F) -> TemplatePatch { + TemplatePatch(func, PhantomData) + } +} + +impl< + F: Fn(&mut T, &mut ResolveContext) + Send + Sync + 'static, + T: Template + Send + Sync + Default + 'static, + > Scene for TemplatePatch +{ + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + let template = scene.get_or_insert_template::(context); + (self.0)(template, context); + Ok(()) + } +} + +/// A [`Scene`] that adds an `L` [`SceneList`] as "related scenes", using the `R` [`Relationship`] +pub struct RelatedScenes { + /// The related [`SceneList`]. Each entity described in the list will be spawned with the given [`Relationship`] to the + /// entity described in the current [`Scene`]. + pub related_template_list: L, + + /// Marker holding the `R` type. + pub marker: PhantomData, +} + +impl RelatedScenes { + /// Creates a new [`RelatedScenes`] with the given `list`. + pub fn new(list: L) -> Self { + Self { + related_template_list: list, + marker: PhantomData, + } + } +} + +impl Scene for RelatedScenes { + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + let related = scene.get_or_insert_related_resolved_scenes::(); + self.related_template_list + .resolve_list(context, &mut related.scenes) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + self.related_template_list + .register_dependencies(dependencies); + } +} + +/// A [`Scene`] that will inherit from the [`ScenePatch`] stored at the given [`AssetPath`]. +/// This will _not_ resolve the inherited scene directly on top of this [`ResolvedScene`]. Instead +/// it will set [`ResolvedScene::inherit`], which (when spawning the [`ResolvedScene`]) will apply the inherited [`ResolvedScene`] +/// first. _Then_ the top-level [`ResolvedScene`] will be applied. +/// +/// This also enables copy-on-write semantics for all future [`Template`] accesses. See [`ResolvedScene`] for more info on "inheritance". +#[derive(Clone)] +pub struct InheritSceneAsset( + /// The [`AssetPath`] of the [`ScenePatch`] to inherit from. + pub AssetPath<'static>, +); + +impl>> From for InheritSceneAsset { + fn from(value: I) -> Self { + InheritSceneAsset(value.into()) + } +} + +impl Scene for InheritSceneAsset { + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + if let Some(handle) = context.assets.get_handle::(&self.0) + && let Some(scene_patch) = context.patches.get(&handle) + { + scene.inherit(handle)?; + context.inherited = Some(scene_patch); + Ok(()) + } else { + Err(ResolveSceneError::MissingSceneDependency(self.0.clone())) + } + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + dependencies.register::(self.0.clone()); + } +} + +impl Result) + Clone + Send + Sync + 'static, O: Bundle> Scene + for FnTemplate +{ + fn resolve( + &self, + _context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + scene.push_template(FnTemplate(self.0.clone())); + Ok(()) + } +} + +/// Sets up a given name as an "entity reference" for the current entity. This pairs the [`Self::name`] field +/// to a given [`Self::index`] field. +/// +/// The `index` should be a dense, unique identifier (within the current "entity scope") that can be used to reference this entity. +/// Usually this is not set manually by a user. Instead this is generally done by a macro (such as the [`bsn!`] macro) or an asset loader +/// (such as the BSN asset loader). +/// +/// [`bsn!`]: crate::bsn +pub struct NameEntityReference { + /// The name to give this entity. + pub name: Name, + /// The index (within the current "entity scope") of this entity reference. + pub index: usize, +} + +impl Scene for NameEntityReference { + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + let this_index = ScopedEntityIndex { + scope: context.current_entity_scope(), + index: self.index, + }; + if let Some(first_index) = scene.entity_indices.first().copied() { + let consolidated_index = context.entity_scopes.get(first_index).unwrap(); + context.entity_scopes.assign(this_index, consolidated_index); + } else { + context.entity_scopes.alloc(this_index); + } + scene.entity_indices.push(this_index); + let name = scene.get_or_insert_template::(context); + *name = self.name.clone(); + Ok(()) + } +} + +/// A [`Scene`] that will create a new "entity scope" and fully resolve the given scene `S` on top of the current [`ResolvedScene`] (using that scope). +/// It is not "inherited" or cached. +pub struct SceneScope(pub S); + +impl Scene for SceneScope { + fn resolve( + &self, + context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + context.new_entity_scope(|context| self.0.resolve(context, scene)) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + self.0.register_dependencies(dependencies); + } +} + +/// A [`SceneList`] that will create a new "entity scope" and fully resolve the given scene list `L` on top of the current [`Vec`] +/// (using that scope). It is not "inherited" or cached. +pub struct SceneListScope(pub L); + +impl SceneList for SceneListScope { + fn resolve_list( + &self, + context: &mut ResolveContext, + scenes: &mut Vec, + ) -> Result<(), ResolveSceneError> { + context.new_entity_scope(|context| self.0.resolve_list(context, scenes)) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + self.0.register_dependencies(dependencies); + } +} + +/// A [`Template`] / [`Scene`] that will create an [`Observer`] of a given [`EntityEvent`] on the current [`Scene`] entity. +/// This is typically initialized using the [`on()`] function, which returns an [`OnTemplate`]. +/// +/// [`Observer`]: bevy_ecs::observer::Observer +pub struct OnTemplate(pub I, pub PhantomData (E, B, M)>); + +impl + Clone, E: EntityEvent, B: Bundle, M: 'static> Template + for OnTemplate +{ + type Output = (); + + fn build_template(&self, context: &mut TemplateContext) -> Result { + context.entity.observe(self.0.clone()); + Ok(()) + } + + fn clone_template(&self) -> Self { + Self(self.0.clone(), PhantomData) + } +} + +impl< + I: IntoObserverSystem + Clone + Send + Sync, + E: EntityEvent, + B: Bundle, + M: 'static, + > Scene for OnTemplate +{ + fn resolve( + &self, + _context: &mut ResolveContext, + scene: &mut ResolvedScene, + ) -> Result<(), ResolveSceneError> { + scene.push_template(OnTemplate(self.0.clone(), PhantomData)); + Ok(()) + } +} + +/// Returns an [`OnTemplate`] that will create an [`Observer`] of a given [`EntityEvent`] on the current [`Scene`] entity. +/// +/// [`Observer`]: bevy_ecs::observer::Observer +pub fn on, E: EntityEvent, B: Bundle, M: 'static>( + observer: I, +) -> OnTemplate { + OnTemplate(observer, PhantomData) +} diff --git a/crates/bevy_scene2/src/scene_list.rs b/crates/bevy_scene2/src/scene_list.rs new file mode 100644 index 0000000000..d47039278f --- /dev/null +++ b/crates/bevy_scene2/src/scene_list.rs @@ -0,0 +1,122 @@ +use crate::{ResolveContext, ResolveSceneError, ResolvedScene, Scene, SceneDependencies}; +use variadics_please::all_tuples; + +/// This behaves like a list of [`Scene`], where each entry in the list is a new entity (see [`Scene`] for more details). +/// +/// [`Scene`] is to [`Entity`] as [`SceneList`] is to [`Vec`]. +/// +/// [`Entity`]: bevy_ecs::entity::Entity +pub trait SceneList: Send + Sync + 'static { + /// This will apply the changes described in this [`SceneList`] to the given [`Vec`]. This should not be called until all of + /// the dependencies in [`Scene::register_dependencies`] have been loaded. + fn resolve_list( + &self, + context: &mut ResolveContext, + scenes: &mut Vec, + ) -> Result<(), ResolveSceneError>; + + /// [`SceneList`] can have [`Asset`] dependencies, which _must_ be loaded before calling [`SceneList::resolve_list`] or it might return a + /// [`ResolveSceneError`]! + /// + /// [`Asset`]: bevy_asset::Asset + fn register_dependencies(&self, dependencies: &mut SceneDependencies); +} + +/// Corresponds to a single member of a [`SceneList`] (an [`Entity`] with an `S` [`Scene`]). +/// +/// [`Entity`]: bevy_ecs::entity::Entity +pub struct EntityScene(pub S); + +impl SceneList for EntityScene { + fn resolve_list( + &self, + context: &mut ResolveContext, + scenes: &mut Vec, + ) -> Result<(), ResolveSceneError> { + let mut resolved_scene = ResolvedScene::default(); + self.0.resolve(context, &mut resolved_scene)?; + scenes.push(resolved_scene); + Ok(()) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + self.0.register_dependencies(dependencies); + } +} + +macro_rules! scene_list_impl { + ($($list: ident),*) => { + impl<$($list: SceneList),*> SceneList for ($($list,)*) { + fn resolve_list(&self, _context: &mut ResolveContext, _scenes: &mut Vec) -> Result<(), ResolveSceneError> { + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro, and as such, may not trigger in all cases." + )] + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + $($list.resolve_list(_context, _scenes)?;)* + Ok(()) + } + + fn register_dependencies(&self, _dependencies: &mut SceneDependencies) { + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro, and as such, may not trigger in all cases." + )] + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + $($list.register_dependencies(_dependencies);)* + } + } + } +} + +all_tuples!(scene_list_impl, 0, 12, P); + +impl SceneList for Vec { + fn resolve_list( + &self, + context: &mut ResolveContext, + scenes: &mut Vec, + ) -> Result<(), ResolveSceneError> { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.resolve(context, &mut resolved_scene)?; + scenes.push(resolved_scene); + } + Ok(()) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + for scene in self { + scene.register_dependencies(dependencies); + } + } +} + +impl SceneList for Vec> { + fn resolve_list( + &self, + context: &mut ResolveContext, + scenes: &mut Vec, + ) -> Result<(), ResolveSceneError> { + for scene in self { + let mut resolved_scene = ResolvedScene::default(); + scene.resolve(context, &mut resolved_scene)?; + scenes.push(resolved_scene); + } + Ok(()) + } + + fn register_dependencies(&self, dependencies: &mut SceneDependencies) { + for scene in self { + scene.register_dependencies(dependencies); + } + } +} diff --git a/crates/bevy_scene2/src/scene_patch.rs b/crates/bevy_scene2/src/scene_patch.rs new file mode 100644 index 0000000000..bf898822d6 --- /dev/null +++ b/crates/bevy_scene2/src/scene_patch.rs @@ -0,0 +1,220 @@ +use crate::{ + ApplySceneError, ResolveContext, ResolveSceneError, ResolvedScene, ResolvedSceneListRoot, + ResolvedSceneRoot, Scene, SceneDependencies, SceneList, +}; +use alloc::sync::Arc; +use bevy_asset::{Asset, AssetServer, Assets, Handle, LoadFromPath, UntypedHandle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + template::EntityScopes, + world::{EntityWorldMut, World}, +}; +use bevy_reflect::TypePath; +use thiserror::Error; + +/// An [`Asset`] that holds a [`Scene`], tracks its dependencies, and holds the [`ResolvedScene`] (after the [`Scene`] has been loaded and resolved). +#[derive(Asset, TypePath)] +pub struct ScenePatch { + /// A [`Scene`]. + pub scene: Box, + /// The dependencies of `scene` (populated using [`Scene::register_dependencies`]). These are "asset dependencies" and will affect the load state. + #[dependency] + pub dependencies: Vec, + /// The [`ResolvedScene`], if exists. This is populated after the [`Scene`] has been loaded and resolved + // TODO: consider breaking this out to prevent mutating asset events when resolved. Assets as Entities will enable this! + // TODO: This Arc exists to allow nested ResolvedSceneRoot::apply when borrowing inherited ScenePatch assets (see the ResolvedSceneRoot::apply implementation). + pub resolved: Option>, +} + +impl ScenePatch { + /// Kicks off a load of the `scene`. This enumerates the scene's dependencies using [`Scene::register_dependencies`], loads + /// them using the given [`AssetServer`], and assigns the resulting asset handles to [`ScenePatch::dependencies`]. + pub fn load(mut assets: &AssetServer, scene: P) -> Self { + Self::load_with(&mut assets, scene) + } + + /// Same as [`Self::load`], but allows passing in any [`LoadFromPath`] impl for more general + /// loading cases. + pub fn load_with(load_from_path: &mut impl LoadFromPath, scene: P) -> Self { + let mut dependencies = SceneDependencies::default(); + scene.register_dependencies(&mut dependencies); + let dependencies = dependencies + .iter() + .map(|i| load_from_path.load_from_path_erased(i.type_id, i.path.clone())) + .collect::>(); + ScenePatch { + scene: Box::new(scene), + dependencies, + resolved: None, + } + } + + /// Resolves the current `scene` (using [`Scene::resolve`]). This should only be called after every dependency has loaded from the `scene`'s + /// [`Scene::register_dependencies`]. If successful, it will store the resolved result in [`ScenePatch::resolved`]. + pub fn resolve( + &mut self, + assets: &AssetServer, + patches: &Assets, + ) -> Result<(), ResolveSceneError> { + let resolved = self.resolve_internal(assets, patches)?; + self.resolved = Some(Arc::new(resolved)); + Ok(()) + } + + pub(crate) fn resolve_internal( + &self, + assets: &AssetServer, + patches: &Assets, + ) -> Result { + let mut scene = ResolvedScene::default(); + let mut entity_scopes = EntityScopes::default(); + self.scene.resolve( + &mut ResolveContext { + assets, + patches, + current_scope: 0, + entity_scopes: &mut entity_scopes, + inherited: None, + }, + &mut scene, + )?; + + Ok(ResolvedSceneRoot { + scene, + entity_scopes, + }) + } + + /// Spawns the scene in `world` as a new entity. This should only be called after [`ScenePatch::resolve`]. + pub fn spawn<'w>(&self, world: &'w mut World) -> Result, SpawnSceneError> { + let resolved = self + .resolved + .as_deref() + .ok_or(SpawnSceneError::UnresolvedSceneError)?; + resolved + .spawn(world) + .map_err(SpawnSceneError::ApplySceneError) + } + + /// Applies the scene to the given `entity`. This should only be called after [`ScenePatch::resolve`] + pub fn apply<'w>(&self, entity: &'w mut EntityWorldMut) -> Result<(), SpawnSceneError> { + let resolved = self + .resolved + .as_deref() + .ok_or(SpawnSceneError::UnresolvedSceneError)?; + resolved + .apply(entity) + .map_err(SpawnSceneError::ApplySceneError) + } +} + +/// An [`Error`] that occurs during scene spawning. +#[derive(Error, Debug)] +pub enum SpawnSceneError { + /// Calling [`ResolvedScene::apply`] failed. + #[error(transparent)] + ApplySceneError(#[from] ApplySceneError), + #[error(transparent)] + /// Calling [`Scene::resolve`] failed. + ResolveSceneError(#[from] ResolveSceneError), + /// Attempted to spawn a scene that has not been resolved yet. + #[error("This scene has not been resolved yet and cannot be spawned. It is likely waiting for dependencies to load")] + UnresolvedSceneError, +} + +/// A component that, when added, will queue applying the given [`ScenePatch`] after the scene and its dependencies have been loaded and resolved. +#[derive(Component, Deref, DerefMut)] +pub struct ScenePatchInstance(pub Handle); + +/// An [`Asset`] that holds a [`SceneList`], tracks its dependencies, and holds a [`Vec`] of [`ResolvedScene`] (after the [`SceneList`] has been loaded and resolved) +#[derive(Asset, TypePath)] +pub struct SceneListPatch { + /// A [`SceneList`]. + pub scene_list: Box, + + /// The dependencies of `scene_list` (populated using [`SceneList::register_dependencies`]). These are "asset dependencies" and will affect the load state. + #[dependency] + pub dependencies: Vec, + + /// The [`ResolvedSceneListRoot`], if exists. This is populated after the scene list and its dependencies have been loaded and resolved. + // TODO: consider breaking this out to prevent mutating asset events when resolved + pub resolved: Option, +} + +impl SceneListPatch { + /// Kicks off a load of the `scene_list`. This enumerates the scene list's dependencies using [`SceneList::register_dependencies`], loads + /// them using the given [`AssetServer`], and assigns the resulting asset handles to [`SceneListPatch::dependencies`]. + pub fn load(assets: &AssetServer, scene_list: L) -> Self { + let mut dependencies = SceneDependencies::default(); + scene_list.register_dependencies(&mut dependencies); + let dependencies = dependencies + .iter() + .map(|dep| assets.load_erased(dep.type_id, &dep.path)) + .collect::>(); + SceneListPatch { + scene_list: Box::new(scene_list), + dependencies, + resolved: None, + } + } + + /// Resolves the current `scene` (using [`SceneList::resolve_list`]). This should only be called after every dependency has loaded from the `scene_list`'s + /// [`SceneList::register_dependencies`]. + pub fn resolve( + &mut self, + assets: &AssetServer, + patches: &Assets, + ) -> Result<(), ResolveSceneError> { + let resolved = self.resolve_internal(assets, patches)?; + self.resolved = Some(resolved); + Ok(()) + } + + /// Resolves the current `scene` (using [`SceneList::resolve_list`]). This should only be called after every dependency has loaded from the `scene_list`'s + /// [`SceneList::register_dependencies`]. + pub(crate) fn resolve_internal( + &self, + assets: &AssetServer, + patches: &Assets, + ) -> Result { + let mut scenes = Vec::new(); + let mut entity_scopes = EntityScopes::default(); + self.scene_list.resolve_list( + &mut ResolveContext { + assets, + patches, + current_scope: 0, + entity_scopes: &mut entity_scopes, + inherited: None, + }, + &mut scenes, + )?; + + Ok(ResolvedSceneListRoot { + scenes, + entity_scopes, + }) + } + + /// Spawns the scene list in `world` as new entities. This should only be called after [`SceneListPatch::resolve`]. + pub fn spawn<'w>(&self, world: &'w mut World) -> Result, SpawnSceneError> { + self.spawn_with(world, |_| {}) + } + + /// Spawns the scene list in `world` as new entities. This should only be called after [`SceneListPatch::resolve`]. + pub(crate) fn spawn_with<'w>( + &self, + world: &'w mut World, + func: impl Fn(&mut EntityWorldMut), + ) -> Result, SpawnSceneError> { + let resolved = self + .resolved + .as_ref() + .ok_or(SpawnSceneError::UnresolvedSceneError)?; + resolved + .spawn_with(world, func) + .map_err(SpawnSceneError::ApplySceneError) + } +} diff --git a/crates/bevy_scene2/src/spawn.rs b/crates/bevy_scene2/src/spawn.rs new file mode 100644 index 0000000000..c8098a0851 --- /dev/null +++ b/crates/bevy_scene2/src/spawn.rs @@ -0,0 +1,828 @@ +use crate::{Scene, SceneList, SceneListPatch, ScenePatch, ScenePatchInstance, SpawnSceneError}; +use alloc::sync::Arc; +use bevy_asset::{AssetEvent, AssetServer, Assets, Handle}; +use bevy_ecs::{message::MessageCursor, prelude::*, relationship::Relationship}; +use bevy_platform::collections::HashMap; +use tracing::error; + +/// Adds scene spawning functionality to [`World`]. +pub trait WorldSceneExt { + /// Spawns the given [`Scene`] immediately. This will resolve the Scene (using [`Scene::resolve`]). If that fails (for example, if there are dependencies that have not been + /// loaded yet), it will return a [`SpawnSceneError`]. If resolving the [`Scene`] is successful, the scene will be spawned. + /// + /// If resolving and spawning is successful, it will return a new [`EntityWorldMut`] containing the full contents of the spawned scene. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// If your scene has a dependency that might not be loaded yet (for example, it inherits from a `.bsn` asset file), consider using [`World::queue_spawn_scene`]. + /// + /// ``` + /// # use bevy_app::App; + /// # use bevy_scene2::{prelude::*, ScenePlugin}; + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::AssetPlugin; + /// # use bevy_app::TaskPoolPlugin; + /// # let mut app = App::new(); + /// # app.add_plugins(( + /// # TaskPoolPlugin::default(), + /// # AssetPlugin::default(), + /// # ScenePlugin::default(), + /// # )); + /// # let world = app.world_mut(); + /// #[derive(Component, Default, Clone)] + /// struct Score(usize); + /// + /// #[derive(Component, Default, Clone)] + /// struct Sword; + /// + /// #[derive(Component, Default, Clone)] + /// struct Shield; + /// + /// world.spawn_scene(bsn! { + /// #Player + /// Score(0) + /// Children [ + /// Sword, + /// Shield, + /// ] + /// }).unwrap(); + /// ``` + fn spawn_scene(&mut self, scene: S) -> Result, SpawnSceneError>; + + /// Queues the `scene` to be spawned. This will evaluate the `scene`'s dependencies (via [`Scene::register_dependencies`]) and queue it to be resolved and spawned + /// after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene will be spawned this frame. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// ``` + /// # use bevy_app::App; + /// # use bevy_scene2::{prelude::*, ScenePlugin}; + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::AssetPlugin; + /// # use bevy_app::TaskPoolPlugin; + /// # let mut app = App::new(); + /// # app.add_plugins(( + /// # TaskPoolPlugin::default(), + /// # AssetPlugin::default(), + /// # ScenePlugin::default(), + /// # )); + /// # let world = app.world_mut(); + /// #[derive(Component, Default, Clone)] + /// struct Score(usize); + /// + /// #[derive(Component, Default, Clone)] + /// struct Sword; + /// + /// #[derive(Component, Default, Clone)] + /// struct Shield; + /// + /// // This scene inherits from the "player.bsn" asset. It will be spawned on the frame that "player.bsn" + /// // is fully loaded. + /// world.queue_spawn_scene(bsn! { + /// :"player.bsn" + /// #Player + /// Score(0) + /// Children [ + /// Sword, + /// Shield, + /// ] + /// }); + /// ``` + fn queue_spawn_scene(&mut self, scene: S) -> EntityWorldMut<'_>; + + /// Spawns the given [`SceneList`] immediately. This will resolve the scene list (using [`SceneList::resolve_list`]). If that fails (for example, if there are dependencies that have not been + /// loaded yet), it will return a [`SpawnSceneError`]. If resolving the [`SceneList`] is successful, the scene list will be spawned. + /// + /// If resolving and spawning is successful, it will return a [`Vec`] containing each entity described in the [`SceneList`]. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// If your scene list has a dependency that might not be loaded yet (for example, it inherits from a `.bsn` asset file), consider using [`World::queue_spawn_scene_list`]. + /// + /// ``` + /// # use bevy_app::App; + /// # use bevy_scene2::{prelude::*, ScenePlugin}; + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::AssetPlugin; + /// # use bevy_app::TaskPoolPlugin; + /// # let mut app = App::new(); + /// # app.add_plugins(( + /// # TaskPoolPlugin::default(), + /// # AssetPlugin::default(), + /// # ScenePlugin::default(), + /// # )); + /// # let world = app.world_mut(); + /// #[derive(Component, FromTemplate)] + /// enum Team { + /// #[default] + /// Red, + /// Blue, + /// } + /// + /// world.spawn_scene_list(bsn_list! { + /// ( + /// #Player1 + /// Team::Red + /// ), + /// ( + /// #Player2 + /// Team::Blue + /// ) + /// }).unwrap(); + /// ``` + // PERF: ideally this is an iterator + fn spawn_scene_list(&mut self, scenes: L) + -> Result, SpawnSceneError>; + + /// Queues the `scene_list` to be spawned. This will evaluate the `scene_list`'s dependencies (via [`Scene::register_dependencies`]) and queue it to be resolved + /// and spawned after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene list will be spawned this frame. + /// ``` + /// # use bevy_app::App; + /// # use bevy_scene2::{prelude::*, ScenePlugin}; + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::AssetPlugin; + /// # use bevy_app::TaskPoolPlugin; + /// # let mut app = App::new(); + /// # app.add_plugins(( + /// # TaskPoolPlugin::default(), + /// # AssetPlugin::default(), + /// # ScenePlugin::default(), + /// # )); + /// # let world = app.world_mut(); + /// #[derive(Component, FromTemplate)] + /// enum Team { + /// #[default] + /// Red, + /// Blue, + /// } + /// // This scene list inherits from the "player.bsn" asset. It will be spawned on the frame that "player.bsn" + /// // is loaded. + /// world.queue_spawn_scene_list(bsn_list! [ + /// ( + /// :"player.bsn" + /// #Player1 + /// Team::Red + /// ), + /// ( + /// :"player.bsn" + /// #Player2 + /// Team::Blue + /// ) + /// ]); + /// ``` + fn queue_spawn_scene_list(&mut self, scenes: L); +} + +impl WorldSceneExt for World { + fn spawn_scene(&mut self, scene: S) -> Result, SpawnSceneError> { + let assets = self.resource::(); + let mut patch = ScenePatch::load(assets, scene); + patch.resolve(assets, self.resource::>())?; + patch.spawn(self) + } + + fn queue_spawn_scene(&mut self, scene: S) -> EntityWorldMut<'_> { + let assets = self.resource::(); + let patch = ScenePatch::load(assets, scene); + let handle = assets.add(patch); + self.spawn(ScenePatchInstance(handle)) + } + + fn spawn_scene_list( + &mut self, + scenes: L, + ) -> Result, SpawnSceneError> { + let assets = self.resource::(); + let mut patch = SceneListPatch::load(assets, scenes); + patch.resolve(assets, self.resource::>())?; + patch.spawn(self) + } + + fn queue_spawn_scene_list(&mut self, scenes: L) { + let assets = self.resource::(); + let patch = SceneListPatch::load(assets, scenes); + let handle = assets.add(patch); + self.resource_mut::() + .scene_list_spawns + .push(handle); + } +} + +/// Adds scene spawning functionality to [`Commands`]. +pub trait CommandsSceneExt { + /// Spawns the given [`Scene`] as soon as [`Commands`] are applied. This will resolve the Scene (using [`Scene::resolve`]). If that fails (for example, if there are dependencies that have not been + /// loaded yet), it will log a [`SpawnSceneError`] as an error. If resolving the [`Scene`] is successful, the scene will be spawned. + /// + /// This is essentially a [`Command`] that runs [`World::spawn_scene`]. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// If your scene has a dependency that might not be loaded yet (for example, it inherits from a `.bsn` asset file), consider using [`Commands::queue_spawn_scene`]. + /// + /// ``` + /// # use bevy_scene2::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let mut commands = world.commands(); + /// #[derive(Component, Default, Clone)] + /// struct Score(usize); + /// + /// #[derive(Component, Default, Clone)] + /// struct Sword; + /// + /// #[derive(Component, Default, Clone)] + /// struct Shield; + /// + /// commands.spawn_scene(bsn! { + /// #Player + /// Score(0) + /// Children [ + /// Sword, + /// Shield, + /// ] + /// }); + /// ``` + fn spawn_scene(&mut self, scene: S) -> EntityCommands<'_>; + + /// Queues the `scene` to be spawned. This will evaluate the `scene`'s dependencies (via [`Scene::register_dependencies`]) and queue it to be resolved and spawned + /// after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene will be spawned this frame. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// ``` + /// # use bevy_scene2::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let mut commands = world.commands(); + /// #[derive(Component, Default, Clone)] + /// struct Score(usize); + /// + /// #[derive(Component, Default, Clone)] + /// struct Sword; + /// + /// #[derive(Component, Default, Clone)] + /// struct Shield; + /// + /// // This scene inherits from the "player.bsn" asset. It will be spawned on the frame that "player.bsn" + /// // is fully loaded. + /// commands.queue_spawn_scene(bsn! { + /// :"player.bsn" + /// #Player + /// Score(0) + /// Children [ + /// Sword, + /// Shield, + /// ] + /// }); + /// ``` + fn queue_spawn_scene(&mut self, scene: S) -> EntityCommands<'_>; + + /// Spawns the given [`SceneList`] as soon as [`Commands`] are applied. This will resolve the scene list (using [`SceneList::resolve_list`]). If that fails (for example, if there are dependencies that have not been + /// loaded yet), it will log a [`SpawnSceneError`] as an error. If resolving the [`Scene`] is successful, the scene list will be spawned. + /// + /// This is essentially a [`Command`] that performs [`World::spawn_scene_list`]. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// If your scene list has a dependency that might not be loaded yet (for example, it inherits from a `.bsn` asset file), consider using [`Commands::queue_spawn_scene_list`]. + /// + /// ``` + /// # use bevy_scene2::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let mut commands = world.commands(); + /// #[derive(Component, FromTemplate)] + /// enum Team { + /// #[default] + /// Red, + /// Blue, + /// } + /// + /// commands.spawn_scene_list(bsn_list! { + /// ( + /// :"player.bsn" + /// #Player1 + /// Team::Red + /// ), + /// ( + /// :"player.bsn" + /// #Player2 + /// Team::Blue + /// ) + /// }); + /// ``` + fn spawn_scene_list(&mut self, scenes: L); + + /// Queues the `scene_list` to be spawned. This will evaluate the `scene_list`'s dependencies (via [`Scene::register_dependencies`]) and queue it to be resolved + /// and spawned after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene will be spawned this frame. + /// + /// ``` + /// # use bevy_scene2::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let mut commands = world.commands(); + /// #[derive(Component, FromTemplate)] + /// enum Team { + /// #[default] + /// Red, + /// Blue, + /// } + /// + /// // This scene list inherits from the "player.bsn" asset. It will be spawned on the frame that "player.bsn" + /// // is loaded. + /// commands.queue_spawn_scene_list(bsn_list! [ + /// ( + /// :"player.bsn" + /// #Player1 + /// Team::Red + /// ), + /// ( + /// :"player.bsn" + /// #Player2 + /// Team::Blue + /// ) + /// ]); + /// ``` + fn queue_spawn_scene_list(&mut self, scenes: L); +} + +impl<'w, 's> CommandsSceneExt for Commands<'w, 's> { + fn spawn_scene(&mut self, scene: S) -> EntityCommands<'_> { + let mut entity_commands = self.spawn_empty(); + let id = entity_commands.id(); + entity_commands.commands().queue(move |world: &mut World| { + if let Ok(mut entity) = world.get_entity_mut(id) + && let Err(err) = entity.apply_scene(scene) + { + error!("{err}"); + } + }); + entity_commands + } + + fn queue_spawn_scene(&mut self, scene: S) -> EntityCommands<'_> { + let mut entity_commands = self.spawn_empty(); + let id = entity_commands.id(); + entity_commands.commands().queue(move |world: &mut World| { + if let Ok(mut entity) = world.get_entity_mut(id) { + entity.queue_apply_scene(scene); + } + }); + entity_commands + } + + fn spawn_scene_list(&mut self, scenes: L) { + self.queue(move |world: &mut World| { + if let Err(err) = world.spawn_scene_list(scenes) { + error!("{err}"); + } + }); + } + + fn queue_spawn_scene_list(&mut self, scenes: L) { + self.queue(move |world: &mut World| { + world.queue_spawn_scene_list(scenes); + }); + } +} + +/// Adds scene functionality to [`EntityWorldMut`]. +pub trait EntityWorldMutSceneExt { + /// Spawns a [`SceneList`], where each entity is related to the current entity using [`RelationshipTarget::Relationship`]. + /// + /// This will evaluate the `scene_list`'s dependencies (via [`SceneList::register_dependencies`]) and queue it to be resolved + /// and spawned after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene list will be spawned this frame. + /// + /// ``` + /// # use bevy_app::App; + /// # use bevy_scene2::{prelude::*, ScenePlugin}; + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::AssetPlugin; + /// # use bevy_app::TaskPoolPlugin; + /// # let mut app = App::new(); + /// # app.add_plugins(( + /// # TaskPoolPlugin::default(), + /// # AssetPlugin::default(), + /// # ScenePlugin::default(), + /// # )); + /// # let world = app.world_mut(); + /// #[derive(Component, FromTemplate)] + /// enum Team { + /// #[default] + /// Red, + /// Blue, + /// } + /// + /// world.spawn_empty().queue_spawn_related_scenes::(bsn_list! { + /// ( + /// #Player1 + /// Team::Red + /// ), + /// ( + /// #Player2 + /// Team::Blue + /// ) + /// }); + /// ``` + fn queue_spawn_related_scenes(self, scenes: impl SceneList) -> Self; + + /// Applies the given [`Scene`] to the current entity immediately. This will resolve the Scene (using [`Scene::resolve`]). If that fails (for example, if there are dependencies that have not been + /// loaded yet), it will return a [`SpawnSceneError`]. If resolving the [`Scene`] is successful, the scene will be spawned. + /// + /// If resolving and spawning is successful, the entity will contain the full contents of the spawned scene. + /// + /// This will write directly on top of any existing components on the entity. [`Scene`] is generally used as a spawning mechanism, so for most things, prefer using [`World::spawn_scene`]. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// If your scene has a dependency that might not be loaded yet (for example, it inherits from a `.bsn` asset file), consider using [`World::queue_spawn_scene`]. + fn apply_scene(&mut self, scene: S) -> Result<(), SpawnSceneError>; + + /// Queues the `scene` to be applied. This will evaluate the `scene`'s dependencies (via [`Scene::register_dependencies`]) and queue it to be resolved and spawned + /// after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene will be spawned this frame. + /// This will write directly on top of any existing components on the entity. [`Scene`] is generally used as a spawning mechanism, so for most things, prefer using [`World::queue_spawn_scene`]. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + fn queue_apply_scene(&mut self, scene: S); +} + +impl EntityWorldMutSceneExt for EntityWorldMut<'_> { + fn queue_spawn_related_scenes(mut self, scenes: impl SceneList) -> Self { + let assets = self.resource::(); + let patch = SceneListPatch::load(assets, scenes); + let handle = assets.add(patch); + let entity = self.id(); + self.resource_mut::() + .related_scene_list_spawns + .push(( + RelatedSceneListSpawn { + entity, + insert: |entity, target| { + entity.insert( + <::Relationship as Relationship>::from(target), + ); + }, + }, + handle, + )); + self + } + + fn apply_scene(&mut self, scene: S) -> Result<(), SpawnSceneError> { + let assets = self.resource::(); + let mut patch = ScenePatch::load(assets, scene); + patch.resolve(assets, self.resource::>())?; + patch.apply(self) + } + + fn queue_apply_scene(&mut self, scene: S) { + let assets = self.resource::(); + let patch = ScenePatch::load(assets, scene); + let handle = assets.add(patch); + self.insert(ScenePatchInstance(handle)); + } +} + +/// Adds scene functionality to [`EntityWorldMut`]. +pub trait EntityCommandsSceneExt { + /// Spawns a [`SceneList`], where each entity is related to the current entity using [`RelationshipTarget::Relationship`]. + /// + /// This will evaluate the `scene_list`'s dependencies (via [`SceneList::register_dependencies`]) and queue it to be resolved + /// and spawned after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene list will be spawned this frame. + /// + /// ``` + /// # use bevy_app::App; + /// # use bevy_scene2::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::AssetPlugin; + /// # use bevy_app::TaskPoolPlugin; + /// # let mut app = App::new(); + /// # let mut commands = app.world_mut().commands(); + /// #[derive(Component, FromTemplate)] + /// enum Team { + /// #[default] + /// Red, + /// Blue, + /// } + /// + /// commands.spawn_empty().queue_spawn_related_scenes::(bsn_list! { + /// ( + /// #Player1 + /// Team::Red + /// ), + /// ( + /// #Player2 + /// Team::Blue + /// ) + /// }); + /// ``` + fn queue_spawn_related_scenes( + &mut self, + scenes: impl SceneList, + ) -> &mut Self; + + /// Applies the given [`Scene`] to the current entity as soon as [`Commands`] are applied. This will resolve the Scene (using [`Scene::resolve`]). If that fails (for example, if there are dependencies that have not been + /// loaded yet), it will log a [`SpawnSceneError`] as an error. If resolving the [`Scene`] is successful, the scene will be spawned. + /// + /// If resolving and spawning is successful, the entity will contain the full contents of the spawned scene. + /// + /// This will write directly on top of any existing components on the entity. [`Scene`] is generally used as a spawning mechanism, so for most things, prefer using [`Commands::spawn_scene`]. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + /// + /// If your scene has a dependency that might not be loaded yet (for example, it inherits from a `.bsn` asset file), consider using [`Commands::spawn_scene`]. + fn apply_scene(&mut self, scene: S) -> &mut Self; + + /// Queues the `scene` to be applied. This will evaluate the `scene`'s dependencies (via [`Scene::register_dependencies`]) and queue it to be resolved and spawned + /// after all of the dependencies have been loaded. If a [`SpawnSceneError`] occurs, it will be logged as an error. + /// + /// If the dependencies are already loaded (or there are no dependencies), then the scene will be spawned this frame. + /// This will write directly on top of any existing components on the entity. [`Scene`] is generally used as a spawning mechanism, so for most things, prefer using [`Commands::queue_spawn_scene`]. + /// + /// See [`Scene`] for the features of the scene system (and how to use it). + fn queue_apply_scene(&mut self, scene: S) -> &mut Self; +} + +impl EntityCommandsSceneExt for EntityCommands<'_> { + fn queue_spawn_related_scenes( + &mut self, + scenes: impl SceneList, + ) -> &mut Self { + self.queue(move |entity: EntityWorldMut| { + entity.queue_spawn_related_scenes::(scenes); + }); + self + } + + fn apply_scene(&mut self, scene: S) -> &mut Self { + self.queue(move |mut entity: EntityWorldMut| entity.apply_scene(scene)); + self + } + + fn queue_apply_scene(&mut self, scene: S) -> &mut Self { + self.queue(move |mut entity: EntityWorldMut| entity.queue_apply_scene(scene)); + self + } +} + +/// A [`System`] that resolves [`ScenePatch`] and [`SceneListPatch`] assets whose dependencies have been fully loaded. +pub fn resolve_scene_patches( + mut events: MessageReader>, + mut list_events: MessageReader>, + assets: Res, + mut patches: ResMut>, + mut list_patches: ResMut>, + mut queued: ResMut, +) { + for event in events.read() { + match *event { + AssetEvent::LoadedWithDependencies { id } => { + if let Some(patch) = patches.get(id) { + match patch.resolve_internal(&assets, &patches) { + Ok(resolved) => { + let mut patch = patches.get_mut(id).unwrap(); + patch.resolved = Some(Arc::new(resolved)); + } + Err(err) => error!("Failed to resolve scene {id}: {err}"), + } + } + } + AssetEvent::Removed { id } => { + if let Some(waiting) = queued.waiting_scene_entities.remove(&id) + && !waiting.is_empty() + { + error!( + "Failed to spawn entities waiting for scene {id:?} because it was removed: {waiting:?}" + ); + } + } + _ => {} + } + } + for event in list_events.read() { + match *event { + AssetEvent::LoadedWithDependencies { id } => { + if let Some(mut list_patch) = list_patches.get_mut(id) { + match list_patch.resolve_internal(&assets, &patches) { + Ok(resolved) => { + list_patch.resolved = Some(resolved); + } + Err(err) => error!("Failed to resolve scene list {id}: {err}"), + } + } + } + AssetEvent::Removed { id } => { + if let Some(waiting) = queued.waiting_scene_list_spawns.remove(&id) + && waiting > 0 + { + error!( + "Failed to spawn scene list {id:?} {waiting} times because it was removed." + ); + } + + if let Some(waiting) = queued.waiting_related_list_entities.remove(&id) + && !waiting.is_empty() + { + let waiting_entities = waiting.iter().map(|r| r.entity).collect::>(); + error!( + "Failed to spawn related entities for scene list {id:?} because it was removed: {waiting_entities:?}" + ); + } + } + _ => {} + } + } +} + +/// A [`Resource`] that tracks entities / scenes that have been queued to spawn. +#[derive(Resource, Default)] +pub struct QueuedScenes { + new_scene_entities: Vec, + related_scene_list_spawns: Vec<(RelatedSceneListSpawn, Handle)>, + scene_list_spawns: Vec>, + waiting_scene_entities: HashMap, Vec>, + waiting_related_list_entities: HashMap, Vec>, + waiting_scene_list_spawns: HashMap, usize>, +} + +pub(crate) struct RelatedSceneListSpawn { + entity: Entity, + insert: fn(&mut EntityWorldMut, target: Entity), +} + +/// An [`Observer`] system that queues newly added [`ScenePatchInstance`] entities. +pub fn on_add_scene_patch_instance( + add: On, + mut queued_scenes: ResMut, +) { + queued_scenes.new_scene_entities.push(add.entity); +} + +/// A system that spawns queued scenes when they are loaded. +pub fn spawn_queued( + world: &mut World, + scene_patch_instances: &mut QueryState<&ScenePatchInstance>, + mut reader: Local>>, + mut list_reader: Local>>, +) { + world.resource_scope(|world, mut list_patches: Mut>| { + world.resource_scope(|world, mut queued: Mut| { + loop { + if queued.is_empty() { + break; + } + queued.spawn_queued(world, scene_patch_instances, &list_patches); + } + + world.resource_scope(|world, events: Mut>>| { + for event in reader.read(&events) { + let patches = world.resource::>(); + if let AssetEvent::LoadedWithDependencies { id } = event + && let Some(resolved) = patches.get(*id).and_then(|p| p.resolved.clone()) + && let Some(entities) = queued.waiting_scene_entities.remove(id) + { + for entity in entities { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) + && let Err(err) = resolved.apply(&mut entity_mut) + { + error!( + "Failed to apply scene (id: {}) to entity {entity}: {}", + id, err + ); + } + } + } + } + }); + world.resource_scope( + |world, list_events: Mut>>| { + for event in list_reader.read(&list_events) { + if let AssetEvent::LoadedWithDependencies { id } = event + && let Some(list_patch) = list_patches.get_mut(*id) + { + if let Some(scene_list_spawns) = + queued.waiting_related_list_entities.remove(id) + { + for scene_list_spawn in scene_list_spawns { + let result = list_patch.spawn_with(world, |entity| { + (scene_list_spawn.insert)(entity, scene_list_spawn.entity); + }); + + if let Err(err) = result { + error!("Failed to spawn scene list (id: {}): {}", id, err); + } + } + } + + if let Some(waiting_list_spawns) = + queued.waiting_scene_list_spawns.remove(id) + { + for _ in 0..waiting_list_spawns { + let result = list_patch.spawn(world); + if let Err(err) = result { + error!("Failed to spawn scene list (id: {}): {}", id, err); + } + } + } + } + } + }, + ); + }); + }); +} + +impl QueuedScenes { + fn is_empty(&self) -> bool { + self.new_scene_entities.is_empty() + && self.related_scene_list_spawns.is_empty() + && self.scene_list_spawns.is_empty() + } + + fn spawn_queued( + &mut self, + world: &mut World, + scene_patch_instances: &mut QueryState<&ScenePatchInstance>, + list_patches: &Assets, + ) { + for entity in core::mem::take(&mut self.new_scene_entities) { + let Ok(handle) = scene_patch_instances.get(world, entity).map(|h| &h.0) else { + continue; + }; + let patches = world.resource::>(); + if let Some(resolved) = patches.get(handle).and_then(|p| p.resolved.clone()) { + let mut entity_mut = world.get_entity_mut(entity).unwrap(); + if let Err(err) = resolved.apply(&mut entity_mut) { + let scene_patch_instance = scene_patch_instances.get(world, entity).unwrap(); + let handle = &scene_patch_instance.0; + let id = handle.id(); + let path = handle.path(); + error!( + "Failed to apply scene (id: {id}, path: {path:?}) to \ + entity {entity}: {err}", + ); + } + } else { + let entities = self + .waiting_scene_entities + .entry(handle.clone()) + .or_default(); + entities.push(entity); + } + } + + for (scene_list_spawn, handle) in core::mem::take(&mut self.related_scene_list_spawns) { + if let Some(list_patch) = list_patches.get(&handle) { + let result = list_patch.spawn_with(world, |entity| { + (scene_list_spawn.insert)(entity, scene_list_spawn.entity); + }); + + if let Err(err) = result { + error!( + "Failed to spawn scene list (id: {}, path: {:?}): {}", + handle.id(), + handle.path(), + err + ); + } + } else { + let entities = self + .waiting_related_list_entities + .entry(handle) + .or_default(); + entities.push(scene_list_spawn); + } + } + + for handle in core::mem::take(&mut self.scene_list_spawns) { + if let Some(list_patch) = list_patches.get(&handle) { + let result = list_patch.spawn(world); + if let Err(err) = result { + error!( + "Failed to spawn scene list (id: {}, path: {:?}): {}", + handle.id(), + handle.path(), + err + ); + } + } else { + let count = self.waiting_scene_list_spawns.entry(handle).or_default(); + *count += 1; + } + } + } +} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f503ad58f5..30b19f33a1 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -39,7 +39,7 @@ collections to build your own "profile" equivalent, without needing to manually |dev|Enable this feature during development to improve the development experience. This adds features like asset hot-reloading and debugging tools. This should not be enabled for published apps! **Feature set:** `debug`, `bevy_dev_tools`, `file_watcher`.| |audio|Features used to build audio Bevy apps. **Feature set:** `bevy_audio`, `vorbis`.| |audio-all-formats|Enables audio features and all supported formats. **Feature set:** `bevy_audio`, `aac`, `flac`, `mp3`, `mp4`, `vorbis`, `wav`.| -|scene|Features used to compose Bevy scenes. **Feature set:** `bevy_scene`.| +|scene|Features used to compose Bevy scenes. **Feature set:** `bevy_scene`, `bevy_scene2`.| |picking|Enables picking with all backends. **Feature set:** `bevy_picking`, `mesh_picking`, `sprite_picking`, `ui_picking`.| |default_app|The core pieces that most apps need. This serves as a baseline feature set for other higher level feature collections (such as "2d" and "3d"). It is also useful as a baseline feature set for scenarios like headless apps that require no rendering (ex: command line tools, servers, etc). **Feature set:** `async_executor`, `bevy_asset`, `bevy_input_focus`, `bevy_log`, `bevy_state`, `bevy_window`, `custom_cursor`, `reflect_auto_register`.| |default_platform|These are platform support features, such as OS support/features, windowing and input backends, etc. **Feature set:** `std`, `android-game-activity`, `bevy_gilrs`, `bevy_winit`, `default_font`, `multi_threaded`, `webgl2`, `x11`, `wayland`, `sysinfo_plugin`.| @@ -94,6 +94,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |bevy_remote|Enable the Bevy Remote Protocol| |bevy_render|Provides rendering functionality| |bevy_scene|Provides scene functionality| +|bevy_scene2|Provides scene functionality| |bevy_settings|Load and save user preferences| |bevy_shader|Provides shaders usable through asset handles.| |bevy_solari|Provides raytraced lighting (experimental)| diff --git a/examples/README.md b/examples/README.md index 4f92e847c1..29b24f9925 100644 --- a/examples/README.md +++ b/examples/README.md @@ -467,6 +467,7 @@ Example | Description Example | Description --- | --- +[BSN example](../examples/scene/bsn.rs) | Demonstrates how to use BSN to compose scenes [Scene](../examples/scene/scene.rs) | Demonstrates loading from and saving scenes to files ### Shaders diff --git a/examples/scene/bsn.rs b/examples/scene/bsn.rs new file mode 100644 index 0000000000..73609b7ad7 --- /dev/null +++ b/examples/scene/bsn.rs @@ -0,0 +1,72 @@ +//! This example demonstrates how to use BSN to compose scenes. +use bevy::{ + ecs::template::template, + prelude::*, + scene2::prelude::{Scene, *}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(world: &mut World) -> Result { + world.spawn_scene_list(bsn_list![Camera2d, ui()])?; + Ok(()) +} + +fn ui() -> impl Scene { + bsn! { + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + column_gap: Val::Px(5.), + } + Children [ + ( + button("Ok") + on(|_event: On>| println!("Ok pressed!")) + ), + ( + button("Cancel") + on(|_event: On>| println!("Cancel pressed!")) + BackgroundColor(Color::srgb(0.4, 0.15, 0.15)) + ), + ] + } +} + +fn button(label: &'static str) -> impl Scene { + bsn! { + Button + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + border_radius: BorderRadius::MAX, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + } + BorderColor::from(Color::BLACK) + BackgroundColor(Color::srgb(0.15, 0.15, 0.15)) + Children [( + Text(label) + // The `template` wrapper can be used for types that can't implement or don't yet have a template + template(|context| { + Ok(TextFont { + font: context + .resource::() + .load("fonts/FiraSans-Bold.ttf").into(), + font_size: FontSize::Px(33.0), + ..default() + }) + }) + TextColor(Color::srgb(0.9, 0.9, 0.9)) + TextShadow + )] + } +} diff --git a/release-content/release-notes/next-generation-scenes.md b/release-content/release-notes/next-generation-scenes.md new file mode 100644 index 0000000000..b081683025 --- /dev/null +++ b/release-content/release-notes/next-generation-scenes.md @@ -0,0 +1,7 @@ +--- +title: "Next Generation Scenes" +authors: ["@cart"] +pull_requests: [23413] +--- + +TODO: Fill this in!